From 7ba3075eeb21d0c33e3bd9c63ba6d0b857ce25a0 Mon Sep 17 00:00:00 2001 From: Niedziolka Michal Date: Sat, 26 Nov 2022 23:44:30 +0100 Subject: [PATCH 1/6] Interactive Examples use height from height-data.json --- kumascript/macros/EmbedInteractiveExample.ejs | 27 ++++++++----------- kumascript/src/api/mdn.ts | 23 ++++++++++++++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/kumascript/macros/EmbedInteractiveExample.ejs b/kumascript/macros/EmbedInteractiveExample.ejs index 19dc9d8d6323..53a6f06b7299 100644 --- a/kumascript/macros/EmbedInteractiveExample.ejs +++ b/kumascript/macros/EmbedInteractiveExample.ejs @@ -3,28 +3,23 @@ // // Parameters: // $0 - The URL of interactive-examples.mdn.mozilla.net page (relative) -// $1 - Optional custom height class to set on iframe element // -// Example call {{EmbedInteractiveExample("pages/css/animation.html", "taller")}} +// Example call {{EmbedInteractiveExample("pages/css/animation.html")}} // const url = (new URL($0, env.interactive_examples.base_url)).toString(); -let heightClass = 'is-default-height'; +// Iframe height is acquired from height-data.json file exported by interactive-examples +const heightData = await mdn.fetchInteractiveExampleHeightData(); -if ($0.includes('/js/')) { - heightClass = 'is-js-height'; -} - -if ($1) { - let supportedHeights = ['shorter', 'taller', 'tabbed-shorter', 'tabbed-standard', 'tabbed-taller']; - let heightIsSupported = (supportedHeights.indexOf($1) > -1); - - if (heightIsSupported) { - heightClass = 'is-' + $1 + '-height'; - } else { - throw new Error(`An unrecognized second size parameter to EmbedInteractiveExample ('${$1}')`); - } +let height; +if (heightData[$0]) { + height = heightData[$0]; +} else if ($0.includes('/js/')) { + height = 'js'; +} else { + height = 'default'; } +const heightClass = 'is-' + height + '-height'; const text = mdn.localStringMap({ "en-US": { diff --git a/kumascript/src/api/mdn.ts b/kumascript/src/api/mdn.ts index f750cef3afe7..86574e6f7c72 100644 --- a/kumascript/src/api/mdn.ts +++ b/kumascript/src/api/mdn.ts @@ -1,10 +1,13 @@ import got from "got"; import { KumaThis } from "../environment.js"; import * as util from "./util.js"; +import { INTERACTIVE_EXAMPLES_BASE_URL } from "../../../libs/env.js"; // Module level caching for repeat calls to fetchWebExtExamples(). let webExtExamples: any = null; +let interactiveExampleHeightData: object = null; + const mdn = { /** * Given a set of names and a corresponding list of values, apply HTML @@ -163,6 +166,26 @@ const mdn = { } return webExtExamples; }, + + /** + * Fetching height-data.json from interactive-examples, which contains height class for every interactive example. + */ + async fetchInteractiveExampleHeightData() { + if (!interactiveExampleHeightData) { + try { + interactiveExampleHeightData = await got( + INTERACTIVE_EXAMPLES_BASE_URL + "/height-data.json", + { + timeout: 1000, + retry: 5, + } + ).json(); + } catch (error) { + interactiveExampleHeightData = {}; + } + } + return interactiveExampleHeightData; + }, }; export default mdn; From 7a1017828a7b19684f9a5c58ef057088e9ec65a6 Mon Sep 17 00:00:00 2001 From: Niedziolka Michal Date: Wed, 18 Jan 2023 23:59:18 +0100 Subject: [PATCH 2/6] Use height of editor from height-data.json --- build/extract-sections.ts | 33 +++++++- client/src/document/index.tsx | 15 +++- .../ingredients/interactive-example.tsx | 84 +++++++++++++++++++ client/src/document/interactive-examples.scss | 76 +---------------- kumascript/macros/EmbedInteractiveExample.ejs | 30 ++++--- kumascript/src/api/mdn.ts | 7 +- libs/types/document.ts | 19 ++++- 7 files changed, 170 insertions(+), 94 deletions(-) create mode 100644 client/src/document/ingredients/interactive-example.tsx diff --git a/build/extract-sections.ts b/build/extract-sections.ts index be4f6f8a6d2b..dc7674836c6e 100644 --- a/build/extract-sections.ts +++ b/build/extract-sections.ts @@ -1,6 +1,7 @@ import * as cheerio from "cheerio"; import { ProseSection, Section } from "../libs/types/document.js"; import { extractSpecifications } from "./extract-specifications.js"; +import { InteractiveEditorHeights } from "../client/src/document/ingredients/interactive-example.js"; type SectionsAndFlaws = [Section[], string[]]; @@ -20,7 +21,9 @@ export function extractSections($: cheerio.CheerioAPI): [Section[], string[]] { iterable.forEach((child) => { if ( (child as cheerio.Element).tagName === "h2" || - (child as cheerio.Element).tagName === "h3" + (child as cheerio.Element).tagName === "h3" || + (child.tagName === "iframe" && + child.attribs.class?.includes("interactive")) // Interactive Example ) { if (c) { const [subSections, subFlaws] = addSections(section.clone()); @@ -163,7 +166,9 @@ export function extractSections($: cheerio.CheerioAPI): [Section[], string[]] { function addSections($: cheerio.Cheerio): SectionsAndFlaws { const flaws: string[] = []; - const countPotentialSpecialDivs = $.find("div.bc-data, div.bc-specs").length; + const countPotentialSpecialDivs = $.find( + "div.bc-data, div.bc-specs, iframe.interactive" + ).length; if (countPotentialSpecialDivs) { /** If there's exactly 1 special table the only section to add is something * like this: @@ -251,7 +256,7 @@ function addSections($: cheerio.Cheerio): SectionsAndFlaws { } if (countSpecialDivsFound !== countPotentialSpecialDivs) { const leftoverCount = countPotentialSpecialDivs - countSpecialDivsFound; - const explanation = `${leftoverCount} 'div.bc-data' or 'div.bc-specs' element${ + const explanation = `${leftoverCount} 'div.bc-data', 'div.bc-specs' or 'iframe.interactive' element${ leftoverCount > 1 ? "s" : "" } found but deeply nested.`; flaws.push(explanation); @@ -266,6 +271,7 @@ function addSections($: cheerio.Cheerio): SectionsAndFlaws { // section underneath. $.find("div.bc-data, h2, h3").remove(); $.find("div.bc-specs, h2, h3").remove(); + $.find("iframe.interactive").remove(); const [proseSections, proseFlaws] = _addSectionProse($); specialSections.push(...proseSections); flaws.push(...proseFlaws); @@ -316,6 +322,9 @@ function _addSingleSpecialSection( specialSectionType = "specifications"; dataQuery = $.find("div.bc-specs").attr("data-bcd-query") ?? ""; specURLsString = $.find("div.bc-specs").attr("data-spec-urls") ?? ""; + } else if ($.find("iframe.interactive").length) { + specialSectionType = "interactive_example"; + dataQuery = $.find("iframe.interactive").attr("src"); } // Some old legacy documents haven't been re-rendered yet, since it @@ -361,6 +370,24 @@ function _addSingleSpecialSection( }, }, ]; + } else if (specialSectionType === "interactive_example") { + const iframe = $.find("iframe.interactive"); + const heights = JSON.parse( + iframe.attr("data-heights") + ) as InteractiveEditorHeights; + + return [ + { + type: specialSectionType, + value: { + title, + id, + isH3, + src: query, + heights, + }, + }, + ]; } throw new Error(`Unrecognized special section type '${specialSectionType}'`); diff --git a/client/src/document/index.tsx b/client/src/document/index.tsx index ed030a95c653..0b8366a76a73 100644 --- a/client/src/document/index.tsx +++ b/client/src/document/index.tsx @@ -38,6 +38,7 @@ import "./index.scss"; import "./interactive-examples.scss"; import { DocumentSurvey } from "../ui/molecules/document-survey"; import { useIncrementFrequentlyViewed } from "../plus/collections/frequently-viewed"; +import { InteractiveExample } from "./ingredients/interactive-example"; // import { useUIStatus } from "../ui-context"; // Lazy sub-components @@ -257,7 +258,12 @@ export function Document(props /* TODO: define a TS interface for this */) { function RenderDocumentBody({ doc }) { return doc.body.map((section, i) => { if (section.type === "prose") { - return ; + return ( + + ); } else if (section.type === "browser_compatibility") { return ( ); + } else if (section.type === "interactive_example") { + return ( + + ); } else { console.warn(section); throw new Error(`No idea how to handle a '${section.type}' section`); diff --git a/client/src/document/ingredients/interactive-example.tsx b/client/src/document/ingredients/interactive-example.tsx new file mode 100644 index 000000000000..cf20157baa25 --- /dev/null +++ b/client/src/document/ingredients/interactive-example.tsx @@ -0,0 +1,84 @@ +import { useEffect, useLayoutEffect, useRef } from "react"; + +interface InteractiveEditorHeight { + minFrameWidth: number; + height: number; +} +export type InteractiveEditorHeights = InteractiveEditorHeight[]; +interface InteractiveEditor { + name: string; + heights: InteractiveEditorHeights; +} +export interface InteractiveExamplesHeightData { + editors: InteractiveEditor[]; + examples: Record; +} + +/** + * Replaces iframe created by EmbedInteractiveExample.ejs and sets its height dynamically based on editor heights provided from height-data.json + */ +export function InteractiveExample({ + src, + heights, +}: { + src: string; + heights: InteractiveEditorHeights; +}) { + const ref = useRef(null); + + useLayoutEffect(() => { + if (ref.current) { + setHeight(ref.current, heights); + + // Updating height whenever iframe is resized + const observer = new ResizeObserver((entries) => + entries.forEach((e) => + setHeight(e.target as typeof ref.current, heights) + ) + ); + observer.observe(ref.current); + return () => (ref.current ? observer.unobserve(ref.current) : undefined); + } + }, [ref.current]); + + return ( + + ); +} + +function setHeight(frame: HTMLIFrameElement, heights) { + const frameWidth = getIFrameWidth(frame); + const height = calculateHeight(frameWidth, heights); + frame.style.height = height; +} + +/** + * Calculates height of the iframe based on its width and data provided by height-data.json + */ +function calculateHeight( + frameWidth: number, + heights: InteractiveEditorHeights +) { + let frameHeight = 0; + for (const height of heights) { + if (frameWidth >= height.minFrameWidth) { + frameHeight = height.height; + } + } + return `${frameHeight}px`; +} + +function getIFrameWidth(frame: HTMLIFrameElement) { + const styles = getComputedStyle(frame); + + return ( + frame.clientWidth - + parseFloat(styles.paddingLeft) - + parseFloat(styles.paddingRight) + ); +} diff --git a/client/src/document/interactive-examples.scss b/client/src/document/interactive-examples.scss index b5475f98d6e3..87aba712c2ca 100644 --- a/client/src/document/interactive-examples.scss +++ b/client/src/document/interactive-examples.scss @@ -3,84 +3,10 @@ .interactive { background-color: var(--background-secondary); border: none; - border-radius: var(--elem-radius); color: var(--text-primary); - // Since heights are now responsive, these classes are added - // in the EmbedInteractiveExample.ejs macro. - height: 675px; margin: 1rem 0; padding: 0; width: 100%; - - &.is-js-height, - &.is-taller-height, - &.is-shorter-height { - border: 0 none; - } - - &.is-js-height { - height: 513px; - } - - &.is-shorter-height { - height: 433px; - } - - &.is-taller-height { - height: 725px; - } - - &.is-tabbed-shorter-height { - height: 487px; - } - - &.is-tabbed-standard-height { - height: 548px; - } - - &.is-tabbed-taller-height { - height: 774px; - } -} - -// The layout switches at 590px in the `mdn/bob` app. -// In order to respect the height shifts without using -// JS, a complicated media query is needed. This is -// fragile, as if the margins or anything changes -// on the main layout, this will need to be adjusted. - -// This spans from the time the iframe is 590px -// wide in the mobile layout to the time it switches -// to two columns. Then, from the time the iframe -// is 590px wide in the two-column layout on up. -@media screen and (min-width: 688px) and (max-width: $screen-md - 1), - screen and (min-width: 1008px) { - .interactive { - height: 375px; - - &.is-js-height { - height: 444px; - } - - &.is-shorter-height { - height: 364px; - } - - &.is-taller-height { - height: 654px; - } - - &.is-tabbed-shorter-height { - height: 351px; - } - - &.is-tabbed-standard-height { - height: 421px; - } - - &.is-tabbed-taller-height { - height: 631px; - } - } + // Height of the editor is set in interactive-example.tsx } diff --git a/kumascript/macros/EmbedInteractiveExample.ejs b/kumascript/macros/EmbedInteractiveExample.ejs index 53a6f06b7299..55116453b2a3 100644 --- a/kumascript/macros/EmbedInteractiveExample.ejs +++ b/kumascript/macros/EmbedInteractiveExample.ejs @@ -11,15 +11,12 @@ const url = (new URL($0, env.interactive_examples.base_url)).toString(); // Iframe height is acquired from height-data.json file exported by interactive-examples const heightData = await mdn.fetchInteractiveExampleHeightData(); -let height; -if (heightData[$0]) { - height = heightData[$0]; -} else if ($0.includes('/js/')) { - height = 'js'; -} else { - height = 'default'; -} -const heightClass = 'is-' + height + '-height'; +const editorName = heightData?.examples?.[$0]; +const editors = heightData?.editors; +const editor = editors?.find(e => e.name === editorName); +const heights = editor?.heights; +// We get heights in form of a string, so we can place it in data-heights attribute, used by interactive-example.tsx +const heightsStr = JSON.stringify(heights || "", null, ""); const text = mdn.localStringMap({ "en-US": { @@ -51,6 +48,17 @@ const text = mdn.localStringMap({ } }); +if (heights) { + // iframe is later replaced by interactive-example.tsx +%> +

