import { CustomElement } from "./custom-element.js"; import splitCodeSections from "./lib/split-code-sections.js"; import performCodeSurgery from "./lib/perform-code-surgery.js"; const MODULE_URL = import.meta.url; const MODULE_PATH = MODULE_URL.slice(0, MODULE_URL.lastIndexOf(`/`)); // Until global `await` gets added to JS, we need to declare this "constant" // using the `let` keyword instead, and then boostrap its value during the // `loadSource` call (using the standard if(undefined){assignvalue} pattern). let IMPORT_GLOBALS_FROM_GRAPHICS_API = undefined; // Really wish this was baked into the DOM API. Having to use an // IntersectionObserver with bounding box fallback is super dumb // from an authoring perspective. function isInViewport(e) { if (typeof window === `undefined`) return true; if (typeof document === `undefined`) return true; var b = e.getBoundingClientRect(); return ( b.top >= 0 && b.left >= 0 && b.bottom <= (window.innerHeight || document.documentElement.clientHeight) && b.right <= (window.innerWidth || document.documentElement.clientWidth) ); } /** * A simple "for programming code" element, for holding entire * programs, rather than code snippets. */ CustomElement.register(class ProgramCode extends HTMLElement {}); /** * Our custom element */ class GraphicsElement extends CustomElement { /** * Create an instance of this element */ constructor() { super({ header: false, footer: false }); this.originalHTML = this.outerHTML; // Do we load immediately? if (isInViewport(this)) { this.loadSource(); } // Or do we load later, once we've been scrolled into view? else { let fallback = this.querySelector(`img`); new IntersectionObserver( (entries, observer) => entries.forEach((entry) => { if (entry.isIntersecting) { this.loadSource(); observer.disconnect(); } }), { threshold: 0.1, rootMargin: `${window.innerHeight}px` } ).observe(fallback); } this.label = document.createElement(`label`); if (!this.title) this.title = ``; this.label.textContent = this.title; } /** * part of the CustomElement API */ getStyle() { return ` :host([hidden]) { display: none; } :host { max-width: calc(2em + ${this.getAttribute(`width`)}px); } :host style { display: none; } :host .top-title { display: flex; flex-direction: row-reverse; justify-content: space-between; } :host canvas { position: relative; z-index: 1; display: block; margin: auto; border-radius: 0; box-sizing: content-box!important; border: 1px solid lightgrey; } :host canvas:focus { border: 1px solid red; } :host a.view-source { font-size: 60%; text-decoration: none; } :host button.reset { font-size: 0.5em; } :host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; } `; } /** * part of the CustomElement API */ handleChildChanges(added, removed) { // debugLog(`child change:`, added, removed); } /** * part of the CustomElement API */ handleAttributeChange(name, oldValue, newValue) { if (name === `title`) { this.label.textContent = this.getAttribute(`title`); } if (this.apiInstance) { let instance = this.apiInstance; if (name === `width`) { instance.setSize(parseInt(newValue), false); instance.redraw(); } if (name === `height`) { instance.setSize(false, parseInt(newValue)); instance.redraw(); } } } /** * Load the graphics code, either from a src URL, a element, or .textContent */ async loadSource() { debugLog(`loading ${this.getAttribute(`src`)}`); if (!IMPORT_GLOBALS_FROM_GRAPHICS_API) { const importStatement = (await fetch(`${MODULE_PATH}/api/graphics-api.js`).then((r) => r.text())) .match(/(export { [^}]+ })/)[0] .replace(`export`, `import`); IMPORT_GLOBALS_FROM_GRAPHICS_API = `${importStatement} from "${MODULE_PATH}/api/graphics-api.js"`; } let src = false; let codeElement = this.querySelector(`program-code`); let code = ``; if (codeElement) { src = codeElement.getAttribute("src"); if (src) { this.src = src; code = await fetch(src).then((response) => response.text()); } else { code = codeElement.textContent; } } else { src = this.getAttribute("src"); if (src) { this.src = src; code = await fetch(src).then((response) => response.text()); } else { code = this.textContent; } } if (!codeElement) { codeElement = document.createElement(`program-code`); codeElement.textContent = code; this.prepend(codeElement); } codeElement.setAttribute(`hidden`, `hidden`); new MutationObserver((_records) => { // nornmally we don't want to completely recreate the shadow DOM this.processSource(src, codeElement.textContent); }).observe(codeElement, { characterData: true, attributes: false, childList: true, subtree: true, }); // But on the first pass, we do. this.processSource(src, code, true); } /** * Transform the graphics source code into global and class code. */ processSource(src, code, rerender = false) { if (this.script) { if (this.script.parentNode) { this.script.parentNode.removeChild(this.script); } this.canvas.parentNode.removeChild(this.canvas); rerender = true; } const uid = (this.uid = `bg-uid-${Date.now()}-${`${Math.random()}`.replace(`0.`, ``)}`); window[uid] = this; // Step 1: fix the imports. This is ... a bit of work. let path; let base = document.querySelector(`base`); if (base) { path = base.href; } else { let loc = window.location.toString(); path = loc.substring(0, loc.lastIndexOf(`/`) + 1); } let modulepath = `${path}${src}`; let modulebase = modulepath.substring(0, modulepath.lastIndexOf(`/`) + 1); // okay, I lied, it's actually quite a lot of work. code = code.replace(/(import .+? from) "([^"]+)"/g, (_, main, group) => { return `${main} "${modulebase}${group}"`; }); this.linkableCode = code; // Then, step 2: split up the code into "global" vs. "class" code. const split = splitCodeSections(code); const globalCode = split.quasiGlobal; const classCode = performCodeSurgery(split.classCode); this.setupCodeInjection(src, uid, globalCode, classCode, rerender); } /** * Form the final, perfectly valid JS module code, and create the