11cb0ef41Sopenharmony_ci// Copyright 2021 the V8 project authors. All rights reserved. 21cb0ef41Sopenharmony_ci// Use of this source code is governed by a BSD-style license that can be 31cb0ef41Sopenharmony_ci// found in the LICENSE file. 41cb0ef41Sopenharmony_ci 51cb0ef41Sopenharmony_ciimport {delay, formatBytes} from './helper.mjs'; 61cb0ef41Sopenharmony_ci 71cb0ef41Sopenharmony_ciexport class V8CustomElement extends HTMLElement { 81cb0ef41Sopenharmony_ci _updateTimeoutId; 91cb0ef41Sopenharmony_ci _updateCallback = this.forceUpdate.bind(this); 101cb0ef41Sopenharmony_ci 111cb0ef41Sopenharmony_ci constructor(templateText) { 121cb0ef41Sopenharmony_ci super(); 131cb0ef41Sopenharmony_ci const shadowRoot = this.attachShadow({mode: 'open'}); 141cb0ef41Sopenharmony_ci shadowRoot.innerHTML = templateText; 151cb0ef41Sopenharmony_ci } 161cb0ef41Sopenharmony_ci 171cb0ef41Sopenharmony_ci $(id) { 181cb0ef41Sopenharmony_ci return this.shadowRoot.querySelector(id); 191cb0ef41Sopenharmony_ci } 201cb0ef41Sopenharmony_ci 211cb0ef41Sopenharmony_ci querySelectorAll(query) { 221cb0ef41Sopenharmony_ci return this.shadowRoot.querySelectorAll(query); 231cb0ef41Sopenharmony_ci } 241cb0ef41Sopenharmony_ci 251cb0ef41Sopenharmony_ci requestUpdate(useAnimation = false) { 261cb0ef41Sopenharmony_ci if (useAnimation) { 271cb0ef41Sopenharmony_ci window.cancelAnimationFrame(this._updateTimeoutId); 281cb0ef41Sopenharmony_ci this._updateTimeoutId = 291cb0ef41Sopenharmony_ci window.requestAnimationFrame(this._updateCallback); 301cb0ef41Sopenharmony_ci } else { 311cb0ef41Sopenharmony_ci // Use timeout tasks to asynchronously update the UI without blocking. 321cb0ef41Sopenharmony_ci clearTimeout(this._updateTimeoutId); 331cb0ef41Sopenharmony_ci const kDelayMs = 5; 341cb0ef41Sopenharmony_ci this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs); 351cb0ef41Sopenharmony_ci } 361cb0ef41Sopenharmony_ci } 371cb0ef41Sopenharmony_ci 381cb0ef41Sopenharmony_ci forceUpdate() { 391cb0ef41Sopenharmony_ci this._updateTimeoutId = undefined; 401cb0ef41Sopenharmony_ci this._update(); 411cb0ef41Sopenharmony_ci } 421cb0ef41Sopenharmony_ci 431cb0ef41Sopenharmony_ci _update() { 441cb0ef41Sopenharmony_ci throw Error('Subclass responsibility'); 451cb0ef41Sopenharmony_ci } 461cb0ef41Sopenharmony_ci 471cb0ef41Sopenharmony_ci get isFocused() { 481cb0ef41Sopenharmony_ci return document.activeElement === this; 491cb0ef41Sopenharmony_ci } 501cb0ef41Sopenharmony_ci} 511cb0ef41Sopenharmony_ci 521cb0ef41Sopenharmony_ciexport class FileReader extends V8CustomElement { 531cb0ef41Sopenharmony_ci constructor(templateText) { 541cb0ef41Sopenharmony_ci super(templateText); 551cb0ef41Sopenharmony_ci this.addEventListener('click', this.handleClick.bind(this)); 561cb0ef41Sopenharmony_ci this.addEventListener('dragover', this.handleDragOver.bind(this)); 571cb0ef41Sopenharmony_ci this.addEventListener('drop', this.handleChange.bind(this)); 581cb0ef41Sopenharmony_ci this.$('#file').addEventListener('change', this.handleChange.bind(this)); 591cb0ef41Sopenharmony_ci this.fileReader = this.$('#fileReader'); 601cb0ef41Sopenharmony_ci this.fileReader.addEventListener('keydown', this.handleKeyEvent.bind(this)); 611cb0ef41Sopenharmony_ci this.progressNode = this.$('#progress'); 621cb0ef41Sopenharmony_ci this.progressTextNode = this.$('#progressText'); 631cb0ef41Sopenharmony_ci } 641cb0ef41Sopenharmony_ci 651cb0ef41Sopenharmony_ci set error(message) { 661cb0ef41Sopenharmony_ci this._updateLabel(message); 671cb0ef41Sopenharmony_ci this.root.className = 'fail'; 681cb0ef41Sopenharmony_ci } 691cb0ef41Sopenharmony_ci 701cb0ef41Sopenharmony_ci _updateLabel(text) { 711cb0ef41Sopenharmony_ci this.$('#label').innerText = text; 721cb0ef41Sopenharmony_ci } 731cb0ef41Sopenharmony_ci 741cb0ef41Sopenharmony_ci handleKeyEvent(event) { 751cb0ef41Sopenharmony_ci if (event.key == 'Enter') this.handleClick(event); 761cb0ef41Sopenharmony_ci } 771cb0ef41Sopenharmony_ci 781cb0ef41Sopenharmony_ci handleClick(event) { 791cb0ef41Sopenharmony_ci this.$('#file').click(); 801cb0ef41Sopenharmony_ci } 811cb0ef41Sopenharmony_ci 821cb0ef41Sopenharmony_ci handleChange(event) { 831cb0ef41Sopenharmony_ci // Used for drop and file change. 841cb0ef41Sopenharmony_ci event.preventDefault(); 851cb0ef41Sopenharmony_ci const host = event.dataTransfer ? event.dataTransfer : event.target; 861cb0ef41Sopenharmony_ci this.readFile(host.files[0]); 871cb0ef41Sopenharmony_ci } 881cb0ef41Sopenharmony_ci 891cb0ef41Sopenharmony_ci handleDragOver(event) { 901cb0ef41Sopenharmony_ci event.preventDefault(); 911cb0ef41Sopenharmony_ci } 921cb0ef41Sopenharmony_ci 931cb0ef41Sopenharmony_ci connectedCallback() { 941cb0ef41Sopenharmony_ci this.fileReader.focus(); 951cb0ef41Sopenharmony_ci } 961cb0ef41Sopenharmony_ci 971cb0ef41Sopenharmony_ci get root() { 981cb0ef41Sopenharmony_ci return this.$('#root'); 991cb0ef41Sopenharmony_ci } 1001cb0ef41Sopenharmony_ci 1011cb0ef41Sopenharmony_ci setProgress(progress, processedBytes = 0) { 1021cb0ef41Sopenharmony_ci this.progress = Math.max(0, Math.min(progress, 1)); 1031cb0ef41Sopenharmony_ci this.processedBytes = processedBytes; 1041cb0ef41Sopenharmony_ci } 1051cb0ef41Sopenharmony_ci 1061cb0ef41Sopenharmony_ci updateProgressBar() { 1071cb0ef41Sopenharmony_ci // Create a circular progress bar, starting at 12 o'clock. 1081cb0ef41Sopenharmony_ci this.progressNode.style.backgroundImage = `conic-gradient( 1091cb0ef41Sopenharmony_ci var(--primary-color) 0%, 1101cb0ef41Sopenharmony_ci var(--primary-color) ${this.progress * 100}%, 1111cb0ef41Sopenharmony_ci var(--surface-color) ${this.progress * 100}%)`; 1121cb0ef41Sopenharmony_ci this.progressTextNode.innerText = 1131cb0ef41Sopenharmony_ci this.processedBytes ? formatBytes(this.processedBytes, 1) : ''; 1141cb0ef41Sopenharmony_ci if (this.root.className == 'loading') { 1151cb0ef41Sopenharmony_ci window.requestAnimationFrame(() => this.updateProgressBar()); 1161cb0ef41Sopenharmony_ci } 1171cb0ef41Sopenharmony_ci } 1181cb0ef41Sopenharmony_ci 1191cb0ef41Sopenharmony_ci readFile(file) { 1201cb0ef41Sopenharmony_ci this.dispatchEvent(new CustomEvent('fileuploadstart', { 1211cb0ef41Sopenharmony_ci bubbles: true, 1221cb0ef41Sopenharmony_ci composed: true, 1231cb0ef41Sopenharmony_ci detail: { 1241cb0ef41Sopenharmony_ci progressCallback: this.setProgress.bind(this), 1251cb0ef41Sopenharmony_ci totalSize: file.size, 1261cb0ef41Sopenharmony_ci } 1271cb0ef41Sopenharmony_ci })); 1281cb0ef41Sopenharmony_ci if (!file) { 1291cb0ef41Sopenharmony_ci this.error = 'Failed to load file.'; 1301cb0ef41Sopenharmony_ci return; 1311cb0ef41Sopenharmony_ci } 1321cb0ef41Sopenharmony_ci this.fileReader.blur(); 1331cb0ef41Sopenharmony_ci this.setProgress(0); 1341cb0ef41Sopenharmony_ci this.root.className = 'loading'; 1351cb0ef41Sopenharmony_ci // Delay the loading a bit to allow for CSS animations to happen. 1361cb0ef41Sopenharmony_ci window.requestAnimationFrame(() => this.asyncReadFile(file)); 1371cb0ef41Sopenharmony_ci } 1381cb0ef41Sopenharmony_ci 1391cb0ef41Sopenharmony_ci async asyncReadFile(file) { 1401cb0ef41Sopenharmony_ci this.updateProgressBar(); 1411cb0ef41Sopenharmony_ci const decoder = globalThis.TextDecoderStream; 1421cb0ef41Sopenharmony_ci if (decoder) { 1431cb0ef41Sopenharmony_ci await this._streamFile(file, decoder); 1441cb0ef41Sopenharmony_ci } else { 1451cb0ef41Sopenharmony_ci await this._readFullFile(file); 1461cb0ef41Sopenharmony_ci } 1471cb0ef41Sopenharmony_ci this._updateLabel(`Finished loading '${file.name}'.`); 1481cb0ef41Sopenharmony_ci this.dispatchEvent( 1491cb0ef41Sopenharmony_ci new CustomEvent('fileuploadend', {bubbles: true, composed: true})); 1501cb0ef41Sopenharmony_ci this.root.className = 'done'; 1511cb0ef41Sopenharmony_ci } 1521cb0ef41Sopenharmony_ci 1531cb0ef41Sopenharmony_ci async _readFullFile(file) { 1541cb0ef41Sopenharmony_ci const text = await file.text(); 1551cb0ef41Sopenharmony_ci this._handleFileChunk(text); 1561cb0ef41Sopenharmony_ci } 1571cb0ef41Sopenharmony_ci 1581cb0ef41Sopenharmony_ci async _streamFile(file, decoder) { 1591cb0ef41Sopenharmony_ci const stream = file.stream().pipeThrough(new decoder()); 1601cb0ef41Sopenharmony_ci const reader = stream.getReader(); 1611cb0ef41Sopenharmony_ci let chunk, readerDone; 1621cb0ef41Sopenharmony_ci do { 1631cb0ef41Sopenharmony_ci const readResult = await reader.read(); 1641cb0ef41Sopenharmony_ci chunk = readResult.value; 1651cb0ef41Sopenharmony_ci readerDone = readResult.done; 1661cb0ef41Sopenharmony_ci if (!chunk) break; 1671cb0ef41Sopenharmony_ci this._handleFileChunk(chunk); 1681cb0ef41Sopenharmony_ci // Artificial delay to allow for layout updates. 1691cb0ef41Sopenharmony_ci await delay(5); 1701cb0ef41Sopenharmony_ci } while (!readerDone); 1711cb0ef41Sopenharmony_ci } 1721cb0ef41Sopenharmony_ci 1731cb0ef41Sopenharmony_ci _handleFileChunk(chunk) { 1741cb0ef41Sopenharmony_ci this.dispatchEvent(new CustomEvent('fileuploadchunk', { 1751cb0ef41Sopenharmony_ci bubbles: true, 1761cb0ef41Sopenharmony_ci composed: true, 1771cb0ef41Sopenharmony_ci detail: chunk, 1781cb0ef41Sopenharmony_ci })); 1791cb0ef41Sopenharmony_ci } 1801cb0ef41Sopenharmony_ci} 1811cb0ef41Sopenharmony_ci 1821cb0ef41Sopenharmony_ciexport class DOM { 1831cb0ef41Sopenharmony_ci static element(type, options) { 1841cb0ef41Sopenharmony_ci const node = document.createElement(type); 1851cb0ef41Sopenharmony_ci if (options === undefined) return node; 1861cb0ef41Sopenharmony_ci if (typeof options === 'string') { 1871cb0ef41Sopenharmony_ci // Old behaviour: options = class string 1881cb0ef41Sopenharmony_ci node.className = options; 1891cb0ef41Sopenharmony_ci } else if (Array.isArray(options)) { 1901cb0ef41Sopenharmony_ci // Old behaviour: options = class array 1911cb0ef41Sopenharmony_ci DOM.addClasses(node, options); 1921cb0ef41Sopenharmony_ci } else { 1931cb0ef41Sopenharmony_ci // New behaviour: options = attribute dict 1941cb0ef41Sopenharmony_ci for (const [key, value] of Object.entries(options)) { 1951cb0ef41Sopenharmony_ci if (key == 'className') { 1961cb0ef41Sopenharmony_ci node.className = value; 1971cb0ef41Sopenharmony_ci } else if (key == 'classList') { 1981cb0ef41Sopenharmony_ci DOM.addClasses(node, value); 1991cb0ef41Sopenharmony_ci } else if (key == 'textContent') { 2001cb0ef41Sopenharmony_ci node.textContent = value; 2011cb0ef41Sopenharmony_ci } else if (key == 'children') { 2021cb0ef41Sopenharmony_ci for (const child of value) { 2031cb0ef41Sopenharmony_ci node.appendChild(child); 2041cb0ef41Sopenharmony_ci } 2051cb0ef41Sopenharmony_ci } else { 2061cb0ef41Sopenharmony_ci node.setAttribute(key, value); 2071cb0ef41Sopenharmony_ci } 2081cb0ef41Sopenharmony_ci } 2091cb0ef41Sopenharmony_ci } 2101cb0ef41Sopenharmony_ci return node; 2111cb0ef41Sopenharmony_ci } 2121cb0ef41Sopenharmony_ci 2131cb0ef41Sopenharmony_ci static addClasses(node, classes) { 2141cb0ef41Sopenharmony_ci const classList = node.classList; 2151cb0ef41Sopenharmony_ci if (typeof classes === 'string') { 2161cb0ef41Sopenharmony_ci classList.add(classes); 2171cb0ef41Sopenharmony_ci } else { 2181cb0ef41Sopenharmony_ci for (let i = 0; i < classes.length; i++) { 2191cb0ef41Sopenharmony_ci classList.add(classes[i]); 2201cb0ef41Sopenharmony_ci } 2211cb0ef41Sopenharmony_ci } 2221cb0ef41Sopenharmony_ci return node; 2231cb0ef41Sopenharmony_ci } 2241cb0ef41Sopenharmony_ci 2251cb0ef41Sopenharmony_ci static text(string) { 2261cb0ef41Sopenharmony_ci return document.createTextNode(string); 2271cb0ef41Sopenharmony_ci } 2281cb0ef41Sopenharmony_ci 2291cb0ef41Sopenharmony_ci static button(label, clickHandler) { 2301cb0ef41Sopenharmony_ci const button = DOM.element('button'); 2311cb0ef41Sopenharmony_ci button.innerText = label; 2321cb0ef41Sopenharmony_ci if (typeof clickHandler != 'function') { 2331cb0ef41Sopenharmony_ci throw new Error( 2341cb0ef41Sopenharmony_ci `DOM.button: Expected function but got clickHandler=${clickHandler}`); 2351cb0ef41Sopenharmony_ci } 2361cb0ef41Sopenharmony_ci button.onclick = clickHandler; 2371cb0ef41Sopenharmony_ci return button; 2381cb0ef41Sopenharmony_ci } 2391cb0ef41Sopenharmony_ci 2401cb0ef41Sopenharmony_ci static div(options) { 2411cb0ef41Sopenharmony_ci return this.element('div', options); 2421cb0ef41Sopenharmony_ci } 2431cb0ef41Sopenharmony_ci 2441cb0ef41Sopenharmony_ci static span(options) { 2451cb0ef41Sopenharmony_ci return this.element('span', options); 2461cb0ef41Sopenharmony_ci } 2471cb0ef41Sopenharmony_ci 2481cb0ef41Sopenharmony_ci static table(options) { 2491cb0ef41Sopenharmony_ci return this.element('table', options); 2501cb0ef41Sopenharmony_ci } 2511cb0ef41Sopenharmony_ci 2521cb0ef41Sopenharmony_ci static tbody(options) { 2531cb0ef41Sopenharmony_ci return this.element('tbody', options); 2541cb0ef41Sopenharmony_ci } 2551cb0ef41Sopenharmony_ci 2561cb0ef41Sopenharmony_ci static td(textOrNode, className) { 2571cb0ef41Sopenharmony_ci const node = this.element('td'); 2581cb0ef41Sopenharmony_ci if (typeof textOrNode === 'object') { 2591cb0ef41Sopenharmony_ci node.appendChild(textOrNode); 2601cb0ef41Sopenharmony_ci } else if (textOrNode) { 2611cb0ef41Sopenharmony_ci node.innerText = textOrNode; 2621cb0ef41Sopenharmony_ci } 2631cb0ef41Sopenharmony_ci if (className) node.className = className; 2641cb0ef41Sopenharmony_ci return node; 2651cb0ef41Sopenharmony_ci } 2661cb0ef41Sopenharmony_ci 2671cb0ef41Sopenharmony_ci static tr(classes) { 2681cb0ef41Sopenharmony_ci return this.element('tr', classes); 2691cb0ef41Sopenharmony_ci } 2701cb0ef41Sopenharmony_ci 2711cb0ef41Sopenharmony_ci static removeAllChildren(node) { 2721cb0ef41Sopenharmony_ci let range = document.createRange(); 2731cb0ef41Sopenharmony_ci range.selectNodeContents(node); 2741cb0ef41Sopenharmony_ci range.deleteContents(); 2751cb0ef41Sopenharmony_ci } 2761cb0ef41Sopenharmony_ci 2771cb0ef41Sopenharmony_ci static defineCustomElement( 2781cb0ef41Sopenharmony_ci path, nameOrGenerator, maybeGenerator = undefined) { 2791cb0ef41Sopenharmony_ci let generator = nameOrGenerator; 2801cb0ef41Sopenharmony_ci let name = nameOrGenerator; 2811cb0ef41Sopenharmony_ci if (typeof nameOrGenerator == 'function') { 2821cb0ef41Sopenharmony_ci console.assert(maybeGenerator === undefined); 2831cb0ef41Sopenharmony_ci name = path.substring(path.lastIndexOf('/') + 1, path.length); 2841cb0ef41Sopenharmony_ci } else { 2851cb0ef41Sopenharmony_ci console.assert(typeof nameOrGenerator == 'string'); 2861cb0ef41Sopenharmony_ci generator = maybeGenerator; 2871cb0ef41Sopenharmony_ci } 2881cb0ef41Sopenharmony_ci path = path + '-template.html'; 2891cb0ef41Sopenharmony_ci fetch(path) 2901cb0ef41Sopenharmony_ci .then(stream => stream.text()) 2911cb0ef41Sopenharmony_ci .then( 2921cb0ef41Sopenharmony_ci templateText => 2931cb0ef41Sopenharmony_ci customElements.define(name, generator(templateText))); 2941cb0ef41Sopenharmony_ci } 2951cb0ef41Sopenharmony_ci} 296