From 98a1c7a67adc08b7c218e269990d655c0da6968c Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Mon, 19 Aug 2024 16:32:26 -0400 Subject: [PATCH 01/34] Add route for serving raw example template --- webapp/app.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index fc6a24b89..f250aa464 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -3,6 +3,7 @@ import json import os import random +import werkzeug import yaml import urllib import markupsafe @@ -286,16 +287,33 @@ def standalone_examples_index(): ) -@app.route("/docs/examples/standalone/") -def standalone_example(example_path): +@app.route("/docs/examples/") +def example(example_path, is_standalone=False, is_raw=False): try: + # If the user has requested the raw template, serve it directly + if is_raw: + raw_example_path = f"../templates/docs/examples/{example_path}.html" + # separate directory from file name so that flask.send_from_directory() can prevent malicious file access + raw_example_directory = os.path.dirname(raw_example_path) + raw_example_file_name = os.path.basename(raw_example_path) + return flask.send_from_directory(raw_example_directory, raw_example_file_name, mimetype="text/raw", max_age=86400) + return flask.render_template( - f"docs/examples/{example_path}.html", is_standalone=True + f"docs/examples/{example_path}.html", is_standalone=is_standalone ) - except jinja2.exceptions.TemplateNotFound: + except (jinja2.exceptions.TemplateNotFound, werkzeug.exceptions.NotFound): return flask.abort(404) +@app.route("/docs/examples/standalone/") +def standalone_example(example_path): + return example(example_path, is_standalone=True) + + +@app.route("/examples/") +def example_raw(example_path): + return example(example_path, is_raw=True) + @app.route("/contribute") def contribute_index(): all_contributors = _get_contributors() From b448992513d8357252afa1820f2b0724fabecbfa Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Mon, 19 Aug 2024 17:25:12 -0400 Subject: [PATCH 02/34] Render flask template in HTML code snippet --- templates/static/js/example.js | 49 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index f214336e8..4e630602e 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -38,20 +38,33 @@ [].slice.call(examples).forEach(fetchExample); }); - function fetchExample(exampleElement) { - var link = exampleElement.href; + async function fetchResponseText(url) { var request = new XMLHttpRequest(); - request.onreadystatechange = function () { - if (request.status === 200 && request.readyState === 4) { - var html = request.responseText; - renderExample(exampleElement, html); - exampleElement.style.display = 'none'; - } - }; + const prms = new Promise(function(resolve, reject) { + request.onreadystatechange = function () { + if (request.status === 200 && request.readyState === 4) { + resolve(request.responseText); + } else if (request.status > 0 && (request.status < 200 || request.status >= 300)) { + reject('Failed to fetch example ' + url + ' with status ' + request.status); + } + }; + }); - request.open('GET', link, true); + request.open('GET', url, true); request.send(null); + + return prms; + } + + async function fetchExample(exampleElement) { + const [renderedHtml, rawHtml] = await Promise.all([ + fetchResponseText(exampleElement.href), + fetchResponseText(exampleElement.href.replace(/docs/, '/').replace(/standalone/, '/')) + ]); + + renderExample(exampleElement, renderedHtml, rawHtml); + exampleElement.style.display = 'none'; } /** @@ -102,19 +115,21 @@ return pre; } - function renderExample(placementElement, html) { + function renderExample(placementElement, renderedHtml, rawHtml) { var bodyPattern = /]*>((.|[\n\r])*)<\/body>/im; + var contentPattern = /{% block content %}([\s\S]*?){% endblock %}/; var titlePattern = /]*>((.|[\n\r])*)<\/title>/im; var headPattern = /]*>((.|[\n\r])*)<\/head>/im; - var title = titlePattern.exec(html)[1].trim(); - var bodyHTML = bodyPattern.exec(html)[1].trim(); - var headHTML = headPattern.exec(html)[1].trim(); + var title = titlePattern.exec(renderedHtml)[1].trim(); + var bodyHTML = bodyPattern.exec(renderedHtml)[1].trim(); + var headHTML = headPattern.exec(renderedHtml)[1].trim(); + var contentTemplate = contentPattern.exec(rawHtml)[1].trim(); var htmlSource = stripScriptsFromSource(bodyHTML); var jsSource = getScriptFromSource(bodyHTML); var cssSource = getStyleFromSource(headHTML); - var externalScripts = getExternalScriptsFromSource(html); + var externalScripts = getExternalScriptsFromSource(renderedHtml); var codePenData = { html: htmlSource, css: cssSource, @@ -141,11 +156,11 @@ codeSnippet.appendChild(header); placementElement.parentNode.insertBefore(codeSnippet, placementElement); - renderIframe(codeSnippet, html, height); + renderIframe(codeSnippet, renderedHtml, height); // Build code block structure var options = ['html']; - codeSnippet.appendChild(createPreCode(htmlSource, 'html')); + codeSnippet.appendChild(createPreCode(contentTemplate, 'html')); if (jsSource) { codeSnippet.appendChild(createPreCode(jsSource, 'js')); options.push('js'); From 3209a10c495418bf70b9404ffa76d29d631646c0 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 22 Aug 2024 13:16:13 -0400 Subject: [PATCH 03/34] Show rendered/raw example --- guides/examples.md | 2 ++ templates/docs/patterns/hero/index.md | 14 ++++++------- templates/docs/patterns/tiered-list/index.md | 12 +++++------ templates/static/js/example.js | 21 +++++++++++++++----- 4 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 guides/examples.md diff --git a/guides/examples.md b/guides/examples.md new file mode 100644 index 000000000..7846b5dd8 --- /dev/null +++ b/guides/examples.md @@ -0,0 +1,2 @@ +## two classes +## `data-height` \ No newline at end of file diff --git a/templates/docs/patterns/hero/index.md b/templates/docs/patterns/hero/index.md index e0bda1c51..83e88f4b4 100644 --- a/templates/docs/patterns/hero/index.md +++ b/templates/docs/patterns/hero/index.md @@ -124,7 +124,7 @@ This is useful when your hero contents, especially your image, are not suitably This makes your hero somewhat safer to use, as it helps to avoid awkward content sizing on medium screens, making all content stack vertically. -
+ @@ -134,7 +134,7 @@ You can use .row--50-50 to create a 50/50 hero that is split on lar This is useful when your available vertical space is limited, and your hero contents are suitably balanced to be viewed side-by-side on medium screens. -
+ @@ -147,7 +147,7 @@ the first column and place the image in a .p-image-container .is-cover + @@ -156,7 +156,7 @@ View example of the hero pattern in 50/50 split with a full-width image If you have a small image that you want to associate with the hero title, you can use the "signpost" layout. This places the image in a small column beside the primary hero content. -
+ @@ -164,7 +164,7 @@ This layout also supports a full-width image. Place the image in a .p-imag level as the hero grid columns to make it take full width beneath the rest of the hero. This is identical to the full-width image layout for the [50/50 layout](#50-50-with-full-width-image). -
+ @@ -177,7 +177,7 @@ The .row--75-25 class is used to maintain the 75/25 split on medium If you find that the image is too tall on small screens, you can use .u-hide--small to hide the image on small screens. -
+ @@ -187,7 +187,7 @@ If you have a very large amount of text content that is difficult to balance wit fallback layout. This places the title and subtitle in their own row above the rest of the hero content. -
+ diff --git a/templates/docs/patterns/tiered-list/index.md b/templates/docs/patterns/tiered-list/index.md index 9487731e0..7bfe35332 100644 --- a/templates/docs/patterns/tiered-list/index.md +++ b/templates/docs/patterns/tiered-list/index.md @@ -32,7 +32,7 @@ The tiered list pattern is composed of the following elements: This variant contains a top-level description which is presented side-by-side with its title on desktop screen sizes. -
+ @@ -42,7 +42,7 @@ This variant does not contain a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -51,7 +51,7 @@ View example of the tiered list pattern This variant contains a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -61,7 +61,7 @@ This variant contains a top-level description. Its title and description are presented side-by-side on desktop screen sizes, and its child list is presented side-by-side on tablet screen sizes. -
+ @@ -71,7 +71,7 @@ This variant does not contain a top-level description, and both its title and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ @@ -81,7 +81,7 @@ This variant contains a top-level description, and its title, description, and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 4e630602e..463f1339a 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -58,10 +58,20 @@ } async function fetchExample(exampleElement) { - const [renderedHtml, rawHtml] = await Promise.all([ - fetchResponseText(exampleElement.href), - fetchResponseText(exampleElement.href.replace(/docs/, '/').replace(/standalone/, '/')) - ]); + // TODO - integrate fetching/rendering more cleanly in future + const fetchRendered = fetchResponseText(exampleElement.href); + var proms = [fetchRendered]; + // If the example requires raw template rendering, request the raw template file as well + if (exampleElement.classList.contains('js-show-template')) { + const fetchRaw = fetchResponseText( + exampleElement.href + .replace(/docs/, '/') + .replace(/standalone/, '/') + ); + proms.push(fetchRaw); + } + + const [renderedHtml, rawHtml] = await Promise.all(proms); renderExample(exampleElement, renderedHtml, rawHtml); exampleElement.style.display = 'none'; @@ -124,7 +134,8 @@ var title = titlePattern.exec(renderedHtml)[1].trim(); var bodyHTML = bodyPattern.exec(renderedHtml)[1].trim(); var headHTML = headPattern.exec(renderedHtml)[1].trim(); - var contentTemplate = contentPattern.exec(rawHtml)[1].trim(); + // Use the raw HTML if it was passed in. Otherwise, use the rendered HTML. + var contentTemplate = (rawHtml ? contentPattern.exec(rawHtml) : bodyPattern.exec(renderedHtml))[1].trim(); var htmlSource = stripScriptsFromSource(bodyHTML); var jsSource = getScriptFromSource(bodyHTML); From 5daa5122509a1b92c5fc012c4223941455aa7d4a Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 22 Aug 2024 14:29:30 -0400 Subject: [PATCH 04/34] Enable switching between Jinja code snippets and rendered HTML --- templates/static/js/example.js | 94 +++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 463f1339a..5c320322e 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -3,6 +3,28 @@ throw Error('VANILLA_VERSION not specified.'); } + /** + * Mapping of example keys to the regex patterns used to strip them out of an example + * @type {{body: RegExp, jinja: RegExp, title: RegExp, head: RegExp}} + */ + const exampleContentPatterns = { + body: /]*>((.|[\n\r])*)<\/body>/im, + jinja: /{% block content %}([\s\S]*?){% endblock %}/, + title: /]*>((.|[\n\r])*)<\/title>/im, + head: /]*>((.|[\n\r])*)<\/head>/im + } + + /** + * Mapping of all example mode options to their labels + * @type {{html: string, css: string, js: string, jinja: string}} + */ + const exampleOptionLabels = { + 'html': "HTML", + 'css': "CSS", + 'js': "JS", + 'jinja': "Jinja" + } + // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ var throttle = function (fn, delay) { @@ -57,6 +79,10 @@ return prms; } + /** + * Fetches the requested example and replaces the example element with the content and code snippet of the example. + * @param {HTMLAnchorElement} exampleElement `a.js-example` element with `href` set to the address of the example to fetch + */ async function fetchExample(exampleElement) { // TODO - integrate fetching/rendering more cleanly in future const fetchRendered = fetchResponseText(exampleElement.href); @@ -80,13 +106,14 @@ /** * Format source code based on language * @param {String} source - source code to format - * @param {String} lang - language of the source code + * @param {'html'|'jinja'|'js'|'css'} lang - language of the source code * @returns {String} formatted source code */ function formatSource(source, lang) { try { switch (lang) { case 'html': + case 'jinja': return window.html_beautify(source, {indent_size: 2}); case 'js': return window.js_beautify(source, {indent_size: 2}); @@ -102,7 +129,14 @@ } } - function createPreCode(source, lang) { + /** + * Create `pre`-formatted code for a block of source + * @param {String} source Unformatted source code + * @param {'html'|'jinja'|'js'|'css'} lang Language of the source code + * @param {Boolean} hide Whether the pre-code should be hidden initially + * @returns {HTMLPreElement} Code snippet containing the source code + */ + function createPreCode(source, lang, hide=true) { var code = document.createElement('code'); code.appendChild(document.createTextNode(formatSource(source, lang))); @@ -112,7 +146,7 @@ // TODO: move max-height elsewhere to CSS? pre.style.maxHeight = '300px'; - if (lang !== 'html') { + if (hide) { pre.classList.add('u-hide'); } @@ -125,17 +159,29 @@ return pre; } - function renderExample(placementElement, renderedHtml, rawHtml) { - var bodyPattern = /]*>((.|[\n\r])*)<\/body>/im; - var contentPattern = /{% block content %}([\s\S]*?){% endblock %}/; - var titlePattern = /]*>((.|[\n\r])*)<\/title>/im; - var headPattern = /]*>((.|[\n\r])*)<\/head>/im; + /** + * Extract a section of HTML from the document + * @param {'body'|'jinja'|'title'|'head'} sectionKey The key/type of content to be extracted + * @param {String} documentHTML The example's full HTML content. This may be rendered or raw Jinja template. + * @returns {String} The requested section of the document, or an empty string if it was not found. + */ + function getExampleSection(sectionKey, documentHTML) { + const pattern = exampleContentPatterns[sectionKey]; + return pattern?.exec(documentHTML)?.[1]?.trim() || ""; + } - var title = titlePattern.exec(renderedHtml)[1].trim(); - var bodyHTML = bodyPattern.exec(renderedHtml)[1].trim(); - var headHTML = headPattern.exec(renderedHtml)[1].trim(); - // Use the raw HTML if it was passed in. Otherwise, use the rendered HTML. - var contentTemplate = (rawHtml ? contentPattern.exec(rawHtml) : bodyPattern.exec(renderedHtml))[1].trim(); + /** + * Replaces an example placeholder element with its rendered result and code snippet. + * @param {HTMLAnchorElement} placementElement `a.js-example` element used as a placeholder for the example to render + * @param {String} renderedHtml Full document HTML of the example as it is shown to end-users + * @param {String|null} jinjaTemplate Jinja Template of the example as it may be used by developers, if supported + */ + function renderExample(placementElement, renderedHtml, jinjaTemplate) { + const bodyHTML = getExampleSection('body', renderedHtml); + const headHTML = getExampleSection('head', renderedHtml); + const title = getExampleSection('title', renderedHtml); + const templateHtml = getExampleSection('jinja', jinjaTemplate); + const hasJinjaTemplate = templateHtml?.length > 0; var htmlSource = stripScriptsFromSource(bodyHTML); var jsSource = getScriptFromSource(bodyHTML); @@ -171,7 +217,12 @@ // Build code block structure var options = ['html']; - codeSnippet.appendChild(createPreCode(contentTemplate, 'html')); + if (hasJinjaTemplate) { + codeSnippet.appendChild(createPreCode(templateHtml, 'jinja', false)); + // Make sure Jinja comes first if it's supported, so it's the default option + options.unshift('jinja') + } + codeSnippet.appendChild(createPreCode(bodyHTML, 'html', hasJinjaTemplate)); if (jsSource) { codeSnippet.appendChild(createPreCode(jsSource, 'js')); options.push('js'); @@ -189,24 +240,29 @@ } } - function renderDropdown(header, options) { + /** + * Renders a dropdown containing the code snippet options, allowing user to switch between multiple views. + * @param {HTMLDivElement} codeSnippetHeader The header element of the code snippet + * @param {('html'|'jinja'|'js'|'css')[]} codeSnippetModes List of code snippet mode options + */ + function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block - if (options.length > 1) { + if (codeSnippetModes.length > 1) { var dropdownsEl = document.createElement('div'); dropdownsEl.classList.add('p-code-snippet__dropdowns'); var selectEl = document.createElement('select'); selectEl.classList.add('p-code-snippet__dropdown'); - options.forEach(function (option) { + codeSnippetModes.forEach(function (option) { var optionHTML = document.createElement('option'); optionHTML.value = option.toLowerCase(); - optionHTML.innerText = option.toUpperCase(); + optionHTML.innerText = exampleOptionLabels[option] || option.toLowerCase(); selectEl.appendChild(optionHTML); }); dropdownsEl.appendChild(selectEl); - header.appendChild(dropdownsEl); + codeSnippetHeader.appendChild(dropdownsEl); attachDropdownEvents(selectEl); } } From 498347e7b70577c6c53533c38e605e33769b816b Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 22 Aug 2024 14:46:32 -0400 Subject: [PATCH 05/34] Slight example.js docs improvements & refactors --- templates/static/js/example.js | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 5c320322e..da7ef2bdc 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -60,10 +60,15 @@ [].slice.call(examples).forEach(fetchExample); }); - async function fetchResponseText(url) { + /** + * Sends a `GET` request to `url` to request an example's contents. + * @param {String} url Address of the example + * @returns {Promise} Response text + */ + async function fetchExampleResponseText(url) { var request = new XMLHttpRequest(); - const prms = new Promise(function(resolve, reject) { + const result = new Promise(function(resolve, reject) { request.onreadystatechange = function () { if (request.status === 200 && request.readyState === 4) { resolve(request.responseText); @@ -76,7 +81,7 @@ request.open('GET', url, true); request.send(null); - return prms; + return result; } /** @@ -85,22 +90,26 @@ */ async function fetchExample(exampleElement) { // TODO - integrate fetching/rendering more cleanly in future - const fetchRendered = fetchResponseText(exampleElement.href); - var proms = [fetchRendered]; + /** Rendered HTML that will be seen by users */ + const fetchRendered = fetchExampleResponseText(exampleElement.href); + + var exampleRequests = [fetchRendered]; + // If the example requires raw template rendering, request the raw template file as well if (exampleElement.classList.contains('js-show-template')) { - const fetchRaw = fetchResponseText( + const fetchRaw = fetchExampleResponseText( exampleElement.href + // Raw templates are served at `/`, without `/docs/` in front. Remove `/docs/`. .replace(/docs/, '/') + // Raw templates are not served at standalone paths, so strip it from the URL if it was found. .replace(/standalone/, '/') ); - proms.push(fetchRaw); + exampleRequests.push(fetchRaw); } - const [renderedHtml, rawHtml] = await Promise.all(proms); + const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); renderExample(exampleElement, renderedHtml, rawHtml); - exampleElement.style.display = 'none'; } /** @@ -180,8 +189,8 @@ const bodyHTML = getExampleSection('body', renderedHtml); const headHTML = getExampleSection('head', renderedHtml); const title = getExampleSection('title', renderedHtml); - const templateHtml = getExampleSection('jinja', jinjaTemplate); - const hasJinjaTemplate = templateHtml?.length > 0; + const templateHTML = getExampleSection('jinja', jinjaTemplate); + const hasJinjaTemplate = templateHTML?.length > 0; var htmlSource = stripScriptsFromSource(bodyHTML); var jsSource = getScriptFromSource(bodyHTML); @@ -218,7 +227,7 @@ // Build code block structure var options = ['html']; if (hasJinjaTemplate) { - codeSnippet.appendChild(createPreCode(templateHtml, 'jinja', false)); + codeSnippet.appendChild(createPreCode(templateHTML, 'jinja', false)); // Make sure Jinja comes first if it's supported, so it's the default option options.unshift('jinja') } @@ -238,6 +247,9 @@ if (Prism) { Prism.highlightAllUnder(codeSnippet); } + + // The example has been rendered successfully, hide the placeholder element. + placementElement.style.display = 'none'; } /** From ba05a026f86e8990706ddac6b81af8acead49720 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 22 Aug 2024 14:47:03 -0400 Subject: [PATCH 06/34] Run prettier --- guides/examples.md | 3 ++- templates/static/js/example.js | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/guides/examples.md b/guides/examples.md index 7846b5dd8..8889f5db7 100644 --- a/guides/examples.md +++ b/guides/examples.md @@ -1,2 +1,3 @@ ## two classes -## `data-height` \ No newline at end of file + +## `data-height` diff --git a/templates/static/js/example.js b/templates/static/js/example.js index da7ef2bdc..820daa3c8 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -11,19 +11,19 @@ body: /]*>((.|[\n\r])*)<\/body>/im, jinja: /{% block content %}([\s\S]*?){% endblock %}/, title: /]*>((.|[\n\r])*)<\/title>/im, - head: /]*>((.|[\n\r])*)<\/head>/im - } + head: /]*>((.|[\n\r])*)<\/head>/im, + }; /** * Mapping of all example mode options to their labels * @type {{html: string, css: string, js: string, jinja: string}} */ const exampleOptionLabels = { - 'html': "HTML", - 'css': "CSS", - 'js': "JS", - 'jinja': "Jinja" - } + html: 'HTML', + css: 'CSS', + js: 'JS', + jinja: 'Jinja', + }; // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ @@ -68,7 +68,7 @@ async function fetchExampleResponseText(url) { var request = new XMLHttpRequest(); - const result = new Promise(function(resolve, reject) { + const result = new Promise(function (resolve, reject) { request.onreadystatechange = function () { if (request.status === 200 && request.readyState === 4) { resolve(request.responseText); @@ -102,7 +102,7 @@ // Raw templates are served at `/`, without `/docs/` in front. Remove `/docs/`. .replace(/docs/, '/') // Raw templates are not served at standalone paths, so strip it from the URL if it was found. - .replace(/standalone/, '/') + .replace(/standalone/, '/'), ); exampleRequests.push(fetchRaw); } @@ -145,7 +145,7 @@ * @param {Boolean} hide Whether the pre-code should be hidden initially * @returns {HTMLPreElement} Code snippet containing the source code */ - function createPreCode(source, lang, hide=true) { + function createPreCode(source, lang, hide = true) { var code = document.createElement('code'); code.appendChild(document.createTextNode(formatSource(source, lang))); @@ -176,7 +176,7 @@ */ function getExampleSection(sectionKey, documentHTML) { const pattern = exampleContentPatterns[sectionKey]; - return pattern?.exec(documentHTML)?.[1]?.trim() || ""; + return pattern?.exec(documentHTML)?.[1]?.trim() || ''; } /** @@ -229,7 +229,7 @@ if (hasJinjaTemplate) { codeSnippet.appendChild(createPreCode(templateHTML, 'jinja', false)); // Make sure Jinja comes first if it's supported, so it's the default option - options.unshift('jinja') + options.unshift('jinja'); } codeSnippet.appendChild(createPreCode(bodyHTML, 'html', hasJinjaTemplate)); if (jsSource) { From 7885811e36125fd800eb824d3c9342c5ab09cf13 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Mon, 26 Aug 2024 14:13:25 -0400 Subject: [PATCH 07/34] RM example guide --- guides/examples.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 guides/examples.md diff --git a/guides/examples.md b/guides/examples.md deleted file mode 100644 index 8889f5db7..000000000 --- a/guides/examples.md +++ /dev/null @@ -1,3 +0,0 @@ -## two classes - -## `data-height` From e0899f181922e58f4bf6dbec561ae684d81a4cd5 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Wed, 28 Aug 2024 12:27:58 -0400 Subject: [PATCH 08/34] Switch jinja example trigger from .js-show-template to [data-lang='jinja'] --- templates/docs/patterns/hero/index.md | 14 +++++++------- templates/docs/patterns/tiered-list/index.md | 12 ++++++------ templates/static/js/example.js | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/templates/docs/patterns/hero/index.md b/templates/docs/patterns/hero/index.md index 83e88f4b4..c89f7dcf6 100644 --- a/templates/docs/patterns/hero/index.md +++ b/templates/docs/patterns/hero/index.md @@ -124,7 +124,7 @@ This is useful when your hero contents, especially your image, are not suitably This makes your hero somewhat safer to use, as it helps to avoid awkward content sizing on medium screens, making all content stack vertically. -
+ @@ -134,7 +134,7 @@ You can use .row--50-50 to create a 50/50 hero that is split on lar This is useful when your available vertical space is limited, and your hero contents are suitably balanced to be viewed side-by-side on medium screens. -
+ @@ -147,7 +147,7 @@ the first column and place the image in a .p-image-container .is-cover + @@ -156,7 +156,7 @@ View example of the hero pattern in 50/50 split with a full-width image If you have a small image that you want to associate with the hero title, you can use the "signpost" layout. This places the image in a small column beside the primary hero content. -
+ @@ -164,7 +164,7 @@ This layout also supports a full-width image. Place the image in a .p-imag level as the hero grid columns to make it take full width beneath the rest of the hero. This is identical to the full-width image layout for the [50/50 layout](#50-50-with-full-width-image). -
+ @@ -177,7 +177,7 @@ The .row--75-25 class is used to maintain the 75/25 split on medium If you find that the image is too tall on small screens, you can use .u-hide--small to hide the image on small screens. -
+ @@ -187,7 +187,7 @@ If you have a very large amount of text content that is difficult to balance wit fallback layout. This places the title and subtitle in their own row above the rest of the hero content. -
+ diff --git a/templates/docs/patterns/tiered-list/index.md b/templates/docs/patterns/tiered-list/index.md index 7bfe35332..846c3810a 100644 --- a/templates/docs/patterns/tiered-list/index.md +++ b/templates/docs/patterns/tiered-list/index.md @@ -32,7 +32,7 @@ The tiered list pattern is composed of the following elements: This variant contains a top-level description which is presented side-by-side with its title on desktop screen sizes. -
+ @@ -42,7 +42,7 @@ This variant does not contain a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -51,7 +51,7 @@ View example of the tiered list pattern This variant contains a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -61,7 +61,7 @@ This variant contains a top-level description. Its title and description are presented side-by-side on desktop screen sizes, and its child list is presented side-by-side on tablet screen sizes. -
+ @@ -71,7 +71,7 @@ This variant does not contain a top-level description, and both its title and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ @@ -81,7 +81,7 @@ This variant contains a top-level description, and its title, description, and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 820daa3c8..6e8e6d665 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -96,7 +96,7 @@ var exampleRequests = [fetchRendered]; // If the example requires raw template rendering, request the raw template file as well - if (exampleElement.classList.contains('js-show-template')) { + if (exampleElement.getAttribute('data-lang') === 'jinja') { const fetchRaw = fetchExampleResponseText( exampleElement.href // Raw templates are served at `/`, without `/docs/` in front. Remove `/docs/`. From 6dca5ff0d0453cad40259f2a4aa6cb0852ae2d9b Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Wed, 28 Aug 2024 12:39:55 -0400 Subject: [PATCH 09/34] Use raw query parameter instead of `/example` path --- templates/static/js/example.js | 9 ++++++--- webapp/app.py | 7 ++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 6e8e6d665..409466c35 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -97,10 +97,13 @@ // If the example requires raw template rendering, request the raw template file as well if (exampleElement.getAttribute('data-lang') === 'jinja') { + var exampleURL = new URL(exampleElement.href); + var queryParams = new URLSearchParams(exampleURL.search); + queryParams.set('raw', true); + exampleURL.search = queryParams.toString(); + const fetchRaw = fetchExampleResponseText( - exampleElement.href - // Raw templates are served at `/`, without `/docs/` in front. Remove `/docs/`. - .replace(/docs/, '/') + exampleURL.href // Raw templates are not served at standalone paths, so strip it from the URL if it was found. .replace(/standalone/, '/'), ); diff --git a/webapp/app.py b/webapp/app.py index f250aa464..914234d59 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -288,8 +288,9 @@ def standalone_examples_index(): @app.route("/docs/examples/") -def example(example_path, is_standalone=False, is_raw=False): +def example(example_path, is_standalone=False): try: + is_raw = (flask.request.args.get("raw") or "").lower() == "true" # If the user has requested the raw template, serve it directly if is_raw: raw_example_path = f"../templates/docs/examples/{example_path}.html" @@ -310,10 +311,6 @@ def standalone_example(example_path): return example(example_path, is_standalone=True) -@app.route("/examples/") -def example_raw(example_path): - return example(example_path, is_raw=True) - @app.route("/contribute") def contribute_index(): all_contributors = _get_contributors() From 1421e274bcc16ae83fdf0165badf4b17c42a59b3 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 14:55:12 -0400 Subject: [PATCH 10/34] Syntax-highlight HTML within Jinja template macro calls --- templates/static/js/example.js | 42 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 409466c35..2b0a612ef 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -7,7 +7,7 @@ * Mapping of example keys to the regex patterns used to strip them out of an example * @type {{body: RegExp, jinja: RegExp, title: RegExp, head: RegExp}} */ - const exampleContentPatterns = { + const EXAMPLE_CONTENT_PATTERNS = { body: /]*>((.|[\n\r])*)<\/body>/im, jinja: /{% block content %}([\s\S]*?){% endblock %}/, title: /]*>((.|[\n\r])*)<\/title>/im, @@ -15,14 +15,34 @@ }; /** - * Mapping of all example mode options to their labels - * @type {{html: string, css: string, js: string, jinja: string}} + * Object representing the structure for language option mappings. + * @typedef {Object} ExampleLanguageConfig + * @property {string} label - Human-readable label. + * @property {string} langIdentifier - Prism language identifier. */ - const exampleOptionLabels = { - html: 'HTML', - css: 'CSS', - js: 'JS', - jinja: 'Jinja', + + /** + * Mapping of example keys to their configurations. + * @type {{jinja: ExampleLanguageConfig, css: ExampleLanguageConfig, js: ExampleLanguageConfig, html: ExampleLanguageConfig}} + */ + const EXAMPLE_OPTIONS_CFG = { + html: { + label: 'HTML', + langIdentifier: 'html', + }, + css: { + label: 'CSS', + langIdentifier: 'css', + }, + js: { + label: 'JS', + langIdentifier: 'js', + }, + jinja: { + label: 'Jinja', + // While `jinja2` is an option on Prism, it does not seem to highlight syntax properly. So use HTML instead. + langIdentifier: 'html', + }, }; // throttling function calls, by Remy Sharp @@ -164,7 +184,7 @@ if (lang) { pre.setAttribute('data-lang', lang); - pre.classList.add('language-' + lang); + pre.classList.add('language-' + (EXAMPLE_OPTIONS_CFG[lang].langIdentifier || lang)); } pre.appendChild(code); @@ -178,7 +198,7 @@ * @returns {String} The requested section of the document, or an empty string if it was not found. */ function getExampleSection(sectionKey, documentHTML) { - const pattern = exampleContentPatterns[sectionKey]; + const pattern = EXAMPLE_CONTENT_PATTERNS[sectionKey]; return pattern?.exec(documentHTML)?.[1]?.trim() || ''; } @@ -272,7 +292,7 @@ codeSnippetModes.forEach(function (option) { var optionHTML = document.createElement('option'); optionHTML.value = option.toLowerCase(); - optionHTML.innerText = exampleOptionLabels[option] || option.toLowerCase(); + optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); selectEl.appendChild(optionHTML); }); From 2819507616efcd5c4cae03dcf5d7d6eded05567d Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 14:56:55 -0400 Subject: [PATCH 11/34] Optional chain to avoid possible null index error --- templates/static/js/example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 2b0a612ef..67349c225 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -184,7 +184,7 @@ if (lang) { pre.setAttribute('data-lang', lang); - pre.classList.add('language-' + (EXAMPLE_OPTIONS_CFG[lang].langIdentifier || lang)); + pre.classList.add('language-' + (EXAMPLE_OPTIONS_CFG[lang]?.langIdentifier || lang)); } pre.appendChild(code); From 7c1330e6428a907e0a2d7998e4879ae0816c5d06 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 15:04:57 -0400 Subject: [PATCH 12/34] Make HTML the default example language, instead of Jinja --- templates/static/js/example.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 67349c225..91aac4b23 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -249,12 +249,11 @@ // Build code block structure var options = ['html']; + codeSnippet.appendChild(createPreCode(bodyHTML, 'html', false)); if (hasJinjaTemplate) { - codeSnippet.appendChild(createPreCode(templateHTML, 'jinja', false)); - // Make sure Jinja comes first if it's supported, so it's the default option - options.unshift('jinja'); + codeSnippet.appendChild(createPreCode(templateHTML, 'jinja')); + options.push('jinja'); } - codeSnippet.appendChild(createPreCode(bodyHTML, 'html', hasJinjaTemplate)); if (jsSource) { codeSnippet.appendChild(createPreCode(jsSource, 'js')); options.push('js'); From d00ba3af6bc3ee04b78e17992a4959eb243d7d37 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 15:42:31 -0400 Subject: [PATCH 13/34] Simplify example xmlhttprequest check --- templates/static/js/example.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 91aac4b23..e5206d6b1 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -90,9 +90,15 @@ const result = new Promise(function (resolve, reject) { request.onreadystatechange = function () { + // If the request is not complete, do nothing + if (request.readyState !== 4) { + return; + } + // Request is complete and successful if (request.status === 200 && request.readyState === 4) { resolve(request.responseText); - } else if (request.status > 0 && (request.status < 200 || request.status >= 300)) { + } else { + // Request failed reject('Failed to fetch example ' + url + ' with status ' + request.status); } }; @@ -130,9 +136,12 @@ exampleRequests.push(fetchRaw); } - const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); - - renderExample(exampleElement, renderedHtml, rawHtml); + try { + const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); + renderExample(exampleElement, renderedHtml, rawHtml); + } catch(err) { + console.error(err); + } } /** From 487238e0cc4b89c3f030619f39384eadf9578891 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:07:04 -0400 Subject: [PATCH 14/34] Change initializer keywords to `let` or `const` --- templates/static/js/example.js | 103 ++++++++++++++++----------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index e5206d6b1..2445d1856 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -47,10 +47,10 @@ // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function (fn, delay) { - var timer = null; + const throttle = function (fn, delay) { + let timer = null; return function () { - var context = this, + let context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { @@ -59,7 +59,7 @@ }; }; - var CODEPEN_CONFIG = { + const CODEPEN_CONFIG = { title: 'Vanilla framework example', head: "", stylesheets: [ @@ -75,7 +75,7 @@ }; document.addEventListener('DOMContentLoaded', function () { - var examples = document.querySelectorAll('.js-example'); + const examples = document.querySelectorAll('.js-example'); [].slice.call(examples).forEach(fetchExample); }); @@ -86,7 +86,7 @@ * @returns {Promise} Response text */ async function fetchExampleResponseText(url) { - var request = new XMLHttpRequest(); + let request = new XMLHttpRequest(); const result = new Promise(function (resolve, reject) { request.onreadystatechange = function () { @@ -119,12 +119,12 @@ /** Rendered HTML that will be seen by users */ const fetchRendered = fetchExampleResponseText(exampleElement.href); - var exampleRequests = [fetchRendered]; + let exampleRequests = [fetchRendered]; // If the example requires raw template rendering, request the raw template file as well if (exampleElement.getAttribute('data-lang') === 'jinja') { - var exampleURL = new URL(exampleElement.href); - var queryParams = new URLSearchParams(exampleURL.search); + let exampleURL = new URL(exampleElement.href); + let queryParams = new URLSearchParams(exampleURL.search); queryParams.set('raw', true); exampleURL.search = queryParams.toString(); @@ -139,7 +139,7 @@ try { const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); renderExample(exampleElement, renderedHtml, rawHtml); - } catch(err) { + } catch (err) { console.error(err); } } @@ -178,10 +178,10 @@ * @returns {HTMLPreElement} Code snippet containing the source code */ function createPreCode(source, lang, hide = true) { - var code = document.createElement('code'); + let code = document.createElement('code'); code.appendChild(document.createTextNode(formatSource(source, lang))); - var pre = document.createElement('pre'); + let pre = document.createElement('pre'); pre.classList.add('p-code-snippet__block'); // TODO: move max-height elsewhere to CSS? @@ -224,26 +224,26 @@ const templateHTML = getExampleSection('jinja', jinjaTemplate); const hasJinjaTemplate = templateHTML?.length > 0; - var htmlSource = stripScriptsFromSource(bodyHTML); - var jsSource = getScriptFromSource(bodyHTML); - var cssSource = getStyleFromSource(headHTML); - var externalScripts = getExternalScriptsFromSource(renderedHtml); - var codePenData = { + const htmlSource = stripScriptsFromSource(bodyHTML); + const jsSource = getScriptFromSource(bodyHTML); + const cssSource = getStyleFromSource(headHTML); + const externalScripts = getExternalScriptsFromSource(renderedHtml); + const codePenData = { html: htmlSource, css: cssSource, js: jsSource, externalJS: externalScripts, }; - var height = placementElement.getAttribute('data-height'); + const height = placementElement.getAttribute('data-height'); - var codeSnippet = document.createElement('div'); + let codeSnippet = document.createElement('div'); codeSnippet.classList.add('p-code-snippet', 'is-bordered'); - var header = document.createElement('div'); + let header = document.createElement('div'); header.classList.add('p-code-snippet__header'); - var titleEl = document.createElement('h5'); + let titleEl = document.createElement('h5'); titleEl.classList.add('p-code-snippet__title'); // example page title is structured as "... | Examples | Vanilla documentation" @@ -257,7 +257,7 @@ renderIframe(codeSnippet, renderedHtml, height); // Build code block structure - var options = ['html']; + let options = ['html']; codeSnippet.appendChild(createPreCode(bodyHTML, 'html', false)); if (hasJinjaTemplate) { codeSnippet.appendChild(createPreCode(templateHTML, 'jinja')); @@ -291,14 +291,14 @@ function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block if (codeSnippetModes.length > 1) { - var dropdownsEl = document.createElement('div'); + let dropdownsEl = document.createElement('div'); dropdownsEl.classList.add('p-code-snippet__dropdowns'); - var selectEl = document.createElement('select'); + let selectEl = document.createElement('select'); selectEl.classList.add('p-code-snippet__dropdown'); codeSnippetModes.forEach(function (option) { - var optionHTML = document.createElement('option'); + let optionHTML = document.createElement('option'); optionHTML.value = option.toLowerCase(); optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); selectEl.appendChild(optionHTML); @@ -312,20 +312,19 @@ function resizeIframe(iframe) { if (iframe.contentDocument.readyState == 'complete') { - var frameHeight = iframe.contentDocument.body.scrollHeight; - var style = iframe.contentWindow.getComputedStyle(iframe.contentDocument.body); + let frameHeight = iframe.contentDocument.body.scrollHeight; iframe.height = frameHeight + 32 + 'px'; // accommodate for body margin } } function renderIframe(container, html, height) { - var iframe = document.createElement('iframe'); + let iframe = document.createElement('iframe'); if (height) { iframe.height = height + 'px'; } container.appendChild(iframe); - var doc = iframe.contentWindow.document; + let doc = iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); @@ -333,7 +332,7 @@ // if height wasn't specified, try to determine it from example content if (!height) { // Wait for content to load before determining height - var resizeInterval = setInterval(function () { + let resizeInterval = setInterval(function () { if (iframe.contentDocument.readyState == 'complete') { resizeIframe(iframe); clearInterval(resizeInterval); @@ -358,16 +357,16 @@ } function renderCodePenEditLink(snippet, sourceData) { - var html = sourceData.html === null ? '' : sourceData.html; - var css = sourceData.css === null ? '' : sourceData.css; - var js = sourceData.js === null ? '' : sourceData.js; + let html = sourceData.html === null ? '' : sourceData.html; + let css = sourceData.css === null ? '' : sourceData.css; + let js = sourceData.js === null ? '' : sourceData.js; if (html || css || js) { - var container = document.createElement('div'); - var form = document.createElement('form'); - var input = document.createElement('input'); - var link = document.createElement('a'); - var data = { + let container = document.createElement('div'); + let form = document.createElement('form'); + let input = document.createElement('input'); + let link = document.createElement('a'); + const data = { title: CODEPEN_CONFIG.title, head: CODEPEN_CONFIG.head, html: html, @@ -377,7 +376,7 @@ js_external: sourceData.externalJS.join(';'), }; // Replace double quotes to avoid errors on CodePen - var JSONstring = JSON.stringify(data).replace(/"/g, '"').replace(/'/g, '''); + const JSONstring = JSON.stringify(data).replace(/"/g, '"').replace(/'/g, '''); container.classList.add('p-code-snippet__header'); @@ -415,17 +414,17 @@ } function getStyleFromSource(source) { - var div = document.createElement('div'); + let div = document.createElement('div'); div.innerHTML = source; - var style = div.querySelector('style'); + let style = div.querySelector('style'); return style ? style.innerHTML.trim() : null; } function stripScriptsFromSource(source) { - var div = document.createElement('div'); + let div = document.createElement('div'); div.innerHTML = source; - var scripts = div.getElementsByTagName('script'); - var i = scripts.length; + let scripts = div.getElementsByTagName('script'); + let i = scripts.length; while (i--) { scripts[i].parentNode.removeChild(scripts[i]); } @@ -433,16 +432,16 @@ } function getScriptFromSource(source) { - var div = document.createElement('div'); + let div = document.createElement('div'); div.innerHTML = source; - var script = div.querySelector('script'); + let script = div.querySelector('script'); return script ? script.innerHTML.trim() : null; } function getExternalScriptsFromSource(source) { - var div = document.createElement('div'); + let div = document.createElement('div'); div.innerHTML = source; - var scripts = div.querySelectorAll('script[src]'); + let scripts = div.querySelectorAll('script[src]'); scripts = [].slice.apply(scripts).map(function (s) { return s.src; }); @@ -463,12 +462,12 @@ */ function attachDropdownEvents(dropdown) { dropdown.addEventListener('change', function (e) { - var snippet = e.target.closest('.p-code-snippet'); + let snippet = e.target.closest('.p-code-snippet'); // toggle code blocks visibility based on selected language - for (var i = 0; i < dropdown.options.length; i++) { - var lang = dropdown.options[i].value; - var block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); + for (let i = 0; i < dropdown.options.length; i++) { + let lang = dropdown.options[i].value; + let block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); if (lang === e.target.value) { block.classList.remove('u-hide'); From b6d49447d7c4c78211b405718cf8e9236f5752c2 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:24:00 -0400 Subject: [PATCH 15/34] Move throttle() to a utils function --- templates/_layouts/_root.html | 2 +- templates/_layouts/docs.html | 2 +- templates/static/js/example.js | 16 ++-------------- templates/static/js/scripts.js | 16 ++-------------- templates/static/js/shared/utils.js | 13 +++++++++++++ 5 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 templates/static/js/shared/utils.js diff --git a/templates/_layouts/_root.html b/templates/_layouts/_root.html index f8d74ea97..42b61fa73 100644 --- a/templates/_layouts/_root.html +++ b/templates/_layouts/_root.html @@ -66,7 +66,7 @@ - + {% block scripts %} {% endblock %} diff --git a/templates/_layouts/docs.html b/templates/_layouts/docs.html index 34a0c4928..551fe1105 100644 --- a/templates/_layouts/docs.html +++ b/templates/_layouts/docs.html @@ -130,5 +130,5 @@

On this page:

- + {% endblock %} diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 2445d1856..62e028570 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -1,3 +1,5 @@ +import {throttle} from './shared/utils.js'; + (function () { if (!window.VANILLA_VERSION) { throw Error('VANILLA_VERSION not specified.'); @@ -45,20 +47,6 @@ }, }; - // throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - const throttle = function (fn, delay) { - let timer = null; - return function () { - let context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }; - const CODEPEN_CONFIG = { title: 'Vanilla framework example', head: "", diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 2330abf55..3d47e86cc 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -1,19 +1,7 @@ +import {throttle} from './shared/utils.js'; + // Setup toggling of side navigation drawer (function () { - // throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function (fn, delay) { - var timer = null; - return function () { - var context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }; - var expandedSidenavContainer = null; var lastFocus = null; var ignoreFocusChanges = false; diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js new file mode 100644 index 000000000..dd73a30b0 --- /dev/null +++ b/templates/static/js/shared/utils.js @@ -0,0 +1,13 @@ +// throttling function calls, by Remy Sharp +// http://remysharp.com/2010/07/21/throttling-function-calls/ +export const throttle = function (fn, delay) { + let timer = null; + return function () { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; +}; From 9871d5c9e85af51db2593987393540fef65a44cc Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:29:49 -0400 Subject: [PATCH 16/34] fix HTML snippets including JS at the bottom --- templates/static/js/example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 62e028570..2ec7d0d56 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -246,7 +246,7 @@ import {throttle} from './shared/utils.js'; // Build code block structure let options = ['html']; - codeSnippet.appendChild(createPreCode(bodyHTML, 'html', false)); + codeSnippet.appendChild(createPreCode(htmlSource, 'html', false)); if (hasJinjaTemplate) { codeSnippet.appendChild(createPreCode(templateHTML, 'jinja')); options.push('jinja'); From e6834dbda297c06eb30c57b372f80650e4e7ec29 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:30:42 -0400 Subject: [PATCH 17/34] Revert "Move throttle() to a utils function" This reverts commit 75e51b8dc028885a57e105244e2c3018401b5d4a. --- templates/_layouts/_root.html | 2 +- templates/_layouts/docs.html | 2 +- templates/static/js/example.js | 16 ++++++++++++++-- templates/static/js/scripts.js | 16 ++++++++++++++-- templates/static/js/shared/utils.js | 13 ------------- 5 files changed, 30 insertions(+), 19 deletions(-) delete mode 100644 templates/static/js/shared/utils.js diff --git a/templates/_layouts/_root.html b/templates/_layouts/_root.html index 42b61fa73..f8d74ea97 100644 --- a/templates/_layouts/_root.html +++ b/templates/_layouts/_root.html @@ -66,7 +66,7 @@ - + {% block scripts %} {% endblock %} diff --git a/templates/_layouts/docs.html b/templates/_layouts/docs.html index 551fe1105..34a0c4928 100644 --- a/templates/_layouts/docs.html +++ b/templates/_layouts/docs.html @@ -130,5 +130,5 @@

On this page:

- + {% endblock %} diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 2ec7d0d56..23f8d0c8b 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -1,5 +1,3 @@ -import {throttle} from './shared/utils.js'; - (function () { if (!window.VANILLA_VERSION) { throw Error('VANILLA_VERSION not specified.'); @@ -47,6 +45,20 @@ import {throttle} from './shared/utils.js'; }, }; + // throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + const throttle = function (fn, delay) { + let timer = null; + return function () { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + }; + const CODEPEN_CONFIG = { title: 'Vanilla framework example', head: "", diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 3d47e86cc..2330abf55 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -1,7 +1,19 @@ -import {throttle} from './shared/utils.js'; - // Setup toggling of side navigation drawer (function () { + // throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + var throttle = function (fn, delay) { + var timer = null; + return function () { + var context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + }; + var expandedSidenavContainer = null; var lastFocus = null; var ignoreFocusChanges = false; diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js deleted file mode 100644 index dd73a30b0..000000000 --- a/templates/static/js/shared/utils.js +++ /dev/null @@ -1,13 +0,0 @@ -// throttling function calls, by Remy Sharp -// http://remysharp.com/2010/07/21/throttling-function-calls/ -export const throttle = function (fn, delay) { - let timer = null; - return function () { - let context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; -}; From f750c59133c9e96b661dc9b3522517bfb6bf6480 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:32:24 -0400 Subject: [PATCH 18/34] rename `createPreCode` param `hide` to `isHidden` --- templates/static/js/example.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 23f8d0c8b..b036583d9 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -174,10 +174,10 @@ * Create `pre`-formatted code for a block of source * @param {String} source Unformatted source code * @param {'html'|'jinja'|'js'|'css'} lang Language of the source code - * @param {Boolean} hide Whether the pre-code should be hidden initially + * @param {Boolean} isHidden Whether the pre-code should be hidden initially * @returns {HTMLPreElement} Code snippet containing the source code */ - function createPreCode(source, lang, hide = true) { + function createPreCode(source, lang, isHidden = true) { let code = document.createElement('code'); code.appendChild(document.createTextNode(formatSource(source, lang))); @@ -187,7 +187,7 @@ // TODO: move max-height elsewhere to CSS? pre.style.maxHeight = '300px'; - if (hide) { + if (isHidden) { pre.classList.add('u-hide'); } From 8370ba51206172b17ae677ee23055cc41d23f8e9 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 16:34:30 -0400 Subject: [PATCH 19/34] Reapply "Move throttle() to a utils function" This reverts commit 99b32c90b3d57db72329d894ddb6660b88228404. --- templates/_layouts/_root.html | 2 +- templates/_layouts/docs.html | 2 +- templates/static/js/example.js | 16 ++-------------- templates/static/js/scripts.js | 16 ++-------------- templates/static/js/shared/utils.js | 13 +++++++++++++ 5 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 templates/static/js/shared/utils.js diff --git a/templates/_layouts/_root.html b/templates/_layouts/_root.html index f8d74ea97..42b61fa73 100644 --- a/templates/_layouts/_root.html +++ b/templates/_layouts/_root.html @@ -66,7 +66,7 @@ - + {% block scripts %} {% endblock %} diff --git a/templates/_layouts/docs.html b/templates/_layouts/docs.html index 34a0c4928..551fe1105 100644 --- a/templates/_layouts/docs.html +++ b/templates/_layouts/docs.html @@ -130,5 +130,5 @@

On this page:

- + {% endblock %} diff --git a/templates/static/js/example.js b/templates/static/js/example.js index b036583d9..764a1f29c 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -1,3 +1,5 @@ +import {throttle} from './shared/utils.js'; + (function () { if (!window.VANILLA_VERSION) { throw Error('VANILLA_VERSION not specified.'); @@ -45,20 +47,6 @@ }, }; - // throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - const throttle = function (fn, delay) { - let timer = null; - return function () { - let context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }; - const CODEPEN_CONFIG = { title: 'Vanilla framework example', head: "", diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 2330abf55..3d47e86cc 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -1,19 +1,7 @@ +import {throttle} from './shared/utils.js'; + // Setup toggling of side navigation drawer (function () { - // throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function (fn, delay) { - var timer = null; - return function () { - var context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }; - var expandedSidenavContainer = null; var lastFocus = null; var ignoreFocusChanges = false; diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js new file mode 100644 index 000000000..dd73a30b0 --- /dev/null +++ b/templates/static/js/shared/utils.js @@ -0,0 +1,13 @@ +// throttling function calls, by Remy Sharp +// http://remysharp.com/2010/07/21/throttling-function-calls/ +export const throttle = function (fn, delay) { + let timer = null; + return function () { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; +}; From 4cb2f266baa8468b643cc80d15575a4b0129aa4f Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 17:27:18 -0400 Subject: [PATCH 20/34] swap xmlhttprequest for fetch() --- templates/static/js/example.js | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 764a1f29c..3cdf062fb 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -74,28 +74,11 @@ import {throttle} from './shared/utils.js'; * @returns {Promise} Response text */ async function fetchExampleResponseText(url) { - let request = new XMLHttpRequest(); - - const result = new Promise(function (resolve, reject) { - request.onreadystatechange = function () { - // If the request is not complete, do nothing - if (request.readyState !== 4) { - return; - } - // Request is complete and successful - if (request.status === 200 && request.readyState === 4) { - resolve(request.responseText); - } else { - // Request failed - reject('Failed to fetch example ' + url + ' with status ' + request.status); - } - }; - }); - - request.open('GET', url, true); - request.send(null); - - return result; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + } + return response.text(); } /** From 6eff9cabc32c4271c83f26fcfafe1d354a11615c Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 29 Aug 2024 17:42:07 -0400 Subject: [PATCH 21/34] Moved fetch code from example file into shared utils --- templates/static/js/example.js | 19 +++---------------- templates/static/js/shared/utils.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 3cdf062fb..2809eb461 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -1,4 +1,4 @@ -import {throttle} from './shared/utils.js'; +import {throttle, fetchResponseText} from './shared/utils.js'; (function () { if (!window.VANILLA_VERSION) { @@ -68,19 +68,6 @@ import {throttle} from './shared/utils.js'; [].slice.call(examples).forEach(fetchExample); }); - /** - * Sends a `GET` request to `url` to request an example's contents. - * @param {String} url Address of the example - * @returns {Promise} Response text - */ - async function fetchExampleResponseText(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch example ${url} with status ${response.status}`); - } - return response.text(); - } - /** * Fetches the requested example and replaces the example element with the content and code snippet of the example. * @param {HTMLAnchorElement} exampleElement `a.js-example` element with `href` set to the address of the example to fetch @@ -88,7 +75,7 @@ import {throttle} from './shared/utils.js'; async function fetchExample(exampleElement) { // TODO - integrate fetching/rendering more cleanly in future /** Rendered HTML that will be seen by users */ - const fetchRendered = fetchExampleResponseText(exampleElement.href); + const fetchRendered = fetchResponseText(exampleElement.href); let exampleRequests = [fetchRendered]; @@ -99,7 +86,7 @@ import {throttle} from './shared/utils.js'; queryParams.set('raw', true); exampleURL.search = queryParams.toString(); - const fetchRaw = fetchExampleResponseText( + const fetchRaw = fetchResponseText( exampleURL.href // Raw templates are not served at standalone paths, so strip it from the URL if it was found. .replace(/standalone/, '/'), diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js index dd73a30b0..90f00f712 100644 --- a/templates/static/js/shared/utils.js +++ b/templates/static/js/shared/utils.js @@ -11,3 +11,28 @@ export const throttle = function (fn, delay) { }, delay); }; }; + +/** + * `fetch()` wrapper that throws an error if the response is not OK. + * @param {String} url Address to fetch + * @param {RequestInit} opts Options for the fetch request + * @returns {Promise} Response object + * @throws {Error} If the response is not in the 200 (OK) range + */ +export const fetchResponse = async function (url, opts = {}) { + const response = await fetch(url, opts); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + } + return response; +}; + +/** + * Fetch the response text of a URL. + * @param {String} url Address to fetch + * @returns {Promise} Response text + * @throws {Error} If the response is not in the 200 (OK) range + */ +export const fetchResponseText = async function (url) { + return (await fetchResponse(url)).text(); +}; From cc1ec444f2379d57e9976bcd1d62f4c1ef8aabc8 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Tue, 3 Sep 2024 14:26:17 -0400 Subject: [PATCH 22/34] Move utils to window scope, rather than JS import --- templates/_layouts/_root.html | 3 +- templates/_layouts/docs.html | 3 +- templates/static/js/example.js | 6 +-- templates/static/js/scripts.js | 4 +- templates/static/js/shared/utils.js | 70 ++++++++++++++++------------- 5 files changed, 45 insertions(+), 41 deletions(-) diff --git a/templates/_layouts/_root.html b/templates/_layouts/_root.html index 42b61fa73..5520c4c06 100644 --- a/templates/_layouts/_root.html +++ b/templates/_layouts/_root.html @@ -66,7 +66,8 @@ - + + {% block scripts %} {% endblock %} diff --git a/templates/_layouts/docs.html b/templates/_layouts/docs.html index 551fe1105..c357da823 100644 --- a/templates/_layouts/docs.html +++ b/templates/_layouts/docs.html @@ -130,5 +130,6 @@

On this page:

- + + {% endblock %} diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 2809eb461..067d7d122 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -1,5 +1,3 @@ -import {throttle, fetchResponseText} from './shared/utils.js'; - (function () { if (!window.VANILLA_VERSION) { throw Error('VANILLA_VERSION not specified.'); @@ -75,7 +73,7 @@ import {throttle, fetchResponseText} from './shared/utils.js'; async function fetchExample(exampleElement) { // TODO - integrate fetching/rendering more cleanly in future /** Rendered HTML that will be seen by users */ - const fetchRendered = fetchResponseText(exampleElement.href); + const fetchRendered = window.fetchResponseText(exampleElement.href); let exampleRequests = [fetchRendered]; @@ -86,7 +84,7 @@ import {throttle, fetchResponseText} from './shared/utils.js'; queryParams.set('raw', true); exampleURL.search = queryParams.toString(); - const fetchRaw = fetchResponseText( + const fetchRaw = window.fetchResponseText( exampleURL.href // Raw templates are not served at standalone paths, so strip it from the URL if it was found. .replace(/standalone/, '/'), diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 3d47e86cc..9443a5e43 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -1,5 +1,3 @@ -import {throttle} from './shared/utils.js'; - // Setup toggling of side navigation drawer (function () { var expandedSidenavContainer = null; @@ -135,7 +133,7 @@ import {throttle} from './shared/utils.js'; // hide side navigation drawer when screen is resized window.addEventListener( 'resize', - throttle(function () { + window.throttle(function () { toggles.forEach((toggle) => { return toggle.setAttribute('aria-expanded', false); }); diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js index 90f00f712..c765436ef 100644 --- a/templates/static/js/shared/utils.js +++ b/templates/static/js/shared/utils.js @@ -1,38 +1,44 @@ +(function() { // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ -export const throttle = function (fn, delay) { - let timer = null; - return function () { - let context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); + const throttle = function(fn, delay) { + let timer = null; + return function() { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function() { + fn.apply(context, args); + }, delay); + }; }; -}; -/** - * `fetch()` wrapper that throws an error if the response is not OK. - * @param {String} url Address to fetch - * @param {RequestInit} opts Options for the fetch request - * @returns {Promise} Response object - * @throws {Error} If the response is not in the 200 (OK) range - */ -export const fetchResponse = async function (url, opts = {}) { - const response = await fetch(url, opts); - if (!response.ok) { - throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + /** + * `fetch()` wrapper that throws an error if the response is not OK. + * @param {String} url Address to fetch + * @param {RequestInit} opts Options for the fetch request + * @returns {Promise} Response object + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponse = async function(url, opts = {}) { + const response = await fetch(url, opts); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + } + return response; + }; + + /** + * Fetch the response text of a URL. + * @param {String} url Address to fetch + * @returns {Promise} Response text + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponseText = async function(url) { + return (await fetchResponse(url)).text(); } - return response; -}; -/** - * Fetch the response text of a URL. - * @param {String} url Address to fetch - * @returns {Promise} Response text - * @throws {Error} If the response is not in the 200 (OK) range - */ -export const fetchResponseText = async function (url) { - return (await fetchResponse(url)).text(); -}; + window.throttle = throttle; + window.fetchResponse = fetchResponse; + window.fetchResponseText = fetchResponseText; +})(); From bdd5fb890587cddc69de722d1fd42b6b8704c0c4 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Tue, 3 Sep 2024 14:31:11 -0400 Subject: [PATCH 23/34] prettier --- templates/static/js/shared/utils.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js index c765436ef..24f8c639e 100644 --- a/templates/static/js/shared/utils.js +++ b/templates/static/js/shared/utils.js @@ -1,13 +1,13 @@ -(function() { -// throttling function calls, by Remy Sharp -// http://remysharp.com/2010/07/21/throttling-function-calls/ - const throttle = function(fn, delay) { +(function () { + // throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + const throttle = function (fn, delay) { let timer = null; - return function() { + return function () { let context = this, args = arguments; clearTimeout(timer); - timer = setTimeout(function() { + timer = setTimeout(function () { fn.apply(context, args); }, delay); }; @@ -20,7 +20,7 @@ * @returns {Promise} Response object * @throws {Error} If the response is not in the 200 (OK) range */ - const fetchResponse = async function(url, opts = {}) { + const fetchResponse = async function (url, opts = {}) { const response = await fetch(url, opts); if (!response.ok) { throw new Error(`Failed to fetch example ${url} with status ${response.status}`); @@ -34,9 +34,9 @@ * @returns {Promise} Response text * @throws {Error} If the response is not in the 200 (OK) range */ - const fetchResponseText = async function(url) { + const fetchResponseText = async function (url) { return (await fetchResponse(url)).text(); - } + }; window.throttle = throttle; window.fetchResponse = fetchResponse; From 982106270e98a90fb7e940ad5b660c73790bbc47 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Wed, 4 Sep 2024 11:59:37 -0400 Subject: [PATCH 24/34] set no-cache on raw template endpoint --- webapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/app.py b/webapp/app.py index 914234d59..504d8ac1c 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -297,7 +297,7 @@ def example(example_path, is_standalone=False): # separate directory from file name so that flask.send_from_directory() can prevent malicious file access raw_example_directory = os.path.dirname(raw_example_path) raw_example_file_name = os.path.basename(raw_example_path) - return flask.send_from_directory(raw_example_directory, raw_example_file_name, mimetype="text/raw", max_age=86400) + return flask.send_from_directory(raw_example_directory, raw_example_file_name, mimetype="text/raw") return flask.render_template( f"docs/examples/{example_path}.html", is_standalone=is_standalone From ee11a5940a18084fd9ca7af7cbc1df3b4f20044b Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Wed, 4 Sep 2024 16:17:36 -0400 Subject: [PATCH 25/34] Re-remove extracted utils functions --- templates/_layouts/_root.html | 1 - templates/_layouts/docs.html | 1 - templates/static/js/example.js | 43 ++++++++++++++++++++++++++-- templates/static/js/scripts.js | 16 ++++++++++- templates/static/js/shared/utils.js | 44 ----------------------------- 5 files changed, 56 insertions(+), 49 deletions(-) delete mode 100644 templates/static/js/shared/utils.js diff --git a/templates/_layouts/_root.html b/templates/_layouts/_root.html index 5520c4c06..f8d74ea97 100644 --- a/templates/_layouts/_root.html +++ b/templates/_layouts/_root.html @@ -66,7 +66,6 @@ - {% block scripts %} diff --git a/templates/_layouts/docs.html b/templates/_layouts/docs.html index c357da823..34a0c4928 100644 --- a/templates/_layouts/docs.html +++ b/templates/_layouts/docs.html @@ -130,6 +130,5 @@

On this page:

- {% endblock %} diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 067d7d122..dcb354268 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -3,6 +3,20 @@ throw Error('VANILLA_VERSION not specified.'); } + // throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + const throttle = function (fn, delay) { + let timer = null; + return function () { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + }; + /** * Mapping of example keys to the regex patterns used to strip them out of an example * @type {{body: RegExp, jinja: RegExp, title: RegExp, head: RegExp}} @@ -66,6 +80,31 @@ [].slice.call(examples).forEach(fetchExample); }); + /** + * `fetch()` wrapper that throws an error if the response is not OK. + * @param {String} url Address to fetch + * @param {RequestInit} opts Options for the fetch request + * @returns {Promise} Response object + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponse = async function (url, opts = {}) { + const response = await fetch(url, opts); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + } + return response; + }; + + /** + * Fetch the response text of a URL. + * @param {String} url Address to fetch + * @returns {Promise} Response text + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponseText = async function (url) { + return (await fetchResponse(url)).text(); + }; + /** * Fetches the requested example and replaces the example element with the content and code snippet of the example. * @param {HTMLAnchorElement} exampleElement `a.js-example` element with `href` set to the address of the example to fetch @@ -73,7 +112,7 @@ async function fetchExample(exampleElement) { // TODO - integrate fetching/rendering more cleanly in future /** Rendered HTML that will be seen by users */ - const fetchRendered = window.fetchResponseText(exampleElement.href); + const fetchRendered = fetchResponseText(exampleElement.href); let exampleRequests = [fetchRendered]; @@ -84,7 +123,7 @@ queryParams.set('raw', true); exampleURL.search = queryParams.toString(); - const fetchRaw = window.fetchResponseText( + const fetchRaw = fetchResponseText( exampleURL.href // Raw templates are not served at standalone paths, so strip it from the URL if it was found. .replace(/standalone/, '/'), diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 9443a5e43..8ab701ca6 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -1,5 +1,19 @@ // Setup toggling of side navigation drawer (function () { + // throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + const throttle = function (fn, delay) { + let timer = null; + return function () { + let context = this, + args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + }; + var expandedSidenavContainer = null; var lastFocus = null; var ignoreFocusChanges = false; @@ -133,7 +147,7 @@ // hide side navigation drawer when screen is resized window.addEventListener( 'resize', - window.throttle(function () { + throttle(function () { toggles.forEach((toggle) => { return toggle.setAttribute('aria-expanded', false); }); diff --git a/templates/static/js/shared/utils.js b/templates/static/js/shared/utils.js deleted file mode 100644 index 24f8c639e..000000000 --- a/templates/static/js/shared/utils.js +++ /dev/null @@ -1,44 +0,0 @@ -(function () { - // throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - const throttle = function (fn, delay) { - let timer = null; - return function () { - let context = this, - args = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }; - - /** - * `fetch()` wrapper that throws an error if the response is not OK. - * @param {String} url Address to fetch - * @param {RequestInit} opts Options for the fetch request - * @returns {Promise} Response object - * @throws {Error} If the response is not in the 200 (OK) range - */ - const fetchResponse = async function (url, opts = {}) { - const response = await fetch(url, opts); - if (!response.ok) { - throw new Error(`Failed to fetch example ${url} with status ${response.status}`); - } - return response; - }; - - /** - * Fetch the response text of a URL. - * @param {String} url Address to fetch - * @returns {Promise} Response text - * @throws {Error} If the response is not in the 200 (OK) range - */ - const fetchResponseText = async function (url) { - return (await fetchResponse(url)).text(); - }; - - window.throttle = throttle; - window.fetchResponse = fetchResponse; - window.fetchResponseText = fetchResponseText; -})(); From 3fe03be1fbb8f50a0d56cbb3a4ee500c59282e4e Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 5 Sep 2024 15:43:08 -0400 Subject: [PATCH 26/34] replace `let` with `const` where applicable --- templates/static/js/example.js | 68 +++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index dcb354268..46b410225 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -8,7 +8,7 @@ const throttle = function (fn, delay) { let timer = null; return function () { - let context = this, + const context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { @@ -114,12 +114,12 @@ /** Rendered HTML that will be seen by users */ const fetchRendered = fetchResponseText(exampleElement.href); - let exampleRequests = [fetchRendered]; + const exampleRequests = [fetchRendered]; // If the example requires raw template rendering, request the raw template file as well if (exampleElement.getAttribute('data-lang') === 'jinja') { - let exampleURL = new URL(exampleElement.href); - let queryParams = new URLSearchParams(exampleURL.search); + const exampleURL = new URL(exampleElement.href); + const queryParams = new URLSearchParams(exampleURL.search); queryParams.set('raw', true); exampleURL.search = queryParams.toString(); @@ -173,10 +173,10 @@ * @returns {HTMLPreElement} Code snippet containing the source code */ function createPreCode(source, lang, isHidden = true) { - let code = document.createElement('code'); + const code = document.createElement('code'); code.appendChild(document.createTextNode(formatSource(source, lang))); - let pre = document.createElement('pre'); + const pre = document.createElement('pre'); pre.classList.add('p-code-snippet__block'); // TODO: move max-height elsewhere to CSS? @@ -232,13 +232,13 @@ const height = placementElement.getAttribute('data-height'); - let codeSnippet = document.createElement('div'); + const codeSnippet = document.createElement('div'); codeSnippet.classList.add('p-code-snippet', 'is-bordered'); - let header = document.createElement('div'); + const header = document.createElement('div'); header.classList.add('p-code-snippet__header'); - let titleEl = document.createElement('h5'); + const titleEl = document.createElement('h5'); titleEl.classList.add('p-code-snippet__title'); // example page title is structured as "... | Examples | Vanilla documentation" @@ -252,7 +252,7 @@ renderIframe(codeSnippet, renderedHtml, height); // Build code block structure - let options = ['html']; + const options = ['html']; codeSnippet.appendChild(createPreCode(htmlSource, 'html', false)); if (hasJinjaTemplate) { codeSnippet.appendChild(createPreCode(templateHTML, 'jinja')); @@ -286,14 +286,14 @@ function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block if (codeSnippetModes.length > 1) { - let dropdownsEl = document.createElement('div'); + const dropdownsEl = document.createElement('div'); dropdownsEl.classList.add('p-code-snippet__dropdowns'); - let selectEl = document.createElement('select'); + const selectEl = document.createElement('select'); selectEl.classList.add('p-code-snippet__dropdown'); codeSnippetModes.forEach(function (option) { - let optionHTML = document.createElement('option'); + const optionHTML = document.createElement('option'); optionHTML.value = option.toLowerCase(); optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); selectEl.appendChild(optionHTML); @@ -307,19 +307,19 @@ function resizeIframe(iframe) { if (iframe.contentDocument.readyState == 'complete') { - let frameHeight = iframe.contentDocument.body.scrollHeight; + const frameHeight = iframe.contentDocument.body.scrollHeight; iframe.height = frameHeight + 32 + 'px'; // accommodate for body margin } } function renderIframe(container, html, height) { - let iframe = document.createElement('iframe'); + const iframe = document.createElement('iframe'); if (height) { iframe.height = height + 'px'; } container.appendChild(iframe); - let doc = iframe.contentWindow.document; + const doc = iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); @@ -327,7 +327,7 @@ // if height wasn't specified, try to determine it from example content if (!height) { // Wait for content to load before determining height - let resizeInterval = setInterval(function () { + const resizeInterval = setInterval(function () { if (iframe.contentDocument.readyState == 'complete') { resizeIframe(iframe); clearInterval(resizeInterval); @@ -352,15 +352,15 @@ } function renderCodePenEditLink(snippet, sourceData) { - let html = sourceData.html === null ? '' : sourceData.html; - let css = sourceData.css === null ? '' : sourceData.css; - let js = sourceData.js === null ? '' : sourceData.js; + const html = sourceData.html === null ? '' : sourceData.html; + const css = sourceData.css === null ? '' : sourceData.css; + const js = sourceData.js === null ? '' : sourceData.js; if (html || css || js) { - let container = document.createElement('div'); - let form = document.createElement('form'); - let input = document.createElement('input'); - let link = document.createElement('a'); + const container = document.createElement('div'); + const form = document.createElement('form'); + const input = document.createElement('input'); + const link = document.createElement('a'); const data = { title: CODEPEN_CONFIG.title, head: CODEPEN_CONFIG.head, @@ -409,16 +409,16 @@ } function getStyleFromSource(source) { - let div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - let style = div.querySelector('style'); + const style = div.querySelector('style'); return style ? style.innerHTML.trim() : null; } function stripScriptsFromSource(source) { - let div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - let scripts = div.getElementsByTagName('script'); + const scripts = div.getElementsByTagName('script'); let i = scripts.length; while (i--) { scripts[i].parentNode.removeChild(scripts[i]); @@ -427,14 +427,14 @@ } function getScriptFromSource(source) { - let div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - let script = div.querySelector('script'); + const script = div.querySelector('script'); return script ? script.innerHTML.trim() : null; } function getExternalScriptsFromSource(source) { - let div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; let scripts = div.querySelectorAll('script[src]'); scripts = [].slice.apply(scripts).map(function (s) { @@ -457,12 +457,12 @@ */ function attachDropdownEvents(dropdown) { dropdown.addEventListener('change', function (e) { - let snippet = e.target.closest('.p-code-snippet'); + const snippet = e.target.closest('.p-code-snippet'); // toggle code blocks visibility based on selected language for (let i = 0; i < dropdown.options.length; i++) { - let lang = dropdown.options[i].value; - let block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); + const lang = dropdown.options[i].value; + const block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); if (lang === e.target.value) { block.classList.remove('u-hide'); From 8d78795cd4d82a6c044736a452431eb4c41aa77e Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 5 Sep 2024 15:53:33 -0400 Subject: [PATCH 27/34] Additional try/catch handler in `fetchResponse()` --- templates/static/js/example.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 46b410225..124dc05e2 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -88,11 +88,16 @@ * @throws {Error} If the response is not in the 200 (OK) range */ const fetchResponse = async function (url, opts = {}) { - const response = await fetch(url, opts); - if (!response.ok) { - throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + try { + const response = await fetch(url, opts); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); + } + return response; + } catch (err) { + console.error('An error occurred while performing a fetch request', err); + throw err; } - return response; }; /** @@ -102,7 +107,7 @@ * @throws {Error} If the response is not in the 200 (OK) range */ const fetchResponseText = async function (url) { - return (await fetchResponse(url)).text(); + return fetchResponse(url).then((response) => response.text()); }; /** @@ -135,7 +140,7 @@ const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); renderExample(exampleElement, renderedHtml, rawHtml); } catch (err) { - console.error(err); + console.error('An error occurred while fetching an example', exampleElement, err); } } From e36ca6b37babda3822661a41d44df5f8e5e719e3 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 5 Sep 2024 16:16:56 -0400 Subject: [PATCH 28/34] renderdropdown guard --- templates/static/js/example.js | 37 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 124dc05e2..25723cf4a 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -290,24 +290,25 @@ */ function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block - if (codeSnippetModes.length > 1) { - const dropdownsEl = document.createElement('div'); - dropdownsEl.classList.add('p-code-snippet__dropdowns'); - - const selectEl = document.createElement('select'); - selectEl.classList.add('p-code-snippet__dropdown'); - - codeSnippetModes.forEach(function (option) { - const optionHTML = document.createElement('option'); - optionHTML.value = option.toLowerCase(); - optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); - selectEl.appendChild(optionHTML); - }); - - dropdownsEl.appendChild(selectEl); - codeSnippetHeader.appendChild(dropdownsEl); - attachDropdownEvents(selectEl); - } + if (codeSnippetModes.length === 0) return; + + const dropdownsEl = document.createElement('div'); + dropdownsEl.classList.add('p-code-snippet__dropdowns'); + + const selectEl = document.createElement('select'); + selectEl.classList.add('p-code-snippet__dropdown'); + + codeSnippetModes.forEach(function (option) { + const optionHTML = document.createElement('option'); + optionHTML.value = option.toLowerCase(); + optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); + selectEl.appendChild(optionHTML); + }); + + dropdownsEl.appendChild(selectEl); + codeSnippetHeader.appendChild(dropdownsEl); + attachDropdownEvents(selectEl); + } function resizeIframe(iframe) { From 0ace747e8e089538e2f0009e45670109ebc42f9f Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Thu, 5 Sep 2024 17:44:58 -0400 Subject: [PATCH 29/34] promise.all parallelization improvement --- templates/static/js/example.js | 198 +++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 83 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 25723cf4a..89930dd77 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -77,7 +77,11 @@ document.addEventListener('DOMContentLoaded', function () { const examples = document.querySelectorAll('.js-example'); - [].slice.call(examples).forEach(fetchExample); + [].slice.call(examples).forEach((placementElement) => { + renderExample(placementElement).catch((error) => { + console.error('Failed to render example', {placementElement, error}); + }); + }); }); /** @@ -110,40 +114,6 @@ return fetchResponse(url).then((response) => response.text()); }; - /** - * Fetches the requested example and replaces the example element with the content and code snippet of the example. - * @param {HTMLAnchorElement} exampleElement `a.js-example` element with `href` set to the address of the example to fetch - */ - async function fetchExample(exampleElement) { - // TODO - integrate fetching/rendering more cleanly in future - /** Rendered HTML that will be seen by users */ - const fetchRendered = fetchResponseText(exampleElement.href); - - const exampleRequests = [fetchRendered]; - - // If the example requires raw template rendering, request the raw template file as well - if (exampleElement.getAttribute('data-lang') === 'jinja') { - const exampleURL = new URL(exampleElement.href); - const queryParams = new URLSearchParams(exampleURL.search); - queryParams.set('raw', true); - exampleURL.search = queryParams.toString(); - - const fetchRaw = fetchResponseText( - exampleURL.href - // Raw templates are not served at standalone paths, so strip it from the URL if it was found. - .replace(/standalone/, '/'), - ); - exampleRequests.push(fetchRaw); - } - - try { - const [renderedHtml, rawHtml] = await Promise.all(exampleRequests); - renderExample(exampleElement, renderedHtml, rawHtml); - } catch (err) { - console.error('An error occurred while fetching an example', exampleElement, err); - } - } - /** * Format source code based on language * @param {String} source - source code to format @@ -172,14 +142,14 @@ /** * Create `pre`-formatted code for a block of source - * @param {String} source Unformatted source code + * @param {String} source Formatted source code * @param {'html'|'jinja'|'js'|'css'} lang Language of the source code * @param {Boolean} isHidden Whether the pre-code should be hidden initially * @returns {HTMLPreElement} Code snippet containing the source code */ function createPreCode(source, lang, isHidden = true) { const code = document.createElement('code'); - code.appendChild(document.createTextNode(formatSource(source, lang))); + code.appendChild(document.createTextNode(source)); const pre = document.createElement('pre'); pre.classList.add('p-code-snippet__block'); @@ -211,74 +181,137 @@ return pattern?.exec(documentHTML)?.[1]?.trim() || ''; } + /** + * Fetches the rendered HTML of an example and extracts the relevant sections for rendering and code snippets. + * @param {HTMLElement} placementElement The placeholder element for the example + * @returns {Promise<{rendered: String, body: String}>} The rendered HTML and source code of the example + */ + async function fetchHtmlSource(placementElement) { + const renderedHtml = await fetchResponseText(placementElement.href); + const bodyHTML = getExampleSection('body', renderedHtml); + + return {rendered: renderedHtml, body: bodyHTML}; + } + + /** + * Fetches the raw Jinja template of an example and returns the Jinja content block + * @param {HTMLElement} placementElement The placeholder element for the example + * @returns {Promise} The Jinja content block of the example + */ + async function fetchJinjaContentBlock(placementElement) { + // Raw templates are not served at standalone paths, so strip it from the URL if it was found. + const exampleUrl = new URL(`${placementElement.href.replace(/standalone/, '/')}`); + + // Add `?raw=true` query parameter to the URL to request the raw Jinja template + const queryParams = new URLSearchParams(exampleUrl.search); + queryParams.set('raw', true); + exampleUrl.search = queryParams.toString(); + + const rawJinjaTemplate = await fetchResponseText(exampleUrl.toString()); + return formatSource(getExampleSection('jinja', rawJinjaTemplate), 'jinja'); + } + /** * Replaces an example placeholder element with its rendered result and code snippet. * @param {HTMLAnchorElement} placementElement `a.js-example` element used as a placeholder for the example to render * @param {String} renderedHtml Full document HTML of the example as it is shown to end-users * @param {String|null} jinjaTemplate Jinja Template of the example as it may be used by developers, if supported */ - function renderExample(placementElement, renderedHtml, jinjaTemplate) { - const bodyHTML = getExampleSection('body', renderedHtml); - const headHTML = getExampleSection('head', renderedHtml); - const title = getExampleSection('title', renderedHtml); - const templateHTML = getExampleSection('jinja', jinjaTemplate); - const hasJinjaTemplate = templateHTML?.length > 0; - - const htmlSource = stripScriptsFromSource(bodyHTML); - const jsSource = getScriptFromSource(bodyHTML); - const cssSource = getStyleFromSource(headHTML); - const externalScripts = getExternalScriptsFromSource(renderedHtml); - const codePenData = { - html: htmlSource, - css: cssSource, - js: jsSource, - externalJS: externalScripts, - }; - - const height = placementElement.getAttribute('data-height'); - + async function renderExample(placementElement) { const codeSnippet = document.createElement('div'); - codeSnippet.classList.add('p-code-snippet', 'is-bordered'); const header = document.createElement('div'); header.classList.add('p-code-snippet__header'); + const titleEl = document.createElement('h5'); titleEl.classList.add('p-code-snippet__title'); - // example page title is structured as "... | Examples | Vanilla documentation" - // we want to strip anything after first | pipe - titleEl.innerText = title.split('|')[0]; + const srcData = { + html: undefined, + jinja: undefined, + css: undefined, + js: undefined, + codePen: undefined, + }; - header.appendChild(titleEl); - codeSnippet.appendChild(header); + const exampleRequests = []; + + const fetchHtml = fetchHtmlSource(placementElement).then(({rendered: renderedHtml, body: bodyHtml}) => { + const title = getExampleSection('title', renderedHtml).split('|')[0]; + const headHtml = getExampleSection('head', renderedHtml); + const htmlBodySource = formatSource(stripScriptsFromSource(bodyHtml), 'html'); + const jsSource = formatSource(getScriptFromSource(bodyHtml), 'js'); + const cssSource = formatSource(getStyleFromSource(headHtml), 'css'); + const externalScripts = getExternalScriptsFromSource(renderedHtml); + + titleEl.innerText = title; + header.appendChild(titleEl); + codeSnippet.appendChild(header); + placementElement.parentNode.insertBefore(codeSnippet, placementElement); + + // HTML iframe is required, so throw if it fails + if (renderedHtml) { + renderIframe(codeSnippet, renderedHtml, placementElement.getAttribute('data-height')); + } else { + throw new Error('Failed to render HTML iframe'); + } - placementElement.parentNode.insertBefore(codeSnippet, placementElement); - renderIframe(codeSnippet, renderedHtml, height); + // HTML source is required, so throw if it fails + if (htmlBodySource) { + srcData.html = htmlBodySource; + } else { + throw new Error('Failed to render HTML source code'); + } - // Build code block structure - const options = ['html']; - codeSnippet.appendChild(createPreCode(htmlSource, 'html', false)); - if (hasJinjaTemplate) { - codeSnippet.appendChild(createPreCode(templateHTML, 'jinja')); - options.push('jinja'); - } - if (jsSource) { - codeSnippet.appendChild(createPreCode(jsSource, 'js')); - options.push('js'); - } - if (cssSource) { - codeSnippet.appendChild(createPreCode(cssSource, 'css')); - options.push('css'); + // The rest of the views are optional + if (jsSource) { + srcData.js = jsSource; + } + if (cssSource) { + srcData.css = cssSource; + } + srcData.codePen = { + html: htmlBodySource, + css: cssSource, + js: jsSource, + externalJS: externalScripts, + }; + }); + exampleRequests.push(fetchHtml); + + if (placementElement.getAttribute('data-lang') === 'jinja') { + // Perform jinja template fetching if the example was marked as a Jinja template + const fetchJinja = fetchJinjaContentBlock(placementElement).then((contentBlock) => { + const hasJinjaTemplate = contentBlock?.length > 0; + if (hasJinjaTemplate) { + srcData.jinja = contentBlock; + } + }); + exampleRequests.push(fetchJinja); } - renderDropdown(header, options); - renderCodePenEditLink(codeSnippet, codePenData); + // Perform as much of the data fetching and processing as possible in parallel + await Promise.all(exampleRequests); + // Code after this point depends on the data above being fully fetched, so must come after an `await` block + + // Gather the languages that have source code available, in the order they should be displayed + // We can't rely on order of these languages being made available in the promise blocks above due to async nature + const languageOptions = ['html', 'jinja', 'js', 'css'].filter((lang) => srcData[lang]); + const sourceBlocks = languageOptions.map((lang, idx) => createPreCode(srcData[lang], lang, idx > 0)); + + sourceBlocks.forEach((block) => codeSnippet.appendChild(block)); if (Prism) { Prism.highlightAllUnder(codeSnippet); } + if (srcData.codePen) { + renderCodePenEditLink(codeSnippet, srcData.codePen); + } + + renderDropdown(header, languageOptions); + // The example has been rendered successfully, hide the placeholder element. placementElement.style.display = 'none'; } @@ -290,7 +323,7 @@ */ function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block - if (codeSnippetModes.length === 0) return; + if (codeSnippetModes.length <= 1) return; const dropdownsEl = document.createElement('div'); dropdownsEl.classList.add('p-code-snippet__dropdowns'); @@ -308,7 +341,6 @@ dropdownsEl.appendChild(selectEl); codeSnippetHeader.appendChild(dropdownsEl); attachDropdownEvents(selectEl); - } function resizeIframe(iframe) { From b35e1152bc23de672fe5e0684896ae06c929d33a Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Fri, 6 Sep 2024 12:24:24 -0400 Subject: [PATCH 30/34] rename `EXAMPLE_OPTION_CFG` to `EXAMPLE_LANGUAGE_OPTION_CONFIG` --- templates/static/js/example.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 89930dd77..6fbb43926 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -39,7 +39,7 @@ * Mapping of example keys to their configurations. * @type {{jinja: ExampleLanguageConfig, css: ExampleLanguageConfig, js: ExampleLanguageConfig, html: ExampleLanguageConfig}} */ - const EXAMPLE_OPTIONS_CFG = { + const EXAMPLE_LANGUAGE_OPTION_CONFIG = { html: { label: 'HTML', langIdentifier: 'html', @@ -163,7 +163,7 @@ if (lang) { pre.setAttribute('data-lang', lang); - pre.classList.add('language-' + (EXAMPLE_OPTIONS_CFG[lang]?.langIdentifier || lang)); + pre.classList.add('language-' + (EXAMPLE_LANGUAGE_OPTION_CONFIG[lang]?.langIdentifier || lang)); } pre.appendChild(code); @@ -334,7 +334,7 @@ codeSnippetModes.forEach(function (option) { const optionHTML = document.createElement('option'); optionHTML.value = option.toLowerCase(); - optionHTML.innerText = EXAMPLE_OPTIONS_CFG[option]?.label || option.toLowerCase(); + optionHTML.innerText = EXAMPLE_LANGUAGE_OPTION_CONFIG[option]?.label || option.toLowerCase(); selectEl.appendChild(optionHTML); }); From 4ebf5e2c7b43747c9bbd815513b2f1e0dd43bb7f Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Fri, 6 Sep 2024 12:25:20 -0400 Subject: [PATCH 31/34] Move HTML body extraction functions into `fetchHtmlSource()` and return them there instead of generating them in the callback --- templates/static/js/example.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 6fbb43926..f13e06338 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -183,14 +183,23 @@ /** * Fetches the rendered HTML of an example and extracts the relevant sections for rendering and code snippets. - * @param {HTMLElement} placementElement The placeholder element for the example - * @returns {Promise<{rendered: String, body: String}>} The rendered HTML and source code of the example + * @param {HTMLAnchorElement} placementElement The placeholder element for the example + * @returns {Promise<{renderedHtml: String, bodyHtml: String, title: String, jsSource: String, externalScripts: NodeListOf, cssSource: String}>} The extracted sections of the example */ async function fetchHtmlSource(placementElement) { const renderedHtml = await fetchResponseText(placementElement.href); - const bodyHTML = getExampleSection('body', renderedHtml); + let bodyHtml = getExampleSection('body', renderedHtml); + + // Extract JS from the body before we strip it out + const jsSource = formatSource(getScriptFromSource(bodyHtml), 'js'); + bodyHtml = formatSource(stripScriptsFromSource(bodyHtml), 'html'); - return {rendered: renderedHtml, body: bodyHTML}; + const title = getExampleSection('title', renderedHtml).split('|')[0]; + const headHtml = getExampleSection('head', renderedHtml); + const cssSource = formatSource(getStyleFromSource(headHtml), 'css'); + const externalScripts = getExternalScriptsFromSource(renderedHtml); + + return {renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}; } /** @@ -214,8 +223,6 @@ /** * Replaces an example placeholder element with its rendered result and code snippet. * @param {HTMLAnchorElement} placementElement `a.js-example` element used as a placeholder for the example to render - * @param {String} renderedHtml Full document HTML of the example as it is shown to end-users - * @param {String|null} jinjaTemplate Jinja Template of the example as it may be used by developers, if supported */ async function renderExample(placementElement) { const codeSnippet = document.createElement('div'); @@ -237,14 +244,7 @@ const exampleRequests = []; - const fetchHtml = fetchHtmlSource(placementElement).then(({rendered: renderedHtml, body: bodyHtml}) => { - const title = getExampleSection('title', renderedHtml).split('|')[0]; - const headHtml = getExampleSection('head', renderedHtml); - const htmlBodySource = formatSource(stripScriptsFromSource(bodyHtml), 'html'); - const jsSource = formatSource(getScriptFromSource(bodyHtml), 'js'); - const cssSource = formatSource(getStyleFromSource(headHtml), 'css'); - const externalScripts = getExternalScriptsFromSource(renderedHtml); - + const fetchHtml = fetchHtmlSource(placementElement).then(({renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}) => { titleEl.innerText = title; header.appendChild(titleEl); codeSnippet.appendChild(header); @@ -258,8 +258,8 @@ } // HTML source is required, so throw if it fails - if (htmlBodySource) { - srcData.html = htmlBodySource; + if (bodyHtml) { + srcData.html = bodyHtml; } else { throw new Error('Failed to render HTML source code'); } @@ -272,7 +272,7 @@ srcData.css = cssSource; } srcData.codePen = { - html: htmlBodySource, + html: bodyHtml, css: cssSource, js: jsSource, externalJS: externalScripts, From ff31106300e347fafa559001fb4028a2377b3f0e Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Fri, 6 Sep 2024 12:26:20 -0400 Subject: [PATCH 32/34] rename `fetchHtmlSource()` to `fetchRenderedHtml()` --- templates/static/js/example.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index f13e06338..bf18766e3 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -186,7 +186,7 @@ * @param {HTMLAnchorElement} placementElement The placeholder element for the example * @returns {Promise<{renderedHtml: String, bodyHtml: String, title: String, jsSource: String, externalScripts: NodeListOf, cssSource: String}>} The extracted sections of the example */ - async function fetchHtmlSource(placementElement) { + async function fetchRenderedHtml(placementElement) { const renderedHtml = await fetchResponseText(placementElement.href); let bodyHtml = getExampleSection('body', renderedHtml); @@ -244,7 +244,7 @@ const exampleRequests = []; - const fetchHtml = fetchHtmlSource(placementElement).then(({renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}) => { + const fetchHtml = fetchRenderedHtml(placementElement).then(({renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}) => { titleEl.innerText = title; header.appendChild(titleEl); codeSnippet.appendChild(header); From c8e32c56ad6cc42fb71c58b1ebf37e7137bfb5ea Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Fri, 6 Sep 2024 12:30:01 -0400 Subject: [PATCH 33/34] Switch default example language from HTML to jinja if jinja is found --- templates/static/js/example.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index bf18766e3..14956cb16 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -297,8 +297,10 @@ // Gather the languages that have source code available, in the order they should be displayed // We can't rely on order of these languages being made available in the promise blocks above due to async nature - const languageOptions = ['html', 'jinja', 'js', 'css'].filter((lang) => srcData[lang]); - const sourceBlocks = languageOptions.map((lang, idx) => createPreCode(srcData[lang], lang, idx > 0)); + const languageOptions = ['jinja', 'html', 'js', 'css'].filter((lang) => srcData[lang]); + const sourceBlocks = languageOptions + // THe first language option that was found is displayed by default. The rest are viewable using dropdown. + .map((lang, idx) => createPreCode(srcData[lang], lang, idx > 0)); sourceBlocks.forEach((block) => codeSnippet.appendChild(block)); From 589e9a99f32f5aed54d65c7e0a40c63b84300293 Mon Sep 17 00:00:00 2001 From: Julie Muzina Date: Fri, 6 Sep 2024 13:07:40 -0400 Subject: [PATCH 34/34] Clean up parallelization per code review --- templates/static/js/example.js | 41 +++++++++++++++------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/templates/static/js/example.js b/templates/static/js/example.js index 14956cb16..125f01180 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -234,43 +234,32 @@ const titleEl = document.createElement('h5'); titleEl.classList.add('p-code-snippet__title'); + // Example data will be asynchronously fetched and placed here on promise resolution. const srcData = { html: undefined, + renderedHtml: undefined, jinja: undefined, css: undefined, js: undefined, codePen: undefined, + title: undefined, }; const exampleRequests = []; const fetchHtml = fetchRenderedHtml(placementElement).then(({renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}) => { - titleEl.innerText = title; - header.appendChild(titleEl); - codeSnippet.appendChild(header); - placementElement.parentNode.insertBefore(codeSnippet, placementElement); - - // HTML iframe is required, so throw if it fails - if (renderedHtml) { - renderIframe(codeSnippet, renderedHtml, placementElement.getAttribute('data-height')); - } else { - throw new Error('Failed to render HTML iframe'); - } - - // HTML source is required, so throw if it fails - if (bodyHtml) { + // There are required, so throw if they failed + if (renderedHtml && bodyHtml && title) { + srcData.renderedHtml = renderedHtml; srcData.html = bodyHtml; + srcData.title = title; } else { - throw new Error('Failed to render HTML source code'); + throw new Error('Failed to fetch HTML for example iframe and HTML source.'); } // The rest of the views are optional - if (jsSource) { - srcData.js = jsSource; - } - if (cssSource) { - srcData.css = cssSource; - } + srcData.js = jsSource; + srcData.css = cssSource; srcData.codePen = { html: bodyHtml, css: cssSource, @@ -293,7 +282,13 @@ // Perform as much of the data fetching and processing as possible in parallel await Promise.all(exampleRequests); - // Code after this point depends on the data above being fully fetched, so must come after an `await` block + // Code after this point depends on the data above being fully fetched, so must come after an `await` + + titleEl.innerText = srcData.title; + header.appendChild(titleEl); + codeSnippet.appendChild(header); + placementElement.parentNode.insertBefore(codeSnippet, placementElement); + renderIframe(codeSnippet, srcData.renderedHtml, placementElement.getAttribute('data-height')); // Gather the languages that have source code available, in the order they should be displayed // We can't rely on order of these languages being made available in the promise blocks above due to async nature @@ -302,8 +297,8 @@ // THe first language option that was found is displayed by default. The rest are viewable using dropdown. .map((lang, idx) => createPreCode(srcData[lang], lang, idx > 0)); + // Code snippet must be populated with code before Prism can highlight it sourceBlocks.forEach((block) => codeSnippet.appendChild(block)); - if (Prism) { Prism.highlightAllUnder(codeSnippet); }