• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {delay, formatBytes} from './helper.mjs';
6
7export class V8CustomElement extends HTMLElement {
8  _updateTimeoutId;
9  _updateCallback = this.forceUpdate.bind(this);
10
11  constructor(templateText) {
12    super();
13    const shadowRoot = this.attachShadow({mode: 'open'});
14    shadowRoot.innerHTML = templateText;
15  }
16
17  $(id) {
18    return this.shadowRoot.querySelector(id);
19  }
20
21  querySelectorAll(query) {
22    return this.shadowRoot.querySelectorAll(query);
23  }
24
25  requestUpdate(useAnimation = false) {
26    if (useAnimation) {
27      window.cancelAnimationFrame(this._updateTimeoutId);
28      this._updateTimeoutId =
29          window.requestAnimationFrame(this._updateCallback);
30    } else {
31      // Use timeout tasks to asynchronously update the UI without blocking.
32      clearTimeout(this._updateTimeoutId);
33      const kDelayMs = 5;
34      this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
35    }
36  }
37
38  forceUpdate() {
39    this._updateTimeoutId = undefined;
40    this._update();
41  }
42
43  _update() {
44    throw Error('Subclass responsibility');
45  }
46
47  get isFocused() {
48    return document.activeElement === this;
49  }
50}
51
52export class FileReader extends V8CustomElement {
53  constructor(templateText) {
54    super(templateText);
55    this.addEventListener('click', this.handleClick.bind(this));
56    this.addEventListener('dragover', this.handleDragOver.bind(this));
57    this.addEventListener('drop', this.handleChange.bind(this));
58    this.$('#file').addEventListener('change', this.handleChange.bind(this));
59    this.fileReader = this.$('#fileReader');
60    this.fileReader.addEventListener('keydown', this.handleKeyEvent.bind(this));
61    this.progressNode = this.$('#progress');
62    this.progressTextNode = this.$('#progressText');
63  }
64
65  set error(message) {
66    this._updateLabel(message);
67    this.root.className = 'fail';
68  }
69
70  _updateLabel(text) {
71    this.$('#label').innerText = text;
72  }
73
74  handleKeyEvent(event) {
75    if (event.key == 'Enter') this.handleClick(event);
76  }
77
78  handleClick(event) {
79    this.$('#file').click();
80  }
81
82  handleChange(event) {
83    // Used for drop and file change.
84    event.preventDefault();
85    const host = event.dataTransfer ? event.dataTransfer : event.target;
86    this.readFile(host.files[0]);
87  }
88
89  handleDragOver(event) {
90    event.preventDefault();
91  }
92
93  connectedCallback() {
94    this.fileReader.focus();
95  }
96
97  get root() {
98    return this.$('#root');
99  }
100
101  setProgress(progress, processedBytes = 0) {
102    this.progress = Math.max(0, Math.min(progress, 1));
103    this.processedBytes = processedBytes;
104  }
105
106  updateProgressBar() {
107    // Create a circular progress bar, starting at 12 o'clock.
108    this.progressNode.style.backgroundImage = `conic-gradient(
109          var(--primary-color) 0%,
110          var(--primary-color) ${this.progress * 100}%,
111          var(--surface-color) ${this.progress * 100}%)`;
112    this.progressTextNode.innerText =
113        this.processedBytes ? formatBytes(this.processedBytes, 1) : '';
114    if (this.root.className == 'loading') {
115      window.requestAnimationFrame(() => this.updateProgressBar());
116    }
117  }
118
119  readFile(file) {
120    this.dispatchEvent(new CustomEvent('fileuploadstart', {
121      bubbles: true,
122      composed: true,
123      detail: {
124        progressCallback: this.setProgress.bind(this),
125        totalSize: file.size,
126      }
127    }));
128    if (!file) {
129      this.error = 'Failed to load file.';
130      return;
131    }
132    this.fileReader.blur();
133    this.setProgress(0);
134    this.root.className = 'loading';
135    // Delay the loading a bit to allow for CSS animations to happen.
136    window.requestAnimationFrame(() => this.asyncReadFile(file));
137  }
138
139  async asyncReadFile(file) {
140    this.updateProgressBar();
141    const decoder = globalThis.TextDecoderStream;
142    if (decoder) {
143      await this._streamFile(file, decoder);
144    } else {
145      await this._readFullFile(file);
146    }
147    this._updateLabel(`Finished loading '${file.name}'.`);
148    this.dispatchEvent(
149        new CustomEvent('fileuploadend', {bubbles: true, composed: true}));
150    this.root.className = 'done';
151  }
152
153  async _readFullFile(file) {
154    const text = await file.text();
155    this._handleFileChunk(text);
156  }
157
158  async _streamFile(file, decoder) {
159    const stream = file.stream().pipeThrough(new decoder());
160    const reader = stream.getReader();
161    let chunk, readerDone;
162    do {
163      const readResult = await reader.read();
164      chunk = readResult.value;
165      readerDone = readResult.done;
166      if (!chunk) break;
167      this._handleFileChunk(chunk);
168      // Artificial delay to allow for layout updates.
169      await delay(5);
170    } while (!readerDone);
171  }
172
173  _handleFileChunk(chunk) {
174    this.dispatchEvent(new CustomEvent('fileuploadchunk', {
175      bubbles: true,
176      composed: true,
177      detail: chunk,
178    }));
179  }
180}
181
182export class DOM {
183  static element(type, options) {
184    const node = document.createElement(type);
185    if (options === undefined) return node;
186    if (typeof options === 'string') {
187      // Old behaviour: options = class string
188      node.className = options;
189    } else if (Array.isArray(options)) {
190      // Old behaviour: options = class array
191      DOM.addClasses(node, options);
192    } else {
193      // New behaviour: options = attribute dict
194      for (const [key, value] of Object.entries(options)) {
195        if (key == 'className') {
196          node.className = value;
197        } else if (key == 'classList') {
198          DOM.addClasses(node, value);
199        } else if (key == 'textContent') {
200          node.textContent = value;
201        } else if (key == 'children') {
202          for (const child of value) {
203            node.appendChild(child);
204          }
205        } else {
206          node.setAttribute(key, value);
207        }
208      }
209    }
210    return node;
211  }
212
213  static addClasses(node, classes) {
214    const classList = node.classList;
215    if (typeof classes === 'string') {
216      classList.add(classes);
217    } else {
218      for (let i = 0; i < classes.length; i++) {
219        classList.add(classes[i]);
220      }
221    }
222    return node;
223  }
224
225  static text(string) {
226    return document.createTextNode(string);
227  }
228
229  static button(label, clickHandler) {
230    const button = DOM.element('button');
231    button.innerText = label;
232    if (typeof clickHandler != 'function') {
233      throw new Error(
234          `DOM.button: Expected function but got clickHandler=${clickHandler}`);
235    }
236    button.onclick = clickHandler;
237    return button;
238  }
239
240  static div(options) {
241    return this.element('div', options);
242  }
243
244  static span(options) {
245    return this.element('span', options);
246  }
247
248  static table(options) {
249    return this.element('table', options);
250  }
251
252  static tbody(options) {
253    return this.element('tbody', options);
254  }
255
256  static td(textOrNode, className) {
257    const node = this.element('td');
258    if (typeof textOrNode === 'object') {
259      node.appendChild(textOrNode);
260    } else if (textOrNode) {
261      node.innerText = textOrNode;
262    }
263    if (className) node.className = className;
264    return node;
265  }
266
267  static tr(classes) {
268    return this.element('tr', classes);
269  }
270
271  static removeAllChildren(node) {
272    let range = document.createRange();
273    range.selectNodeContents(node);
274    range.deleteContents();
275  }
276
277  static defineCustomElement(
278      path, nameOrGenerator, maybeGenerator = undefined) {
279    let generator = nameOrGenerator;
280    let name = nameOrGenerator;
281    if (typeof nameOrGenerator == 'function') {
282      console.assert(maybeGenerator === undefined);
283      name = path.substring(path.lastIndexOf('/') + 1, path.length);
284    } else {
285      console.assert(typeof nameOrGenerator == 'string');
286      generator = maybeGenerator;
287    }
288    path = path + '-template.html';
289    fetch(path)
290        .then(stream => stream.text())
291        .then(
292            templateText =>
293                customElements.define(name, generator(templateText)));
294  }
295}
296