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';