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(`/`)); /** * 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 { constructor() { super({ header: false, footer: false, focus: true }); this.loadSource(); if (this.title) { this.label = document.createElement(`label`); this.label.textContent = this.title; } } /** * part of the CustomElement API */ getStyle() { // TODO: document the "focus-css" attribute return ` :host([hidden]) { display: none; } :host style { display: none; } :host canvas { display: block; margin: auto; border-radius: 0; } :host canvas:focus { ${ this.getAttribute(`focus-css`) || `border: 1px solid red !important;` } } :host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; } `; } /** * part of the CustomElement API */ handleChildChanges(added, removed) { // console.log(`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() { let codeElement = this.querySelector(`program-code`); let code = ``; if (codeElement) { let src = codeElement.getAttribute("src"); if (src) { code = await fetch(src).then((response) => response.text()); } else { code = codeElement.textContent; } } else { let src = this.getAttribute("src"); if (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(codeElement.textContent); }).observe(codeElement, { characterData: true, attributes: false, childList: true, subtree: true, }); // But on the first pass, we do. this.processSource(code, true); } /** * Transform the graphics source code into global and class code. */ processSource(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 = `bg-uid-${Date.now()}-${`${Math.random()}`.replace(`0.`, ``)}`; window[uid] = this; const split = splitCodeSections(code); const globalCode = split.quasiGlobal; const classCode = performCodeSurgery(split.classCode); this.setupCodeInjection(uid, globalCode, classCode, rerender); } /** * Form the final, perfectly valid JS module code, and create the