1// Copyright 2020 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
5export class CSSColor {
6  static _cache = new Map();
7
8  static get(name) {
9    let color = this._cache.get(name);
10    if (color !== undefined) return color;
11    const style = getComputedStyle(document.body);
12    color = style.getPropertyValue(`--${name}`);
13    if (color === undefined) {
14      throw new Error(`CSS color does not exist: ${name}`);
15    }
16    color = color.trim();
17    this._cache.set(name, color);
18    return color;
19  }
20
21  static reset() {
22    this._cache.clear();
23  }
24
25  static get backgroundColor() {
26    return this.get('background-color');
27  }
28  static get surfaceColor() {
29    return this.get('surface-color');
30  }
31  static get primaryColor() {
32    return this.get('primary-color');
33  }
34  static get secondaryColor() {
35    return this.get('secondary-color');
36  }
37  static get onSurfaceColor() {
38    return this.get('on-surface-color');
39  }
40  static get onBackgroundColor() {
41    return this.get('on-background-color');
42  }
43  static get onPrimaryColor() {
44    return this.get('on-primary-color');
45  }
46  static get onSecondaryColor() {
47    return this.get('on-secondary-color');
48  }
49  static get defaultColor() {
50    return this.get('default-color');
51  }
52  static get errorColor() {
53    return this.get('error-color');
54  }
55  static get mapBackgroundColor() {
56    return this.get('map-background-color');
57  }
58  static get timelineBackgroundColor() {
59    return this.get('timeline-background-color');
60  }
61  static get red() {
62    return this.get('red');
63  }
64  static get green() {
65    return this.get('green');
66  }
67  static get yellow() {
68    return this.get('yellow');
69  }
70  static get blue() {
71    return this.get('blue');
72  }
73
74  static get orange() {
75    return this.get('orange');
76  }
77
78  static get violet() {
79    return this.get('violet');
80  }
81
82  static at(index) {
83    return this.list[index % this.list.length];
84  }
85
86  static darken(hexColorString, amount = -50) {
87    if (hexColorString[0] !== '#') {
88      throw new Error(`Unsupported color: ${hexColorString}`);
89    }
90    let color = parseInt(hexColorString.substring(1), 16);
91    let b = Math.min(Math.max((color & 0xFF) + amount, 0), 0xFF);
92    let g = Math.min(Math.max(((color >> 8) & 0xFF) + amount, 0), 0xFF);
93    let r = Math.min(Math.max(((color >> 16) & 0xFF) + amount, 0), 0xFF);
94    color = (r << 16) + (g << 8) + b;
95    return `#${color.toString(16).padStart(6, '0')}`;
96  }
97
98  static get list() {
99    if (!this._colors) {
100      this._colors = [
101        this.green,
102        this.violet,
103        this.orange,
104        this.yellow,
105        this.primaryColor,
106        this.red,
107        this.blue,
108        this.yellow,
109        this.secondaryColor,
110        this.darken(this.green),
111        this.darken(this.violet),
112        this.darken(this.orange),
113        this.darken(this.yellow),
114        this.darken(this.primaryColor),
115        this.darken(this.red),
116        this.darken(this.blue),
117        this.darken(this.yellow),
118        this.darken(this.secondaryColor),
119      ];
120    }
121    return this._colors;
122  }
123}
124
125export class DOM {
126  static element(type, options) {
127    const node = document.createElement(type);
128    if (options !== undefined) {
129      if (typeof options === 'string') {
130        // Old behaviour: options = class string
131        node.className = options;
132      } else if (Array.isArray(options)) {
133        // Old behaviour: options = class array
134        DOM.addClasses(node, options);
135      } else {
136        // New behaviour: options = attribute dict
137        for (const [key, value] of Object.entries(options)) {
138          if (key == 'className') {
139            node.className = value;
140          } else if (key == 'classList') {
141            node.classList = value;
142          } else if (key == 'textContent') {
143            node.textContent = value;
144          } else if (key == 'children') {
145            for (const child of value) {
146              node.appendChild(child);
147            }
148          } else {
149            node.setAttribute(key, value);
150          }
151        }
152      }
153    }
154    return node;
155  }
156
157  static addClasses(node, classes) {
158    const classList = node.classList;
159    if (typeof classes === 'string') {
160      classList.add(classes);
161    } else {
162      for (let i = 0; i < classes.length; i++) {
163        classList.add(classes[i]);
164      }
165    }
166    return node;
167  }
168
169  static text(string) {
170    return document.createTextNode(string);
171  }
172
173  static button(label, clickHandler) {
174    const button = DOM.element('button');
175    button.innerText = label;
176    button.onclick = clickHandler;
177    return button;
178  }
179
180  static div(options) {
181    return this.element('div', options);
182  }
183
184  static span(options) {
185    return this.element('span', options);
186  }
187
188  static table(options) {
189    return this.element('table', options);
190  }
191
192  static tbody(options) {
193    return this.element('tbody', options);
194  }
195
196  static td(textOrNode, className) {
197    const node = this.element('td');
198    if (typeof textOrNode === 'object') {
199      node.appendChild(textOrNode);
200    } else if (textOrNode) {
201      node.innerText = textOrNode;
202    }
203    if (className) node.className = className;
204    return node;
205  }
206
207  static tr(classes) {
208    return this.element('tr', classes);
209  }
210
211  static removeAllChildren(node) {
212    let range = document.createRange();
213    range.selectNodeContents(node);
214    range.deleteContents();
215  }
216
217  static defineCustomElement(
218      path, nameOrGenerator, maybeGenerator = undefined) {
219    let generator = nameOrGenerator;
220    let name = nameOrGenerator;
221    if (typeof nameOrGenerator == 'function') {
222      console.assert(maybeGenerator === undefined);
223      name = path.substring(path.lastIndexOf('/') + 1, path.length);
224    } else {
225      console.assert(typeof nameOrGenerator == 'string');
226      generator = maybeGenerator;
227    }
228    path = path + '-template.html';
229    fetch(path)
230        .then(stream => stream.text())
231        .then(
232            templateText =>
233                customElements.define(name, generator(templateText)));
234  }
235}
236
237const SVGNamespace = 'http://www.w3.org/2000/svg';
238export class SVG {
239  static element(type, classes) {
240    const node = document.createElementNS(SVGNamespace, type);
241    if (classes !== undefined) DOM.addClasses(node, classes);
242    return node;
243  }
244
245  static svg(classes) {
246    return this.element('svg', classes);
247  }
248
249  static rect(classes) {
250    return this.element('rect', classes);
251  }
252
253  static g(classes) {
254    return this.element('g', classes);
255  }
256}
257
258export function $(id) {
259  return document.querySelector(id)
260}
261
262export class V8CustomElement extends HTMLElement {
263  _updateTimeoutId;
264  _updateCallback = this.forceUpdate.bind(this);
265
266  constructor(templateText) {
267    super();
268    const shadowRoot = this.attachShadow({mode: 'open'});
269    shadowRoot.innerHTML = templateText;
270  }
271
272  $(id) {
273    return this.shadowRoot.querySelector(id);
274  }
275
276  querySelectorAll(query) {
277    return this.shadowRoot.querySelectorAll(query);
278  }
279
280  requestUpdate(useAnimation = false) {
281    if (useAnimation) {
282      window.cancelAnimationFrame(this._updateTimeoutId);
283      this._updateTimeoutId =
284          window.requestAnimationFrame(this._updateCallback);
285    } else {
286      // Use timeout tasks to asynchronously update the UI without blocking.
287      clearTimeout(this._updateTimeoutId);
288      const kDelayMs = 5;
289      this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
290    }
291  }
292
293  forceUpdate() {
294    this._update();
295  }
296
297  _update() {
298    throw Error('Subclass responsibility');
299  }
300}
301
302export class CollapsableElement extends V8CustomElement {
303  constructor(templateText) {
304    super(templateText);
305    this._hasPendingUpdate = false;
306    this._closer.onclick = _ => this._requestUpdateIfVisible();
307  }
308
309  get _closer() {
310    return this.$('#closer');
311  }
312
313  get _contentIsVisible() {
314    return !this._closer.checked;
315  }
316
317  hide() {
318    if (this._contentIsVisible) {
319      this._closer.checked = true;
320      this._requestUpdateIfVisible();
321    }
322    this.scrollIntoView();
323  }
324
325  show() {
326    if (!this._contentIsVisible) {
327      this._closer.checked = false;
328      this._requestUpdateIfVisible();
329    }
330    this.scrollIntoView();
331  }
332
333  requestUpdate(useAnimation = false) {
334    // A pending update will be resolved later, no need to try again.
335    if (this._hasPendingUpdate) return;
336    this._hasPendingUpdate = true;
337    this._requestUpdateIfVisible(useAnimation);
338  }
339
340  _requestUpdateIfVisible(useAnimation = true) {
341    if (!this._contentIsVisible) return;
342    return super.requestUpdate(useAnimation);
343  }
344
345  forceUpdate() {
346    this._hasPendingUpdate = false;
347    super.forceUpdate();
348  }
349}
350
351export class ExpandableText {
352  constructor(node, string, limit = 200) {
353    this._node = node;
354    this._string = string;
355    this._delta = limit / 2;
356    this._start = 0;
357    this._end = string.length;
358    this._button = this._createExpandButton();
359    this.expand();
360  }
361
362  _createExpandButton() {
363    const button = DOM.element('button');
364    button.innerText = '...';
365    button.onclick = (e) => {
366      e.stopImmediatePropagation();
367      this.expand()
368    };
369    return button;
370  }
371
372  expand() {
373    DOM.removeAllChildren(this._node);
374    this._start = this._start + this._delta;
375    this._end = this._end - this._delta;
376    if (this._start >= this._end) {
377      this._node.innerText = this._string;
378      this._button.onclick = undefined;
379      return;
380    }
381    this._node.appendChild(DOM.text(this._string.substring(0, this._start)));
382    this._node.appendChild(this._button);
383    this._node.appendChild(
384        DOM.text(this._string.substring(this._end, this._string.length)));
385  }
386}
387
388export class Chunked {
389  constructor(iterable, limit) {
390    this._iterator = iterable[Symbol.iterator]();
391    this._limit = limit;
392  }
393
394  * next(limit = undefined) {
395    for (let i = 0; i < (limit ?? this._limit); i++) {
396      const {value, done} = this._iterator.next();
397      if (done) {
398        this._iterator = undefined;
399        return;
400      };
401      yield value;
402    }
403  }
404
405  get hasMore() {
406    return this._iterator !== undefined;
407  }
408}
409
410export class LazyTable {
411  constructor(table, rowData, rowElementCreator, limit = 100) {
412    this._table = table;
413    this._chunkedRowData = new Chunked(rowData, limit);
414    this._rowElementCreator = rowElementCreator;
415    if (table.tBodies.length == 0) {
416      table.appendChild(DOM.tbody());
417    } else {
418      table.replaceChild(DOM.tbody(), table.tBodies[0]);
419    }
420    if (!table.tFoot) this._addFooter();
421    table.tFoot.addEventListener('click', this._clickHandler);
422    this._addMoreRows();
423  }
424
425  _addFooter() {
426    const td = DOM.td();
427    td.setAttribute('colspan', 100);
428    for (let addCount of [10, 100, 250, 500]) {
429      const button = DOM.element('button');
430      button.innerText = `+${addCount}`;
431      button.onclick = (e) => this._addMoreRows(addCount);
432      td.appendChild(button);
433    }
434    this._table.appendChild(DOM.element('tfoot'))
435        .appendChild(DOM.tr())
436        .appendChild(td);
437  }
438
439  _addMoreRows(count = undefined) {
440    const fragment = new DocumentFragment();
441    for (let row of this._chunkedRowData.next(count)) {
442      const tr = this._rowElementCreator(row);
443      fragment.appendChild(tr);
444    }
445    this._table.tBodies[0].appendChild(fragment);
446    if (!this._chunkedRowData.hasMore) {
447      DOM.removeAllChildren(this._table.tFoot);
448    }
449  }
450}
451
452export function gradientStopsFromGroups(
453    totalLength, maxHeight, groups, colorFn) {
454  const kMaxHeight = maxHeight === '%' ? 100 : maxHeight;
455  const kUnit = maxHeight === '%' ? '%' : 'px';
456  let increment = 0;
457  let lastHeight = 0.0;
458  const stops = [];
459  for (let group of groups) {
460    const color = colorFn(group.key);
461    increment += group.length;
462    const height = (increment / totalLength * kMaxHeight) | 0;
463    stops.push(`${color} ${lastHeight}${kUnit} ${height}${kUnit}`)
464    lastHeight = height;
465  }
466  return stops;
467}
468
469export * from '../helper.mjs';