Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(macros/EmbedInteractiveExample): use height from height-data.json #7679

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions build/extract-sections.ts
Original file line number Diff line number Diff line change
@@ -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[]];

Expand All @@ -22,7 +23,9 @@ export async function extractSections(
for (const child of iterable) {
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] = await addSections(section.clone());
Expand Down Expand Up @@ -168,7 +171,9 @@ async function addSections(
): Promise<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:
Expand Down Expand Up @@ -258,7 +263,7 @@ async function addSections(
}
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);
Expand All @@ -273,6 +278,7 @@ async function addSections(
// 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);
Expand Down Expand Up @@ -323,6 +329,9 @@ async 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
Expand Down Expand Up @@ -368,6 +377,24 @@ async 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}'`);
Expand Down
15 changes: 14 additions & 1 deletion client/src/document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { useInteractiveExamplesActionHandler as useInteractiveExamplesTelemetry
import { BottomBanner, SidePlacement } from "../ui/organisms/placement";
import { BaselineIndicator } from "./baseline-indicator";
import { PlayQueue } from "../playground/queue";
import { InteractiveExample } from "./ingredients/interactive-example";
// import { useUIStatus } from "../ui-context";

// Lazy sub-components
Expand Down Expand Up @@ -278,7 +279,12 @@ export function Document(props /* TODO: define a TS interface for this */) {
export function RenderDocumentBody({ doc }) {
return doc.body.map((section, i) => {
if (section.type === "prose") {
return <Prose key={section.value.id} section={section.value} />;
return (
<Prose
key={section.value.id || `top_level_prose${i}`}
section={section.value}
/>
);
} else if (section.type === "browser_compatibility") {
return (
<LazyBrowserCompatibilityTable
Expand All @@ -290,6 +296,13 @@ export function RenderDocumentBody({ doc }) {
return (
<SpecificationSection key={`specifications${i}`} {...section.value} />
);
} else if (section.type === "interactive_example") {
return (
<InteractiveExample
key={`interactive_example${i}`}
{...section.value}
/>
);
} else {
console.warn(section);
throw new Error(`No idea how to handle a '${section.type}' section`);
Expand Down
83 changes: 83 additions & 0 deletions client/src/document/ingredients/interactive-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { 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<string, string>;
}

/**
* 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<HTMLIFrameElement>(null);

useLayoutEffect(() => {
if (ref.current) {
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 iframe, heights))
);
observer.observe(iframe);
return () => (iframe ? observer.unobserve(iframe) : undefined);
}
}, [heights]);

return (
<iframe
ref={ref}
className="interactive"
src={src}
title="MDN Web Docs Interactive Example"
></iframe>
);
}

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)
);
}
76 changes: 1 addition & 75 deletions client/src/document/interactive-examples.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
36 changes: 17 additions & 19 deletions kumascript/macros/EmbedInteractiveExample.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,15 @@
//
// 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';
const heights = await mdn.getInteractiveExampleHeight($0);

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}')`);
}
}
// 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({
"en-US": {
Expand Down Expand Up @@ -56,6 +43,17 @@ const text = mdn.localStringMap({
}
});

if (heights) {
// iframe is later replaced by interactive-example.tsx
%>
<h2><%=text["title"]%></h2>
<iframe class="interactive" height="200" src="<%= url %>" title="MDN Web Docs Interactive Example" data-heights="<%= heightsStr %>"></iframe>
<%
} else {
%>
<h2><%=text["title"]%></h2>
<iframe class="interactive <%= heightClass %>" height="200" src="<%= url %>" title="MDN Web Docs Interactive Example"></iframe>
<div class="notecard warning">
<p>
Error! Height of interactive example couldn't be fetched.
</p>
</div>
<% } %>
Loading