<%=text["title"]%>

+ +<% +} else { %> -

<%=text["title"]%>

- +
+

+ Error! Height of interactive example couldn't be fetched. +

+
+<% } %> diff --git a/kumascript/src/api/mdn.ts b/kumascript/src/api/mdn.ts index 86574e6f7c72..8d49f21a4866 100644 --- a/kumascript/src/api/mdn.ts +++ b/kumascript/src/api/mdn.ts @@ -2,11 +2,12 @@ import got from "got"; import { KumaThis } from "../environment.js"; import * as util from "./util.js"; import { INTERACTIVE_EXAMPLES_BASE_URL } from "../../../libs/env.js"; +import { InteractiveExamplesHeightData } from "../../../client/src/document/ingredients/interactive-example.js"; // Module level caching for repeat calls to fetchWebExtExamples(). let webExtExamples: any = null; -let interactiveExampleHeightData: object = null; +let interactiveExampleHeightData: InteractiveExamplesHeightData | null = null; const mdn = { /** @@ -173,7 +174,7 @@ const mdn = { async fetchInteractiveExampleHeightData() { if (!interactiveExampleHeightData) { try { - interactiveExampleHeightData = await got( + interactiveExampleHeightData = await got( INTERACTIVE_EXAMPLES_BASE_URL + "/height-data.json", { timeout: 1000, @@ -181,7 +182,7 @@ const mdn = { } ).json(); } catch (error) { - interactiveExampleHeightData = {}; + interactiveExampleHeightData = null; } } return interactiveExampleHeightData; diff --git a/libs/types/document.ts b/libs/types/document.ts index e3ca384741dd..c236019b4141 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -1,3 +1,5 @@ +import { InteractiveEditorHeights } from "../../client/src/document/ingredients/interactive-example"; + export interface Source { folder: string; github_url: string; @@ -168,7 +170,11 @@ export interface DocFrontmatter { original_slug?: string; } -export type Section = ProseSection | SpecificationsSection | BCDSection; +export type Section = + | ProseSection + | SpecificationsSection + | BCDSection + | InteractiveExample; export interface ProseSection { type: "prose"; @@ -207,6 +213,17 @@ export interface BCDSection { }; } +export interface InteractiveExample { + type: "interactive_example"; + value: { + id: string; + title: string; + isH3: boolean; + src: string; + heights: InteractiveEditorHeights; + }; +} + export interface NewsItem { url: string; title: string; From 75bb59e19ff96f5cb1b9f6146afcf123d4b6c2a3 Mon Sep 17 00:00:00 2001 From: Niedziolka Michal Date: Thu, 19 Jan 2023 00:35:50 +0100 Subject: [PATCH 3/6] Move logic to `getInteractiveExampleHeight` --- kumascript/macros/EmbedInteractiveExample.ejs | 9 +---- kumascript/src/api/mdn.ts | 38 +++++++++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/kumascript/macros/EmbedInteractiveExample.ejs b/kumascript/macros/EmbedInteractiveExample.ejs index 55116453b2a3..97a35ed28e40 100644 --- a/kumascript/macros/EmbedInteractiveExample.ejs +++ b/kumascript/macros/EmbedInteractiveExample.ejs @@ -8,14 +8,9 @@ // const url = (new URL($0, env.interactive_examples.base_url)).toString(); -// Iframe height is acquired from height-data.json file exported by interactive-examples -const heightData = await mdn.fetchInteractiveExampleHeightData(); +const heights = await mdn.getInteractiveExampleHeight(url); -const editorName = heightData?.examples?.[$0]; -const editors = heightData?.editors; -const editor = editors?.find(e => e.name === editorName); -const heights = editor?.heights; -// We get heights in form of a string, so we can place it in data-heights attribute, used by interactive-example.tsx +// We convert the heights to a string, so we can place it in the data-heights attribute, which is used by interactive-example.tsx const heightsStr = JSON.stringify(heights || "", null, ""); const text = mdn.localStringMap({ diff --git a/kumascript/src/api/mdn.ts b/kumascript/src/api/mdn.ts index 8d49f21a4866..195bfb6928a9 100644 --- a/kumascript/src/api/mdn.ts +++ b/kumascript/src/api/mdn.ts @@ -2,7 +2,10 @@ import got from "got"; import { KumaThis } from "../environment.js"; import * as util from "./util.js"; import { INTERACTIVE_EXAMPLES_BASE_URL } from "../../../libs/env.js"; -import { InteractiveExamplesHeightData } from "../../../client/src/document/ingredients/interactive-example.js"; +import { + InteractiveEditorHeights, + InteractiveExamplesHeightData, +} from "../../../client/src/document/ingredients/interactive-example.js"; // Module level caching for repeat calls to fetchWebExtExamples(). let webExtExamples: any = null; @@ -169,9 +172,9 @@ const mdn = { }, /** - * Fetching height-data.json from interactive-examples, which contains height class for every interactive example. + * Fetching height-data.json from interactive-examples, which contains height information of every interactive example */ - async fetchInteractiveExampleHeightData() { + async fetchInteractiveExampleHeightData(): Promise { if (!interactiveExampleHeightData) { try { interactiveExampleHeightData = await got( @@ -187,6 +190,35 @@ const mdn = { } return interactiveExampleHeightData; }, + + /** + * @param pagePath - for example "pages/css/animation.html" + * @returns exact information about height of a given interactive example. Exemplary output: + * [ + * { + * "minFrameWidth": 0, + * "height": 723 + * }, + * { + * "minFrameWidth": 590, + * "height": 654 + * } + * ] + */ + async getInteractiveExampleHeight( + pagePath: string + ): Promise { + const heightData = await mdn.fetchInteractiveExampleHeightData(); + + if (!heightData) { + return undefined; + } + + const editorName = heightData.examples?.pagePath; + const editors = heightData.editors; + const editor = editors?.find((e) => e.name === editorName); + return editor?.heights; + }, }; export default mdn; From f0f4200b28e2af3076324748cb186fc9e75c8a10 Mon Sep 17 00:00:00 2001 From: Niedziolka Michal Date: Thu, 19 Jan 2023 00:45:45 +0100 Subject: [PATCH 4/6] Applied github lint suggestions --- .../document/ingredients/interactive-example.tsx | 15 +++++++-------- kumascript/src/api/mdn.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/client/src/document/ingredients/interactive-example.tsx b/client/src/document/ingredients/interactive-example.tsx index cf20157baa25..9ac7c014d1dd 100644 --- a/client/src/document/ingredients/interactive-example.tsx +++ b/client/src/document/ingredients/interactive-example.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; +import { useLayoutEffect, useRef } from "react"; interface InteractiveEditorHeight { minFrameWidth: number; @@ -28,18 +28,17 @@ export function InteractiveExample({ useLayoutEffect(() => { if (ref.current) { - setHeight(ref.current, heights); + const iframe = ref.current; + setHeight(iframe, heights); // Updating height whenever iframe is resized const observer = new ResizeObserver((entries) => - entries.forEach((e) => - setHeight(e.target as typeof ref.current, heights) - ) + entries.forEach((e) => setHeight(e.target as typeof iframe, heights)) ); - observer.observe(ref.current); - return () => (ref.current ? observer.unobserve(ref.current) : undefined); + observer.observe(iframe); + return () => (iframe ? observer.unobserve(iframe) : undefined); } - }, [ref.current]); + }, [ref.current, heights]); return (