diff --git a/src/ui/DOMUtil.js b/src/ui/DOMUtil.js index 7c1081c..62a7a1c 100644 --- a/src/ui/DOMUtil.js +++ b/src/ui/DOMUtil.js @@ -37,6 +37,54 @@ export function html(parts, ...values) { return fragment } +/** + * @template T + * @typedef {HTMLElement & {state: T}} Elem + */ + +/** + * @template State + * @template {unknown[]} ArgType + * @param {string} tag + * @param {(elem: HTMLElement, ...args: ArgType) => State} createFn + * @param {{ + * connected?: (elem: Elem) => void + * disconnected?: (elem: Elem) => void + * }} callbacks + */ +export function define(tag, createFn, {connected, disconnected} = {}) { + let constructor = /** @type {{ new (...args: ArgType): Elem }} */( + customElements.get(tag) + ) + + if (!constructor) { + constructor = class extends HTMLElement { + /** + * @param {ArgType} args + */ + constructor(...args) { + super() + this.state = createFn(this, ...args) + } + + connectedCallback() { + if (connected) { + connected(this) + } + } + + disconnectedCallback() { + if (disconnected) { + disconnected(this) + } + } + } + customElements.define(tag, constructor) + } + + return constructor +} + /** * @template {keyof HTMLElementTagNameMap} K * @param {K} tagName diff --git a/src/ui/ElementInterfaces.d.ts b/src/ui/ElementInterfaces.d.ts index 59ae412..b0d3871 100644 --- a/src/ui/ElementInterfaces.d.ts +++ b/src/ui/ElementInterfaces.d.ts @@ -1,8 +1,8 @@ // Interfaces between elements +import * as $fileToolbar from './FileToolbar.js' import {Cell, CellPart, Module} from '../Model.js' import {CellEntryElement} from './CellEntry.js' -import {FileToolbarElement} from './FileToolbar.js' import {ModulePropertiesElement} from './ModuleProperties.js' import {PatternEditElement} from './PatternEdit.js' import {PatternTableElement} from './PatternTable.js' @@ -58,7 +58,7 @@ interface PlaybackControlsTarget { // Element type extensions interface HTMLElementTagNameMap { 'cell-entry': CellEntryElement - 'file-toolbar': FileToolbarElement + 'file-toolbar': $fileToolbar.Elem 'module-properties': ModulePropertiesElement 'pattern-edit': PatternEditElement 'pattern-table': PatternTableElement diff --git a/src/ui/FileToolbar.js b/src/ui/FileToolbar.js index adcc0ab..e625798 100644 --- a/src/ui/FileToolbar.js +++ b/src/ui/FileToolbar.js @@ -27,63 +27,79 @@ const template = $dom.html` ` -export class FileToolbarElement extends HTMLElement { - constructor() { - super() +/** @typedef {ReturnType} State */ +/** @typedef {$dom.Elem} Elem */ + +function create() { + return { /** @type {FileToolbarTarget} */ - this._target = null + target: null } +} - connectedCallback() { - let fragment = template.cloneNode(true) +/** + * @param {Elem} e + * @param {FileToolbarTarget} target + */ +export function setTarget(e, target) { + e.state.target = target +} - fragment.querySelector('#newModule').addEventListener('click', - () => this._target._moduleLoaded($module.defaultNew)) +/** + * @param {Elem} e + */ +function connected(e) { + let fragment = template.cloneNode(true) - /** @type {HTMLInputElement} */ - let fileSelect = fragment.querySelector('#fileSelect') - fileSelect.addEventListener('change', () => { - if (fileSelect.files.length == 1) { - this._readModuleBlob(fileSelect.files[0]) - } - }) - $dom.addMenuListener(fragment.querySelector('#demoMenu'), value => { - let dialog = $dialog.open(new WaitDialogElement()) - window.fetch(value) - .then(r => r.blob()) - .then(b => this._readModuleBlob(b)) - .then(() => $dialog.close(dialog)) - .catch(/** @param {Error} error */ error => { - $dialog.close(dialog) - AlertDialogElement.open(error.message) - }) - }) - fragment.querySelector('#fileSave').addEventListener('click', () => this._saveFile()) + fragment.querySelector('#newModule').addEventListener('click', + () => e.state.target._moduleLoaded($module.defaultNew)) - this.style.display = 'contents' - this.appendChild(fragment) - } + /** @type {HTMLInputElement} */ + let fileSelect = fragment.querySelector('#fileSelect') + fileSelect.addEventListener('change', () => { + if (fileSelect.files.length == 1) { + readModuleBlob(e, fileSelect.files[0]) + } + }) + $dom.addMenuListener(fragment.querySelector('#demoMenu'), value => { + let dialog = $dialog.open(new WaitDialogElement()) + window.fetch(value) + .then(r => r.blob()) + .then(b => readModuleBlob(e, b)) + .then(() => $dialog.close(dialog)) + .catch(/** @param {Error} error */ error => { + $dialog.close(dialog) + AlertDialogElement.open(error.message) + }) + }) + fragment.querySelector('#fileSave').addEventListener('click', () => saveFile(e)) - /** - * @private - * @param {Blob} blob - */ - _readModuleBlob(blob) { - let reader = new FileReader() - reader.onload = () => { - if (reader.result instanceof ArrayBuffer) { - let module = Object.freeze($mod.read(reader.result)) - this._target._moduleLoaded(module) - } + e.style.display = 'contents' + e.appendChild(fragment) +} + +/** + * @param {Elem} e + * @param {Blob} blob + */ +function readModuleBlob(e, blob) { + let reader = new FileReader() + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) { + let module = Object.freeze($mod.read(reader.result)) + e.state.target._moduleLoaded(module) } - reader.readAsArrayBuffer(blob) } + reader.readAsArrayBuffer(blob) +} - /** @private */ - _saveFile() { - let blob = new Blob([$mod.write(this._target._module)], {type: 'application/octet-stream'}) - $ext.download(blob, (this._target._module.name || 'Untitled') + '.mod') - this._target._moduleSaved() - } +/** + * @param {Elem} e + */ +function saveFile(e) { + let blob = new Blob([$mod.write(e.state.target._module)], {type: 'application/octet-stream'}) + $ext.download(blob, (e.state.target._module.name || 'Untitled') + '.mod') + e.state.target._moduleSaved() } -window.customElements.define('file-toolbar', FileToolbarElement) + +export const Elem = $dom.define('file-toolbar', create, {connected}) diff --git a/src/ui/TrackerMain.js b/src/ui/TrackerMain.js index 028cfce..e45012b 100644 --- a/src/ui/TrackerMain.js +++ b/src/ui/TrackerMain.js @@ -3,11 +3,11 @@ import * as $dialog from './Dialog.js' import * as $dom from './DOMUtil.js' import * as $play from '../Playback.js' import * as $module from '../edit/Module.js' +import * as $fileToolbar from './FileToolbar.js' import {CLIDialogElement} from './dialogs/CLIDialog.js' import {ConfirmDialogElement} from './dialogs/UtilDialogs.js' import {Cell, Module} from '../Model.js' import appVersion from '../gen/Version.js' -import './FileToolbar.js' import './ModuleProperties.js' import './PatternEdit.js' import './PlaybackControls.js' @@ -150,7 +150,7 @@ export class TrackerMainElement extends HTMLElement { this.style.display = 'contents' this.appendChild(fragment) - this._fileToolbar._target = this + $fileToolbar.setTarget(this._fileToolbar, this) this._moduleProperties._target = this this._playbackControls._target = this this._patternEdit._setTarget(this)