From 42b003e4c2c357ce69c9fdf6094cd75ebac2b9de Mon Sep 17 00:00:00 2001 From: Niedziolka Michal Date: Sun, 12 Mar 2023 12:27:16 +0100 Subject: [PATCH 01/17] refactor: Convert editor/js files to .ts --- .eslintrc.json | 7 + .prettierignore | 1 + .../js/{editable-css.js => editable-css.ts} | 4 +- editor/js/{editable-js.js => editable-js.ts} | 6 +- .../js/{editable-wat.js => editable-wat.ts} | 21 +- .../js/editor-libs/{clippy.js => clippy.ts} | 10 +- ...emirror-editor.js => codemirror-editor.ts} | 8 +- .../{console-utils.js => console-utils.ts} | 0 .../js/editor-libs/{console.js => console.ts} | 14 +- ...ss-editor-utils.js => css-editor-utils.ts} | 13 +- .../js/editor-libs/{events.js => events.ts} | 13 +- ...eature-detector.js => feature-detector.ts} | 0 .../{mce-utils.js => mce-utils.ts} | 2 +- editor/js/editor-libs/{tabby.js => tabby.ts} | 17 +- editor/js/{editor.js => editor.ts} | 10 +- package-lock.json | 327 +++++++++++++++++- package.json | 3 +- 17 files changed, 402 insertions(+), 54 deletions(-) rename editor/js/{editable-css.js => editable-css.ts} (97%) rename editor/js/{editable-js.js => editable-js.ts} (96%) rename editor/js/{editable-wat.js => editable-wat.ts} (93%) rename editor/js/editor-libs/{clippy.js => clippy.ts} (88%) rename editor/js/editor-libs/{codemirror-editor.js => codemirror-editor.ts} (96%) rename editor/js/editor-libs/{console-utils.js => console-utils.ts} (100%) rename editor/js/editor-libs/{console.js => console.ts} (68%) rename editor/js/editor-libs/{css-editor-utils.js => css-editor-utils.ts} (96%) rename editor/js/editor-libs/{events.js => events.ts} (93%) rename editor/js/editor-libs/{feature-detector.js => feature-detector.ts} (100%) rename editor/js/editor-libs/{mce-utils.js => mce-utils.ts} (97%) rename editor/js/editor-libs/{tabby.js => tabby.ts} (91%) rename editor/js/{editor.js => editor.ts} (96%) diff --git a/.eslintrc.json b/.eslintrc.json index 6f1419934..dcba84eef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,6 +9,7 @@ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:import/errors", + "plugin:import/typescript", "prettier" ], "parser": "@typescript-eslint/parser", @@ -19,5 +20,11 @@ "rules": { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unused-expressions": "error" + }, + "settings": { + "import/resolver": { + "typescript": true, + "node": true + } } } diff --git a/.prettierignore b/.prettierignore index 02bf4afda..136156b0b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ CHANGELOG.md # TSC output: webpack.config.js lib/*.js +editor/js/**/*.js diff --git a/editor/js/editable-css.js b/editor/js/editable-css.ts similarity index 97% rename from editor/js/editable-css.js rename to editor/js/editable-css.ts index be9946d32..6ff5302f2 100644 --- a/editor/js/editable-css.js +++ b/editor/js/editable-css.ts @@ -125,10 +125,10 @@ import "../css/editable-css.css"; copyBtn.setAttribute("aria-label", "Copy to clipboard"); choice.addEventListener("mouseover", () => { - copyBtn.setAttribute("aria-hidden", false); + copyBtn.setAttribute("aria-hidden", "false"); }); choice.addEventListener("mouseout", () => { - copyBtn.setAttribute("aria-hidden", true); + copyBtn.setAttribute("aria-hidden", "true"); }); } } diff --git a/editor/js/editable-js.js b/editor/js/editable-js.ts similarity index 96% rename from editor/js/editable-js.js rename to editor/js/editable-js.ts index 9a53e1b93..ad4cce8ee 100644 --- a/editor/js/editable-js.js +++ b/editor/js/editable-js.ts @@ -20,7 +20,7 @@ import { let codeMirror; let staticContainer; - let liveContainer = ""; + let liveContainer; /** * Reads the textContent from the interactiveCodeBlock, sends the @@ -79,8 +79,8 @@ import { try { // Create a new Function from the code, and immediately execute it. new Function(exampleCode)(); - } catch (event) { - output.textContent = "Error: " + event.message; + } catch (event: unknown) { + output.textContent = "Error: " + (event as Error)?.message; } output.addEventListener("animationend", () => diff --git a/editor/js/editable-wat.js b/editor/js/editable-wat.ts similarity index 93% rename from editor/js/editable-wat.js rename to editor/js/editable-wat.ts index 5ba017038..e52926d38 100644 --- a/editor/js/editable-wat.js +++ b/editor/js/editable-wat.ts @@ -24,12 +24,13 @@ import { const wabt = await wabtConstructor(); const tabContainer = document.getElementById("tab-container"); - const tabs = tabContainer.querySelectorAll("button[role='tab']"); + const tabs = + tabContainer.querySelectorAll("button[role='tab']"); const tabList = document.getElementById("tablist"); let watCodeMirror; let jsCodeMirror; - let liveContainer = ""; + let liveContainer; let staticContainer; /** @@ -47,7 +48,7 @@ import { function registerEventListeners() { tabList.addEventListener("click", (event) => { - const eventTarget = event.target; + const eventTarget = event.target as typeof tabList; const role = eventTarget.getAttribute("role"); if (role === "tab") { @@ -61,7 +62,7 @@ import { // now show the selected tabpanel selectedPanel.classList.remove("hidden"); - selectedPanel.setAttribute("aria-hidden", false); + selectedPanel.setAttribute("aria-hidden", "false"); } }); @@ -94,7 +95,7 @@ import { * @param {Object} nextActiveTab - The tab to activate * @param {Object} [activeTab] - The current active tab */ - function setActiveTab(nextActiveTab, activeTab) { + function setActiveTab(nextActiveTab, activeTab?) { if (activeTab) { // set the currentSelectedTab to false activeTab.setAttribute("aria-selected", false); @@ -122,7 +123,7 @@ import { } if (direction === "forward") { - if (activeTab.nextElementSibling) { + if (activeTab.nextElementSibling instanceof HTMLElement) { setActiveTab(activeTab.nextElementSibling, activeTab); activeTab.nextElementSibling.click(); } else { @@ -131,7 +132,7 @@ import { tabs[0].click(); } } else if (direction === "reverse") { - if (activeTab.previousElementSibling) { + if (activeTab.previousElementSibling instanceof HTMLElement) { setActiveTab(activeTab.previousElementSibling, activeTab); activeTab.previousElementSibling.click(); } else { @@ -244,8 +245,10 @@ import { // Create an new async function from the code, and immediately execute it. // using an async function since WebAssembly.instantiate is async and // we need to await in order to capture errors - // eslint-disable-next-line @typescript-eslint/no-empty-function - const AsyncFunction = async function () {}.constructor; + const AsyncFunction = Object.getPrototypeOf( + // eslint-disable-next-line @typescript-eslint/no-empty-function + async function () {} + ).constructor; await new AsyncFunction(exampleCode)(); } catch (error) { console.error(error); diff --git a/editor/js/editor-libs/clippy.js b/editor/js/editor-libs/clippy.ts similarity index 88% rename from editor/js/editor-libs/clippy.js rename to editor/js/editor-libs/clippy.ts index 72c72821a..70cbc7001 100644 --- a/editor/js/editor-libs/clippy.js +++ b/editor/js/editor-libs/clippy.ts @@ -28,7 +28,7 @@ function setClippyPosition(clippyEvent, msgContainer) { export function addClippy() { const clipboard = new Clipboard(".copy", { target: function (clippyButton) { - const targetAttr = clippyButton.dataset.clipboardTarget; + const targetAttr = (clippyButton as HTMLElement).dataset.clipboardTarget; if (targetAttr) { // The attribute will override the automated target selection return document.querySelector(targetAttr); @@ -46,13 +46,13 @@ export function addClippy() { const msgContainer = document.getElementById("user-message"); msgContainer.classList.add("show"); - msgContainer.setAttribute("aria-hidden", false); + msgContainer.setAttribute("aria-hidden", "false"); setClippyPosition(event, msgContainer); window.setTimeout(() => { msgContainer.classList.remove("show"); - msgContainer.setAttribute("aria-hidden", true); + msgContainer.setAttribute("aria-hidden", "true"); }, 1000); event.clearSelection(); @@ -70,9 +70,9 @@ export function toggleClippy(container) { for (let i = 0, l = clippyButtons.length; i < l; i++) { clippyButtons[i].classList.add("hidden"); - clippyButtons[i].setAttribute("aria-hidden", true); + clippyButtons[i].setAttribute("aria-hidden", "true"); } activeClippy.classList.remove("hidden"); - activeClippy.setAttribute("aria-hidden", false); + activeClippy.setAttribute("aria-hidden", "false"); } diff --git a/editor/js/editor-libs/codemirror-editor.js b/editor/js/editor-libs/codemirror-editor.ts similarity index 96% rename from editor/js/editor-libs/codemirror-editor.js rename to editor/js/editor-libs/codemirror-editor.ts index e66906f78..797ff9246 100644 --- a/editor/js/editor-libs/codemirror-editor.js +++ b/editor/js/editor-libs/codemirror-editor.ts @@ -17,7 +17,7 @@ import { javascript, javascriptLanguage } from "@codemirror/lang-javascript"; import { wast } from "@codemirror/lang-wast"; import { css, cssLanguage } from "@codemirror/lang-css"; -import { parseMixed } from "@lezer/common"; +import { NodeType, parseMixed } from "@lezer/common"; import { tags } from "@lezer/highlight"; import { parser as jsParser } from "@lezer/javascript"; import { parser as htmlParser } from "@lezer/html"; @@ -143,7 +143,9 @@ function highlighting(specs) { function scopedHighlighting(specs, scope, nodeName = undefined) { const style = HighlightStyle.define(specs, { scope: scope }); if (nodeName) { - style.scope = (node) => node.name === nodeName; // This line overrides internal scope check, because alternative parsers don't attach chosen language to props + type Writeable = { -readonly [P in keyof T]: T[P] }; + (style as Writeable).scope = (node: NodeType) => + node.name === nodeName; // This line overrides internal scope check, because alternative parsers don't attach chosen language to props } return syntaxHighlighting(style, { fallback: false }); } @@ -266,7 +268,7 @@ export function initCodeEditor( const extensions = [...BASE_EXTENSIONS, ...language.extensions]; if (options.lineNumbers) { - extensions.push(view.lineNumbers(), view.gutter()); + extensions.push(view.lineNumbers(), view.gutter({})); } return new EditorView({ diff --git a/editor/js/editor-libs/console-utils.js b/editor/js/editor-libs/console-utils.ts similarity index 100% rename from editor/js/editor-libs/console-utils.js rename to editor/js/editor-libs/console-utils.ts diff --git a/editor/js/editor-libs/console.js b/editor/js/editor-libs/console.ts similarity index 68% rename from editor/js/editor-libs/console.js rename to editor/js/editor-libs/console.ts index b7c144b8d..78aee1969 100644 --- a/editor/js/editor-libs/console.js +++ b/editor/js/editor-libs/console.ts @@ -1,29 +1,29 @@ import { writeOutput, formatOutput } from "./console-utils.js"; // Thanks in part to https://stackoverflow.com/questions/11403107/capturing-javascript-console-log -export default function (targetWindow) { +export default function (targetWindow?) { /* Getting reference to console, either from current window or from the iframe window */ const console = targetWindow ? targetWindow.console : window.console; const originalConsoleLogger = console.log; // eslint-disable-line no-console const originalConsoleError = console.error; - console.error = function (loggedItem) { + console.error = function (loggedItem, ...otherArgs) { writeOutput(loggedItem); // do not swallow console.error - originalConsoleError.apply(console, arguments); + originalConsoleError.apply(console, [loggedItem, ...otherArgs]); }; // eslint-disable-next-line no-console - console.log = function () { + console.log = function (...args) { const formattedList = []; - for (let i = 0, l = arguments.length; i < l; i++) { - const formatted = formatOutput(arguments[i]); + for (let i = 0, l = args.length; i < l; i++) { + const formatted = formatOutput(args[i]); formattedList.push(formatted); } const output = formattedList.join(" "); writeOutput(output); // do not swallow console.log - originalConsoleLogger.apply(console, arguments); + originalConsoleLogger.apply(console, args); }; } diff --git a/editor/js/editor-libs/css-editor-utils.js b/editor/js/editor-libs/css-editor-utils.ts similarity index 96% rename from editor/js/editor-libs/css-editor-utils.js rename to editor/js/editor-libs/css-editor-utils.ts index ce615a1cf..ca19b5d68 100644 --- a/editor/js/editor-libs/css-editor-utils.js +++ b/editor/js/editor-libs/css-editor-utils.ts @@ -1,6 +1,11 @@ export let editTimer = undefined; -export function applyCode(code, choice, targetElement, immediateInvalidChange) { +export function applyCode( + code, + choice, + targetElement?, + immediateInvalidChange? +) { // http://regexr.com/3fvik const cssCommentsMatch = /(\/\*)[\s\S]+(\*\/)/g; const element = targetElement || document.getElementById("example-element"); @@ -85,7 +90,7 @@ export function isCodeSupported(element, declarations) { // List of not applied properties - because of lack of support for its name or value const notAppliedProperties = new Set(); - for (let declaration of declarationsArray) { + for (const declaration of declarationsArray) { const previousCSSText = style.cssText; // Declarations are added one by one, because browsers sometimes combine multiple declarations into one // For example Chrome changes "column-count: auto;column-width: 8rem;" into "columns: 8rem auto;" @@ -152,11 +157,11 @@ export function resetDefault() { // loop over all sections and set to hidden for (let i = 0, l = sections.length; i < l; i++) { sections[i].classList.add("hidden"); - sections[i].setAttribute("aria-hidden", true); + sections[i].setAttribute("aria-hidden", "true"); } // show the default example defaultExample.classList.remove("hidden"); - defaultExample.setAttribute("aria-hidden", false); + defaultExample.setAttribute("aria-hidden", "false"); } resetUIState(); diff --git a/editor/js/editor-libs/events.js b/editor/js/editor-libs/events.ts similarity index 93% rename from editor/js/editor-libs/events.js rename to editor/js/editor-libs/events.ts index a98dde397..42c5dc72d 100644 --- a/editor/js/editor-libs/events.js +++ b/editor/js/editor-libs/events.ts @@ -5,12 +5,13 @@ import * as cssEditorUtils from "./css-editor-utils.js"; * Adds listeners for events from the CSS live examples * @param {Object} exampleChoiceList - The object to which events are added */ -function addCSSEditorEventListeners(exampleChoiceList) { +function addCSSEditorEventListeners(exampleChoiceList: HTMLElement) { exampleChoiceList.addEventListener("cut", copyTextOnly); exampleChoiceList.addEventListener("copy", copyTextOnly); exampleChoiceList.addEventListener("keyup", (event) => { - const exampleChoiceParent = event.target.parentElement; + const target = event.target as typeof exampleChoiceList; + const exampleChoiceParent = target.parentElement; cssEditorUtils.applyCode( exampleChoiceParent.textContent, @@ -20,7 +21,9 @@ function addCSSEditorEventListeners(exampleChoiceList) { const exampleChoices = exampleChoiceList.querySelectorAll(".example-choice"); Array.from(exampleChoices).forEach((choice) => { - choice.addEventListener("click", handleChoiceEvent); + choice.addEventListener("click", (e) => + onChoose(e.currentTarget as HTMLElement) + ); }); } @@ -108,10 +111,6 @@ function copyTextOnly(event) { event.clipboardData.setData("text/html", range.toString()); } -function handleChoiceEvent() { - onChoose(this); -} - /** * Called when a new `example-choice` has been selected. * @param {Object} choice - The selected `example-choice` element diff --git a/editor/js/editor-libs/feature-detector.js b/editor/js/editor-libs/feature-detector.ts similarity index 100% rename from editor/js/editor-libs/feature-detector.js rename to editor/js/editor-libs/feature-detector.ts diff --git a/editor/js/editor-libs/mce-utils.js b/editor/js/editor-libs/mce-utils.ts similarity index 97% rename from editor/js/editor-libs/mce-utils.js rename to editor/js/editor-libs/mce-utils.ts index dd21fe983..2927c9326 100644 --- a/editor/js/editor-libs/mce-utils.js +++ b/editor/js/editor-libs/mce-utils.ts @@ -70,7 +70,7 @@ export function scrollToAnchors(contentWindow, rootElement, relativeLinks) { relativeLinks.forEach((relativeLink) => { relativeLink.addEventListener("click", (event) => { event.preventDefault(); - let element = rootElement.querySelector(relativeLink.hash); + const element = rootElement.querySelector(relativeLink.hash); if (element) { element.scrollIntoView(); contentWindow.location.hash = relativeLink.hash; diff --git a/editor/js/editor-libs/tabby.js b/editor/js/editor-libs/tabby.ts similarity index 91% rename from editor/js/editor-libs/tabby.js rename to editor/js/editor-libs/tabby.ts index 97000ec5a..4c9a35903 100644 --- a/editor/js/editor-libs/tabby.js +++ b/editor/js/editor-libs/tabby.ts @@ -12,7 +12,8 @@ const staticHTMLCode = htmlEditor.querySelector("pre"); const staticCSSCode = cssEditor.querySelector("pre"); const staticJSCode = jsEditor.querySelector("pre"); const tabContainer = document.getElementById("tab-container"); -const tabs = tabContainer.querySelectorAll('button[role="tab"]'); +const tabs = + tabContainer.querySelectorAll('button[role="tab"]'); const tabList = document.getElementById("tablist"); /** @@ -34,7 +35,7 @@ function hideTabPanels() { * @param {Object} nextActiveTab - The tab to activate * @param {Object} [activeTab] - The current active tab */ -function setActiveTab(nextActiveTab, activeTab) { +function setActiveTab(nextActiveTab, activeTab?) { if (activeTab) { // set the currentSelectedTab to false activeTab.setAttribute("aria-selected", false); @@ -54,11 +55,11 @@ function setActiveTab(nextActiveTab, activeTab) { function setDefaultTab(tab) { const panel = document.getElementById(tab.id + "-panel"); - tab.setAttribute("aria-selected", true); + tab.setAttribute("aria-selected", "true"); tab.removeAttribute("tabindex"); panel.classList.remove("hidden"); - panel.setAttribute("aria-hidden", false); + panel.setAttribute("aria-hidden", "false"); tab.focus(); } @@ -78,7 +79,7 @@ function setNextActiveTab(direction) { } if (direction === "forward") { - if (activeTab.nextElementSibling) { + if (activeTab.nextElementSibling instanceof HTMLElement) { setActiveTab(activeTab.nextElementSibling, activeTab); activeTab.nextElementSibling.click(); } else { @@ -87,7 +88,7 @@ function setNextActiveTab(direction) { tabs[0].click(); } } else if (direction === "reverse") { - if (activeTab.previousElementSibling) { + if (activeTab.previousElementSibling instanceof HTMLElement) { setActiveTab(activeTab.previousElementSibling, activeTab); activeTab.previousElementSibling.click(); } else { @@ -146,7 +147,7 @@ export function initEditor(editorTypes, defaultTab) { */ export function registerEventListeners() { tabList.addEventListener("click", (event) => { - const eventTarget = event.target; + const eventTarget = event.target as HTMLElement; const role = eventTarget.getAttribute("role"); if (role === "tab") { @@ -160,7 +161,7 @@ export function registerEventListeners() { // now show the selected tabpanel selectedPanel.classList.remove("hidden"); - selectedPanel.setAttribute("aria-hidden", false); + selectedPanel.setAttribute("aria-hidden", "false"); } }); diff --git a/editor/js/editor.js b/editor/js/editor.ts similarity index 96% rename from editor/js/editor.js rename to editor/js/editor.ts index c08be8260..2b320a174 100644 --- a/editor/js/editor.js +++ b/editor/js/editor.ts @@ -23,7 +23,9 @@ import "../css/tabbed-editor.css"; const staticCSSCode = cssEditor.querySelector("pre"); const staticHTMLCode = htmlEditor.querySelector("pre"); const staticJSCode = jsEditor.querySelector("pre"); - const outputIFrame = document.getElementById("output-iframe"); + const outputIFrame = document.getElementById( + "output-iframe" + ) as HTMLIFrameElement; const outputTemplate = getOutputTemplate(); const editorType = editorContainer.dataset.editorType; @@ -131,7 +133,8 @@ import "../css/tabbed-editor.css"; * This process is purposefully delayed, so console logs hooks are attached first */ function executeJSEditorCode() { - outputIFrame.contentWindow.executeExample(); + type JSEditorWindow = Window & { executeExample: () => void }; + (outputIFrame.contentWindow as JSEditorWindow).executeExample(); } /** @@ -187,7 +190,8 @@ import "../css/tabbed-editor.css"; outputIFrame.addEventListener("load", () => onOutputLoaded()); header.addEventListener("click", (event) => { - if (event.target.classList.contains("reset")) { + const target = event.target as typeof header; + if (target.classList.contains("reset")) { window.location.reload(); } }); diff --git a/package-lock.json b/package-lock.json index 2c741d086..b2b64ca78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,8 @@ "bundlesize": "0.18.1", "eslint": "^8.2.0", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.3", + "eslint-import-resolver-typescript": "^3.5.3", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.1.5", "http-server": "14.1.1", "husky": "^8.0.1", @@ -1483,6 +1484,32 @@ "node": ">= 8" } }, + "node_modules/@pkgr/utils": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "is-glob": "^4.0.3", + "open": "^8.4.0", + "picocolors": "^1.0.0", + "tiny-glob": "^0.2.9", + "tslib": "^2.4.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.25.23", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.23.tgz", @@ -4079,6 +4106,15 @@ "node": ">=0.10.0" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -4526,6 +4562,62 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz", + "integrity": "sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.10.0", + "get-tsconfig": "^4.2.0", + "globby": "^13.1.2", + "is-core-module": "^2.10.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-module-utils": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", @@ -5352,6 +5444,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz", + "integrity": "sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-build": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/github-build/-/github-build-1.2.3.tgz", @@ -5500,6 +5601,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -5520,6 +5627,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", @@ -6057,6 +6170,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -6275,6 +6403,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8406,6 +8546,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -10542,6 +10699,28 @@ "node": ">= 10" } }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -10729,6 +10908,16 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -12835,6 +13024,28 @@ "fastq": "^1.6.0" } }, + "@pkgr/utils": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "is-glob": "^4.0.3", + "open": "^8.4.0", + "picocolors": "^1.0.0", + "tiny-glob": "^0.2.9", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, "@sinclair/typebox": { "version": "0.25.23", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.23.tgz", @@ -14692,6 +14903,12 @@ "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", "dev": true }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -15067,6 +15284,42 @@ } } }, + "eslint-import-resolver-typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz", + "integrity": "sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.10.0", + "get-tsconfig": "^4.2.0", + "globby": "^13.1.2", + "is-core-module": "^2.10.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.4" + }, + "dependencies": { + "globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + } + } + }, "eslint-module-utils": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", @@ -15640,6 +15893,12 @@ "get-intrinsic": "^1.1.1" } }, + "get-tsconfig": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz", + "integrity": "sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==", + "dev": true + }, "github-build": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/github-build/-/github-build-1.2.3.tgz", @@ -15761,6 +16020,12 @@ "define-properties": "^1.1.3" } }, + "globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, "globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -15775,6 +16040,12 @@ "slash": "^3.0.0" } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", @@ -16155,6 +16426,12 @@ "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", "dev": true }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -16298,6 +16575,15 @@ "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", "dev": true }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -17891,6 +18177,17 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, "opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -19402,6 +19699,24 @@ } } }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -19543,6 +19858,16 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, + "tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "requires": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index cd99f7f0d..601d4aa5c 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "bundlesize": "0.18.1", "eslint": "^8.2.0", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.3", + "eslint-import-resolver-typescript": "^3.5.3", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.1.5", "http-server": "14.1.1", "husky": "^8.0.1", From db2c78969b2e5528e818fff3515cf5d005aae59e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 27 Oct 2023 11:03:46 +0200 Subject: [PATCH 02/17] Trigger checks From 682ada75b608bbf8ba3404f6fef659b3e720f67e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 27 Oct 2023 11:10:57 +0200 Subject: [PATCH 03/17] Merge 'main' into 'ts-editor-js' --- .github/workflows/codeql-analysis.yml | 2 +- .../workflows/npm-published-simulation.yml | 6 +- .github/workflows/test.yml | 6 +- .gitignore | 4 + CHANGELOG.md | 37 + __tests__/built-example.test.js | 40 + editor/css/editable-css.css | 7 +- editor/css/editable-js-and-wat.css | 8 +- .../css/editor-libs/codemirror-override.css | 10 + editor/css/editor-libs/common.css | 23 +- editor/js/editable-css.ts | 8 +- editor/js/editor-libs/codemirror-editor.ts | 12 +- editor/js/editor-libs/css-editor-utils.ts | 9 + editor/js/editor-libs/events.ts | 89 +- editor/js/editor-libs/mce-utils.ts | 29 - editor/js/editor-libs/telemetry.ts | 97 + editor/js/editor-libs/utils.ts | 23 + editor/tmpl/live-css-tmpl.html | 14 +- editor/tmpl/live-js-tmpl.html | 3 - editor/tmpl/live-tabbed-tmpl.html | 15 +- lib/config.ts | 28 +- lib/heightBuilder.ts | 40 +- lib/mdn-bob.ts | 41 +- lib/pageBuilder.ts | 148 +- lib/pageBuilderUtils.ts | 97 +- lib/processor.ts | 156 +- lib/tabbedPageBuilder.ts | 47 +- lib/utils.ts | 22 +- .../css-examples/fonts/font-stretch.css | 6 + .../css-examples/fonts/font-stretch.html | 54 + live-examples/css-examples/fonts/meta.json | 11 + live-examples/fonts/LeagueMono-VF.ttf | Bin 0 -> 124800 bytes live-examples/fonts/rapscallion.css | 4 + .../content-sectioning/css/article.css | 2 +- .../content-sectioning/meta.json | 1 + .../html-examples/table-content/meta.json | 5 +- package-lock.json | 9893 +++++++++-------- package.json | 43 +- tsconfig.json | 5 +- types/types.d.ts | 202 + webpack.config.ts | 9 +- 41 files changed, 6408 insertions(+), 4848 deletions(-) create mode 100644 __tests__/built-example.test.js create mode 100644 editor/js/editor-libs/telemetry.ts create mode 100644 editor/js/editor-libs/utils.ts create mode 100644 live-examples/css-examples/fonts/font-stretch.css create mode 100644 live-examples/css-examples/fonts/font-stretch.html create mode 100644 live-examples/css-examples/fonts/meta.json create mode 100644 live-examples/fonts/LeagueMono-VF.ttf create mode 100644 live-examples/fonts/rapscallion.css create mode 100644 types/types.d.ts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a374388f4..ab0924bc9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/npm-published-simulation.yml b/.github/workflows/npm-published-simulation.yml index 9bac7a8de..88336d5eb 100644 --- a/.github/workflows/npm-published-simulation.yml +++ b/.github/workflows/npm-published-simulation.yml @@ -15,12 +15,12 @@ jobs: steps: - name: Setup Node.js environment (interactive-examples) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Checkout mdn/interactive-examples - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: mdn/interactive-examples path: interactive-examples @@ -30,7 +30,7 @@ jobs: run: npm ci - name: Checkout this repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: bob diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6ea4ab35..8487de603 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,12 +6,14 @@ jobs: name: test-bob runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Install dependencies run: npm ci + - name: Prepack + run: npm run prepack - name: Build bob run: npm run build - name: Install developer dependencies diff --git a/.gitignore b/.gitignore index 1c0f9bd86..25fd1d63e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ typings/ # project specific /docs/ + +# Built TypeScript files +lib/*.js +webpack.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfd42714..3f2960e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## [3.4.0](https://github.com/mdn/bob/compare/v3.3.0...v3.4.0) (2023-08-02) + + +### Features + +* Add border around the warning box ([#1173](https://github.com/mdn/bob/issues/1173)) ([9d818f0](https://github.com/mdn/bob/commit/9d818f0fdc2ff3e80acff480675752a40234e2bb)) +* **css:** Allow `[@import](https://github.com/import)` in css examples ([#1129](https://github.com/mdn/bob/issues/1129)) ([4949ded](https://github.com/mdn/bob/commit/4949dedbbef8805cde287d87a9740cf0dbd99980)) +* **meta:** Allow `cssHiddenSrc` to be an array ([#1130](https://github.com/mdn/bob/issues/1130)) ([07e90b3](https://github.com/mdn/bob/commit/07e90b31c2ba464867fe008ac0f7f8b91d97bfb5)) + + +### Bug Fixes + +* Fix CTRL+X in CSS editor ([#1147](https://github.com/mdn/bob/issues/1147)) ([7b7ad44](https://github.com/mdn/bob/commit/7b7ad44a044e6ef2d5585344abb5768f77b9dae4)) +* **jest:** Fix `jest` script on windows ([#1128](https://github.com/mdn/bob/issues/1128)) ([ceb3658](https://github.com/mdn/bob/commit/ceb36581086a58a8a4f9b1b2c7f0bd1f16601ae7)) +* Refresh `invalid css` icon immediately after edit ([#1148](https://github.com/mdn/bob/issues/1148)) ([38d20ae](https://github.com/mdn/bob/commit/38d20ae8dd319622d1d637b1b9483c469cc19642)) +* remove search functionality from code editor ([#1334](https://github.com/mdn/bob/issues/1334)) ([9645008](https://github.com/mdn/bob/commit/96450082ff964674ddd485ed773d39fa45a85f43)) +* **style:** Fix horizontal scrollbar of js editor ([#1137](https://github.com/mdn/bob/issues/1137)) ([229487a](https://github.com/mdn/bob/commit/229487a88127e6aac6a383043ce6579058ae6e4f)) +* **style:** Make font-family of code match ordinary MDN examples ([742a5b5](https://github.com/mdn/bob/commit/742a5b599912062f28dbc3d919a264d2bab05c17)) +* **style:** Make font-family of code, match ordinary MDN examples ([#1133](https://github.com/mdn/bob/issues/1133)) ([742a5b5](https://github.com/mdn/bob/commit/742a5b599912062f28dbc3d919a264d2bab05c17)) +* **style:** Make scrollbar colors match MDN ([#1132](https://github.com/mdn/bob/issues/1132)) ([29b044d](https://github.com/mdn/bob/commit/29b044db9db451b8b8d6fe45727a1a4ac6a857d8)) +* **style:** Use better text selection color ([#1131](https://github.com/mdn/bob/issues/1131)) ([f92927f](https://github.com/mdn/bob/commit/f92927f3323698040926e2cf69777cd96379d5cd)) +* Widen CSS example text area ([#1149](https://github.com/mdn/bob/issues/1149)) ([bba4a3a](https://github.com/mdn/bob/commit/bba4a3a6e4e845e2b40ba54ef61ad3be82500eb4)) + +## [3.3.0](https://github.com/mdn/bob/compare/v3.2.0...v3.3.0) (2023-05-04) + + +### Features + +* **editor-libs:** extend the feature detection for CSS functions ([#1270](https://github.com/mdn/bob/issues/1270)) ([5de231c](https://github.com/mdn/bob/commit/5de231cf1dca5b1687e9bb4c704519f5c92b638a)) + +## [3.2.0](https://github.com/mdn/bob/compare/v3.1.0...v3.2.0) (2023-03-13) + + +### Features + +* **telemetry:** record interactions + post to parent ([#1145](https://github.com/mdn/bob/issues/1145)) ([a56cf8e](https://github.com/mdn/bob/commit/a56cf8e7dd6c410eda7339189f20c7428ec33175)) + ## [3.1.0](https://github.com/mdn/bob/compare/v3.0.3...v3.1.0) (2023-03-09) diff --git a/__tests__/built-example.test.js b/__tests__/built-example.test.js new file mode 100644 index 000000000..70568eb28 --- /dev/null +++ b/__tests__/built-example.test.js @@ -0,0 +1,40 @@ +import fse from "fs-extra"; +import getConfig from "../lib/config.js"; + +const config = getConfig(); + +describe("example", () => { + describe("font-stretch", () => { + it("should have content of @import directly in css", () => { + const path = "live-examples/css-examples/fonts/font-stretch.css"; + const content = fse.readFileSync(config.baseDir + path, "utf8"); + + const expectedOutput = + '@font-face{font-family:molot;src:url("/media/fonts/molot.woff2") format("woff2")}#output section{font-size:1.2em;font-family:molot,sans-serif}'; + + expect(content).toBe(expectedOutput); + }); + }); + describe("article", () => { + it("should have content of `cssHiddenSrc` path", () => { + const path = "pages/tabbed/article.html"; + const content = fse.readFileSync(config.baseDir + path, "utf8"); + + const expectedOutput = + '@font-face{font-family:molot;src:url("/media/fonts/molot.woff2") format("woff2")}'; + + expect(content).toContain(expectedOutput); + }); + }); + describe("caption", () => { + it("should have content of both `cssHiddenSrc` paths", () => { + const path = "pages/tabbed/caption.html"; + const content = fse.readFileSync(config.baseDir + path, "utf8"); + + const expectedOutput = + '@font-face{font-family:molot;src:url("/media/fonts/molot.woff2") format("woff2")}@font-face{font-family:rapscallion;src:url("/media/fonts/rapscall.woff2") format("woff2")}'; + + expect(content).toContain(expectedOutput); + }); + }); +}); diff --git a/editor/css/editable-css.css b/editor/css/editable-css.css index fd6e0ee64..4a9ead900 100644 --- a/editor/css/editable-css.css +++ b/editor/css/editable-css.css @@ -61,7 +61,7 @@ position: relative; z-index: 1; display: block; - padding: 0 1rem; + padding: 0 12px; margin: 0.2em 0; width: 100%; border: 1px solid var(--border-primary); @@ -83,7 +83,6 @@ height: auto; text-align: left; padding: 4px 0; - padding-right: 1rem; background: none; border: none; font-size: 0.875rem; @@ -93,6 +92,10 @@ outline: none; } +.cm-editor .cm-line { + padding: 0; +} + .example-choice-list .example-choice:first-child { margin-top: 0; } diff --git a/editor/css/editable-js-and-wat.css b/editor/css/editable-js-and-wat.css index ec456d8d3..516bcf56c 100644 --- a/editor/css/editable-js-and-wat.css +++ b/editor/css/editable-js-and-wat.css @@ -119,9 +119,15 @@ flex-direction: row; } + .output { + width: calc( + 100% - 108px + ); /* 108px is 100px of a .button width + 8px of a .buttons-container padding-right */ + } + .buttons-container { flex-direction: column; width: auto; - padding-right: 0.5rem; + padding-right: 8px; } } diff --git a/editor/css/editor-libs/codemirror-override.css b/editor/css/editor-libs/codemirror-override.css index cefe332e1..9d8f475c5 100644 --- a/editor/css/editor-libs/codemirror-override.css +++ b/editor/css/editor-libs/codemirror-override.css @@ -19,12 +19,22 @@ .cm-editor .cm-scroller { line-height: inherit; + font-family: var(--font-code); } .cm-editor .cm-cursor { border-color: var(--text-primary); } +.cm-editor .cm-line::selection, +.cm-editor .cm-line ::selection { + background-color: var(--selection-background-color) !important; +} +.cm-editor .cm-selectionBackground, +.cm-editor.cm-focused .cm-selectionBackground { + background-color: initial; +} + /* Language specific rules */ .cm-operator, .cm-variable { diff --git a/editor/css/editor-libs/common.css b/editor/css/editor-libs/common.css index 230d71af5..d02716602 100644 --- a/editor/css/editor-libs/common.css +++ b/editor/css/editor-libs/common.css @@ -2,7 +2,8 @@ --font-fallback: BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-sans; --font-body: Inter, var(--font-fallback); - --font-code: Consolas, "Courier New", monospace; + --font-code: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, + monospace; --type-body-l: 400 1rem/1.1876 var(--font-body); /* 16px/19px */ --type-emphasis-m: 600 0.8125rem/1.23 var(--font-body); /* 13px/16px */ --font-heading: var(--font-body); @@ -108,6 +109,7 @@ --http-accent-background-color: #009a4630; --apis-accent-background-color: #9b65ff30; --learn-accent-background-color: #ff1f7230; + --selection-background-color: #d3e7ff; --plus-accent-engage: rgba(255, 42, 81, 0.7); --html-accent-engage: rgba(255, 42, 81, 0.7); --css-accent-engage: rgba(0, 133, 242, 0.7); @@ -132,6 +134,7 @@ --text-primary-green: #007936; --text-primary-blue: #0069c2; --text-primary-yellow: #746a00; + color-scheme: light; } .theme-dark { @@ -170,7 +173,7 @@ --field-focus-border: #fff; --code-token-tag: #c1cff1; --code-token-punctuation: #9e9e9e; - --code-token-attribute-name: #ff97a0; + --code-token-attribute-name: #ffbb88; --code-token-attribute-value: #00d061; --code-token-comment: #9e9e9e; --code-token-default: #fff; @@ -217,11 +220,13 @@ --apis-accent-engage: rgba(174, 138, 255, 0.7); --learn-accent-engage: rgba(255, 109, 145, 0.7); --modal-backdrop-color: rgba(27, 27, 27, 0.7); + --selection-background-color: #233244; --blend-color: #00080; --text-primary-red: #ff97a0; --text-primary-green: #00d061; --text-primary-blue: #8cb4ff; --text-primary-yellow: #c7b700; + color-scheme: dark; } @media (prefers-color-scheme: light) { @@ -317,6 +322,7 @@ --http-accent-background-color: #009a4630; --apis-accent-background-color: #9b65ff30; --learn-accent-background-color: #ff1f7230; + --selection-background-color: #d3e7ff; --plus-accent-engage: rgba(255, 42, 81, 0.7); --html-accent-engage: rgba(255, 42, 81, 0.7); --css-accent-engage: rgba(0, 133, 242, 0.7); @@ -341,6 +347,7 @@ --text-primary-green: #007936; --text-primary-blue: #0069c2; --text-primary-yellow: #746a00; + color-scheme: light; } } @@ -382,7 +389,7 @@ --field-focus-border: #fff; --code-token-tag: #c1cff1; --code-token-punctuation: #9e9e9e; - --code-token-attribute-name: #ff97a0; + --code-token-attribute-name: #ffbb88; --code-token-attribute-value: #00d061; --code-token-comment: #9e9e9e; --code-token-default: #fff; @@ -434,6 +441,8 @@ --text-primary-green: #00d061; --text-primary-blue: #8cb4ff; --text-primary-yellow: #c7b700; + --selection-background-color: #233244; + color-scheme: dark; } } @@ -520,10 +529,16 @@ body { z-index: 4; } +.warning-container { + border: 1px solid var(--border-primary); + border-top: 0; + border-radius: 0 0 var(--button-radius) var(--button-radius); + padding: 16px; +} + .warning { position: relative; padding: 1rem 1rem 1rem 3rem; - margin: 1rem; background-color: var(--background-warning); border-radius: var(--elem-radius); border: 1px solid var(--border-secondary); diff --git a/editor/js/editable-css.ts b/editor/js/editable-css.ts index 6ff5302f2..4e1d13cd6 100644 --- a/editor/js/editable-css.ts +++ b/editor/js/editable-css.ts @@ -1,6 +1,5 @@ import * as clippy from "./editor-libs/clippy.js"; import * as mceEvents from "./editor-libs/events.js"; -import * as mceUtils from "./editor-libs/mce-utils.js"; import * as cssEditorUtils from "./editor-libs/css-editor-utils.js"; import { initCodeEditor, @@ -14,6 +13,10 @@ import "../css/editable-css.css"; (function () { const exampleChoiceList = document.getElementById("example-choice-list"); const exampleChoices = exampleChoiceList.querySelectorAll(".example-choice"); + const exampleDeclarations = Array.from( + exampleChoices, + (choice) => choice.querySelector("code").textContent + ); const editorWrapper = document.getElementById("editor-wrapper"); const output = document.getElementById("output"); const warningNoSupport = document.getElementById("warning-no-support"); @@ -64,6 +67,7 @@ import "../css/editable-css.css"; } mceEvents.register(); + mceEvents.addCSSEditorEventListeners(exampleChoiceList); handleResetEvents(); handleChoiceHover(); // Adding or removing class "invalid" @@ -138,7 +142,7 @@ import "../css/editable-css.css"; is a non standard object available only in IE10 and older, this will stop JS from executing in those versions. */ if ( - mceUtils.isPropertySupported(exampleChoiceList.dataset) && + cssEditorUtils.isAnyDeclarationSetSupported(exampleDeclarations) && !document.all ) { enableLiveEditor(); diff --git a/editor/js/editor-libs/codemirror-editor.ts b/editor/js/editor-libs/codemirror-editor.ts index 797ff9246..b547426a4 100644 --- a/editor/js/editor-libs/codemirror-editor.ts +++ b/editor/js/editor-libs/codemirror-editor.ts @@ -11,7 +11,6 @@ import { LRLanguage, } from "@codemirror/language"; import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete"; -import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; import { lintKeymap } from "@codemirror/lint"; import { javascript, javascriptLanguage } from "@codemirror/lang-javascript"; import { wast } from "@codemirror/lang-wast"; @@ -23,6 +22,8 @@ import { parser as jsParser } from "@lezer/javascript"; import { parser as htmlParser } from "@lezer/html"; import { parser as cssParser } from "@lezer/css"; +import { recordAction } from "./telemetry"; + import "../../css/editor-libs/codemirror-override.css"; /** @@ -178,6 +179,12 @@ function mixedHTML() { return LRLanguage.define({ parser: mixedHTMLParser }); } +function eventHandlers() { + return EditorView.domEventHandlers({ + input: () => recordAction("input", true), + }); +} + /** * Set of basic extensions, that every editor should have. * This list is mostly based on [basic setup](https://github.com/codemirror/basic-setup) @@ -193,16 +200,15 @@ const BASE_EXTENSIONS = [ closeBrackets(), view.rectangularSelection(), view.crosshairCursor(), - highlightSelectionMatches(), view.keymap.of([ ...closeBracketsKeymap, ...commands.defaultKeymap, - ...searchKeymap, ...commands.historyKeymap, ...foldKeymap, ...lintKeymap, TAB_KEY_MAP, ]), + eventHandlers(), ]; /** diff --git a/editor/js/editor-libs/css-editor-utils.ts b/editor/js/editor-libs/css-editor-utils.ts index ca19b5d68..78581caf5 100644 --- a/editor/js/editor-libs/css-editor-utils.ts +++ b/editor/js/editor-libs/css-editor-utils.ts @@ -42,6 +42,15 @@ export function applyCode( } } +/** + * Creates a temporary element and tests whether any of the provided CSS sets of declarations are fully supported by the user's browser + * @param {Array} declarationSets - Array in which every element is one or multiple declarations separated by semicolons + */ +export function isAnyDeclarationSetSupported(declarationSets) { + const tmpElem = document.createElement("div"); + return declarationSets.some(isCodeSupported.bind(null, tmpElem)); +} + /** * Checks if every passed declaration is supported by the browser. * In case browser recognizes property with vendor prefix(like -webkit-), lacking support for unprefixed property is ignored. diff --git a/editor/js/editor-libs/events.ts b/editor/js/editor-libs/events.ts index 42c5dc72d..aadce55a7 100644 --- a/editor/js/editor-libs/events.ts +++ b/editor/js/editor-libs/events.ts @@ -1,22 +1,23 @@ import * as clippy from "./clippy.js"; import * as cssEditorUtils from "./css-editor-utils.js"; +import { initTelemetry } from "./telemetry"; +import { getStorageItem, storeItem } from "./utils"; /** * Adds listeners for events from the CSS live examples - * @param {Object} exampleChoiceList - The object to which events are added + * @param exampleChoiceList - The object to which events are added */ -function addCSSEditorEventListeners(exampleChoiceList: HTMLElement) { - exampleChoiceList.addEventListener("cut", copyTextOnly); - exampleChoiceList.addEventListener("copy", copyTextOnly); - +export function addCSSEditorEventListeners(exampleChoiceList: HTMLElement) { exampleChoiceList.addEventListener("keyup", (event) => { const target = event.target as typeof exampleChoiceList; const exampleChoiceParent = target.parentElement; - cssEditorUtils.applyCode( - exampleChoiceParent.textContent, - exampleChoiceParent.closest(".cm-scroller") - ); + if (exampleChoiceParent) { + cssEditorUtils.applyCode( + exampleChoiceParent.textContent, + exampleChoiceParent.closest(".example-choice") + ); + } }); const exampleChoices = exampleChoiceList.querySelectorAll(".example-choice"); @@ -40,6 +41,9 @@ function addPostMessageListener() { // to be to allow for future themes to be added without a change here. if (event.data.theme !== undefined) { const body = document.querySelector("body"); + if (!body) { + return; + } for (let i = body.classList.length - 1; i >= 0; i--) { const className = body.classList[i]; if (className.startsWith("theme-")) { @@ -57,65 +61,29 @@ function addPostMessageListener() { document.addEventListener("DOMContentLoaded", () => { const theme = getStorageItem("theme"); if (theme !== null) { - document.querySelector("body").classList.add("theme-" + theme); + const body = document.querySelector("body"); + if (body) { + body.classList.add("theme-" + theme); + } } }); -/** - * Adds key & value to {@link localStorage}, without throwing an exception when it is unavailable - */ -function storeItem(key, value) { - try { - localStorage.setItem(key, value); - } catch (err) { - console.warn(`Unable to write ${key} to localStorage`, err); - } -} - -/** - * @returns the value of a given key from {@link localStorage}, or null when the key wasn't found. - * It doesn't throw an exception when {@link localStorage} is unavailable - */ -function getStorageItem(key) { - try { - return localStorage.getItem(key); - } catch (err) { - console.warn(`Unable to read ${key} from localStorage`, err); - return null; - } -} - function sendOwnHeight() { - if (parent) { - parent.postMessage( - { url: window.location.href, height: document.body.scrollHeight }, - "*" - ); - } + postParentMessage("height", { height: document.body.scrollHeight }); } -/** - * Ensure that only the text portion of a copy event is stored in the - * clipboard, by setting both 'text/plain', and 'text/html' to the same - * plain text value. - * @param {Object} event - The copy event - */ -function copyTextOnly(event) { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - event.preventDefault(); - event.stopPropagation(); - - event.clipboardData.setData("text/plain", range.toString()); - event.clipboardData.setData("text/html", range.toString()); +export function postParentMessage( + type: string, + values: Record +) { + parent?.postMessage({ type, url: window.location.href, ...values }, "*"); } /** * Called when a new `example-choice` has been selected. - * @param {Object} choice - The selected `example-choice` element + * @param choice - The selected `example-choice` element */ -export function onChoose(choice) { +export function onChoose(choice: HTMLElement) { const selected = document.querySelector(".selected"); // highlght the code we are leaving @@ -132,8 +100,6 @@ export function onChoose(choice) { * has been completed. */ export function register() { - const exampleChoiceList = document.getElementById("example-choice-list"); - addPostMessageListener(); if (document.readyState !== "loading") { @@ -142,8 +108,5 @@ export function register() { document.addEventListener("DOMContentLoaded", sendOwnHeight); } - // only bind events if the `exampleChoiceList` container exist - if (exampleChoiceList) { - addCSSEditorEventListeners(exampleChoiceList); - } + initTelemetry(); } diff --git a/editor/js/editor-libs/mce-utils.ts b/editor/js/editor-libs/mce-utils.ts index 2927c9326..a7cdb5d9e 100644 --- a/editor/js/editor-libs/mce-utils.ts +++ b/editor/js/editor-libs/mce-utils.ts @@ -16,35 +16,6 @@ export function findParentChoiceElem(element) { } return parent; } - -/** - * Creates a temporary element and tests whether the passed - * property exists on the `style` property of the element. - * @param {Object} dataset = The dataset from which to get the property - */ -export function isPropertySupported(dataset) { - /* If there are no 'property' attributes, - there is nothing to test, so return true. */ - if (dataset["property"] === undefined) { - return true; - } - - // `property` may be a space-separated list of properties. - const properties = dataset["property"].split(" "); - /* Iterate through properties: if any of them apply, - the browser supports this example. */ - const tmpElem = document.createElement("div"); - let supported = false; - - for (let i = 0, l = properties.length; i < l; i++) { - if (tmpElem.style[properties[i]] !== undefined) { - supported = true; - } - } - - return supported; -} - /** * Interrupts the default click event on external links inside * the iframe and opens them in a new tab instead diff --git a/editor/js/editor-libs/telemetry.ts b/editor/js/editor-libs/telemetry.ts new file mode 100644 index 000000000..738bdbe72 --- /dev/null +++ b/editor/js/editor-libs/telemetry.ts @@ -0,0 +1,97 @@ +import { postParentMessage } from "./events.js"; +import { getStorageItem, storeItem } from "./utils"; + +const ACTION_COUNTS_KEY = "action-counts"; + +type ActionCounts = Record; + +/** + * Reads action counts from local storage. + * Ignores action counts that belong to another example. + * + * @returns The action counts from the previous session, if available. + */ +function getActionsCounts() { + const json = getStorageItem(ACTION_COUNTS_KEY); + + if (!json) { + return {}; + } + + const value = JSON.parse(json); + if (value && value.href === window.location.href && value.counts) { + return value.counts; + } + + return {}; +} + +/** + * Writes action counts to local storage, to persist them between reloads. + * + * @param {object} counts - The current action counts. + */ +function storeActionCounts(counts: ActionCounts) { + storeItem( + ACTION_COUNTS_KEY, + JSON.stringify({ + href: window.location.href, + counts, + }) + ); +} + +/** + * Action counts by key. + * Used to distinguish 1st/2nd/etc occurrences of the same event. + */ +let actionCounts: ActionCounts = {}; + +/** + * Last observed action. + * Used to ignore multiple input actions until another action happened. + */ +let lastAction: string | null = null; + +/** + * Records an action, by counting it and forwarding it to the window parent. + * + * @param {string} key - Distinct name of the type of action. + * @param {boolean} deduplicate - Should multiple actions of the same type be ignored until another action occurred? + */ +export function recordAction(key: string, deduplicate = false) { + if (deduplicate && key === lastAction) { + return; + } else { + lastAction = key; + } + + actionCounts[key] = (actionCounts[key] ?? 0) + 1; + storeActionCounts(actionCounts); + + const source = `${key} -> ${actionCounts[key]}`; + postParentMessage("action", { source }); +} + +export function initTelemetry() { + actionCounts = getActionsCounts(); + lastAction = null; + + document.addEventListener("DOMContentLoaded", () => { + // User focuses the iframe. + window.addEventListener("focus", () => recordAction("focus")); + + // User copies / cuts / paste text in the iframe. + window.addEventListener("copy", () => recordAction("copy")); + window.addEventListener("cut", () => recordAction("cut")); + window.addEventListener("paste", () => recordAction("paste")); + + // User clicks on any element with an id. + window.addEventListener("click", (event) => { + const id = (event.target as HTMLElement | null)?.id; + if (id) { + recordAction(`click@${id}`); + } + }); + }); +} diff --git a/editor/js/editor-libs/utils.ts b/editor/js/editor-libs/utils.ts new file mode 100644 index 000000000..36876c23e --- /dev/null +++ b/editor/js/editor-libs/utils.ts @@ -0,0 +1,23 @@ +/** + * Adds key & value to {@link localStorage}, without throwing an exception when it is unavailable + */ +export function storeItem(key, value) { + try { + localStorage.setItem(key, value); + } catch (err) { + console.warn(`Unable to write ${key} to localStorage`, err); + } +} + +/** + * @returns the value of a given key from {@link localStorage}, or null when the key wasn't found. + * It doesn't throw an exception when {@link localStorage} is unavailable + */ +export function getStorageItem(key) { + try { + return localStorage.getItem(key); + } catch (err) { + console.warn(`Unable to read ${key} from localStorage`, err); + return null; + } +} diff --git a/editor/tmpl/live-css-tmpl.html b/editor/tmpl/live-css-tmpl.html index ecfab26d0..35e97ecc9 100644 --- a/editor/tmpl/live-css-tmpl.html +++ b/editor/tmpl/live-css-tmpl.html @@ -17,13 +17,17 @@

%title%