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