diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2019-08-30 10:22:43 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2019-08-30 12:36:28 +0000 |
commit | 271a6c3487a14599023a9106329505597638d793 (patch) | |
tree | e040d58ffc86c1480b79ca8528020ca9ec919bf8 /chromium/third_party/blink/renderer/core/script/resources/layered_api | |
parent | 7b2ffa587235a47d4094787d72f38102089f402a (diff) | |
download | qtwebengine-chromium-271a6c3487a14599023a9106329505597638d793.tar.gz |
BASELINE: Update Chromium to 77.0.3865.59
Change-Id: I1e89a5f3b009a9519a6705102ad65c92fe736f21
Reviewed-by: Michael BrĂ¼ning <michael.bruning@qt.io>
Diffstat (limited to 'chromium/third_party/blink/renderer/core/script/resources/layered_api')
30 files changed, 2517 insertions, 1997 deletions
diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/PRESUBMIT.py b/chromium/third_party/blink/renderer/core/script/resources/layered_api/PRESUBMIT.py new file mode 100644 index 00000000000..4364aafa726 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/PRESUBMIT.py @@ -0,0 +1,39 @@ +# Copyright 2019 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +import sys + + +def _CommonChecks(input_api, output_api): + results = [] + # TODO(tkent): {kv-storage,toast,virtual-scroller}/.eslintrc.js specify babel-eslint parser, which + # is not in third_party/node/node_modules/. + mjs_files = input_api.AffectedFiles( + file_filter=lambda f: (f.LocalPath().endswith('.mjs') and + not '/virtual-scroller/' in f.LocalPath() and + not '/kv-storage/' in f.LocalPath() and + not '/toast/' in f.LocalPath()), + include_deletes=False) + if not mjs_files: + return results + try: + old_sys_path = sys.path[:] + cwd = input_api.PresubmitLocalPath() + sys.path += [input_api.os_path.join(cwd, '..', '..', '..', '..', '..', + '..', '..', 'tools')] + import web_dev_style.js_checker + checker = web_dev_style.js_checker.JSChecker(input_api, output_api) + results += checker.RunEsLintChecks(mjs_files) + finally: + sys.path = old_sys_path + return results + + +def CheckChangeOnUpload(input_api, output_api): + return _CommonChecks(input_api, output_api) + + +def CheckChangeOnCommit(input_api, output_api): + return _CommonChecks(input_api, output_api) diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/README.md b/chromium/third_party/blink/renderer/core/script/resources/layered_api/README.md index 270d8117aa5..479818efa35 100644 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/README.md +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/README.md @@ -22,11 +22,27 @@ and commit these files together with the changes under resources/layered_api/. ## Which files are bundled -All files under this directory will be included in the grdp and thus bundled -in the Chromium binary, except for +All files under -- Files directly under `core/script/resources/layered_api`, or -- Files starting with '.', 'README', or 'OWNERS'. +- Sub-directories which have 'index.mjs' or +- Directories of which last path component is 'internal' + +will be included in the grdp and thus bundled in the Chrome binary, +except for files starting with '.', 'README', or 'OWNERS'. So be careful about binary size increase when you add new files or add more contents to existing files. + +## What are exposed + +All bundled resources are mapped to `std-internal://path-relative-to-here`, and +`std-internal:` resources are not accessible from the web. Resources loaded as +`std-internal:` can import other `std-internal:` resources. + +For example, `layered_api/foo/bar/baz.mjs` is mapped to +`std-internal://foo/bar/baz.mjs`. + +All `index.mjs` resources are mapped to `std:directory-name-relative-to-here` +too, and they are web-exposed. For example, +`layered_api/elements/toast/index.mjs` is mapped to `std:elements/toast` as +well as `std-internal://elements/toast/index.mjs`. diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/internal/reflection.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/internal/reflection.mjs new file mode 100644 index 00000000000..989a4aa1ff1 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/internal/reflection.mjs @@ -0,0 +1,63 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @file Manage attribute-property reflections. + * https://html.spec.whatwg.org/C/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes + */ + +/** + * Add a bool reflection property to the specified prototype for the specified + * attribute. + * + * @param {!Object} proto An element prototype + * @param {string} attrName An attribute name + * @param {string} propName An optional property name. attrName will be used if + * this argument is omitted. + */ +export function installBool(proto, attrName, propName = attrName) { + let getter = function() { + return this.hasAttribute(attrName); + }; + let setter = function(value) { + this.toggleAttribute(attrName, Boolean(value)); + }; + Object.defineProperty( + getter, 'name', + {configurable: true, enumerable: false, value: 'get ' + propName}); + Object.defineProperty( + setter, 'name', + {configurable: true, enumerable: false, value: 'set ' + propName}); + Object.defineProperty( + proto, propName, + {configurable: true, enumerable: true, get: getter, set: setter}); +} + +/** + * Add a DOMString reflection property to the specified prototype for the + * specified attribute. + * + * @param {!Element} element An element prototype + * @param {string} attrName An attribute name + * @param {string} propName An optional property name. attrName will be used if + * this argument is omitted. + */ +export function installString(proto, attrName, propName = attrName) { + let getter = function() { + let value = this.getAttribute(attrName); + return value === null ? '' : value; + }; + let setter = function(value) { + this.setAttribute(attrName, value); + }; + Object.defineProperty( + getter, 'name', + {configurable: true, enumerable: false, value: 'get ' + propName}); + Object.defineProperty( + setter, 'name', + {configurable: true, enumerable: false, value: 'set ' + propName}); + Object.defineProperty( + proto, propName, + {configurable: true, enumerable: true, get: getter, set: setter}); +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/README.md b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/README.md new file mode 100644 index 00000000000..c8962f767cb --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/README.md @@ -0,0 +1,3 @@ +# Switch control element + +https://github.com/tkent-google/std-switch/blob/master/README.md diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/face_utils.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/face_utils.mjs new file mode 100644 index 00000000000..8db1f4177ff --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/face_utils.mjs @@ -0,0 +1,75 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @file Utilities for form-associated custom elements + */ + +import * as reflection from '../internal/reflection.mjs'; + +function installGetter(proto, propName, getter) { + Object.defineProperty( + getter, 'name', + {configurable: true, enumerable: false, value: 'get ' + propName}); + Object.defineProperty( + proto, propName, {configurable: true, enumerable: true, get: getter}); +} + +/** + * Add the following properties to |proto|. + * - disabled + * - name + * - type + * - form + * - willValidate + * - validity + * - validationMessage + * - labels + * - checkValidity() + * - reportValidity() + * - setCustomValidity(error) + * + * @param {!Object} proto An Element prototype which will have properties + * @param {!Symbol} internals A Symbol of the ElementInternals property of the + * element + */ +export function installPropertiesAndFunctions(proto, internals) { + reflection.installBool(proto, 'disabled'); + reflection.installString(proto, 'name'); + installGetter(proto, 'type', function() { + if (!(this instanceof proto.constructor)) { + throw TypeError( + 'The context object is not an instance of ' + proto.contructor.name); + } + return this.localName; + }); + + installGetter(proto, 'form', function() { + return this[internals].form; + }); + installGetter(proto, 'willValidate', function() { + return this[internals].willValidate; + }); + installGetter(proto, 'validity', function() { + return this[internals].validity; + }); + installGetter(proto, 'validationMessage', function() { + return this[internals].validationMessage; + }); + installGetter(proto, 'labels', function() { + return this[internals].labels; + }); + proto.checkValidity = function() { + return this[internals].checkValidity(); + }; + proto.reportValidity = function() { + return this[internals].reportValidity(); + }; + proto.setCustomValidity = function(error) { + if (error === undefined) { + throw new TypeError('Too few arguments'); + } + this[internals].setValidity({customError: true}, error); + }; +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/index.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/index.mjs new file mode 100644 index 00000000000..6c7be3d46fc --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/index.mjs @@ -0,0 +1,128 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as face from './face_utils.mjs'; +import * as reflection from '../internal/reflection.mjs'; +import { SwitchTrack } from './track.mjs'; +import * as style from './style.mjs'; + +// https://github.com/tkent-google/std-switch/issues/2 +const STATE_ATTR = 'on'; + +// Private property symbols +// TODO(tkent): Use private fields. +const _internals = Symbol(); +const _track = Symbol(); +const _rippleElement = Symbol(); +const _containerElement = Symbol(); + +export class StdSwitchElement extends HTMLElement { + // TODO(tkent): The following should be |static fooBar = value;| + // after enabling babel-eslint. + static get formAssociated() { + return true; + } + static get observedAttributes() { + return [STATE_ATTR]; + } + + constructor() { + super(); + if (new.target !== StdSwitchElement) { + throw new TypeError('Illegal constructor: StdSwitchElement is not ' + + 'extensible for now'); + } + this[_internals] = this.attachInternals(); + this._initializeDOM(); + + this.addEventListener('click', this._onClick); + this.addEventListener('keypress', this._onKeyPress); + } + + attributeChangedCallback(attrName, oldValue, newValue) { + if (attrName == STATE_ATTR) { + this[_track].value = newValue !== null; + // TODO(tkent): We should not add aria-checked attribute. + // https://github.com/WICG/aom/issues/127 + this.setAttribute('aria-checked', newValue !== null ? 'true' : 'false'); + } + } + + connectedCallback() { + // TODO(tkent): We should not add tabindex attribute. + // https://github.com/w3c/webcomponents/issues/762 + if (!this.hasAttribute('tabindex')) { + this.setAttribute('tabindex', '0'); + } + + // TODO(tkent): We should not add role attribute. + // https://github.com/WICG/aom/issues/127 + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'switch'); + } + } + + // TODO(tkent): Make this private. + _initializeDOM() { + let factory = this.ownerDocument; + let root = this.attachShadow({mode: 'closed'}); + this[_containerElement] = factory.createElement('span'); + this[_containerElement].id = 'container'; + // Shadow elements should be invisible for a11y technologies. + this[_containerElement].setAttribute('aria-hidden', 'true'); + root.appendChild(this[_containerElement]); + + this[_track] = new SwitchTrack(factory); + this[_containerElement].appendChild(this[_track].element); + this[_track].value = this.on; + + let thumbElement = this[_containerElement].appendChild(factory.createElement('span')); + thumbElement.id = 'thumb'; + thumbElement.part.add('thumb'); + + this[_rippleElement] = thumbElement.appendChild(factory.createElement('span')); + this[_rippleElement].id = 'ripple'; + + root.adoptedStyleSheets = [style.styleSheetFactory()()]; + } + + // TODO(tkent): Make this private. + _onClick(event) { + for (let element of this[_containerElement].querySelectorAll('*')) { + style.markTransition(element); + } + this.on = !this.on; + this.dispatchEvent(new Event('input', {bubbles: true})); + this.dispatchEvent(new Event('change', {bubbles: true})); + } + + // TODO(tkent): Make this private. + _onKeyPress(event) { + if (event.code == 'Space') { + // Do not scroll the page. + event.preventDefault(); + this._onClick(event); + } + } +} + +reflection.installBool(StdSwitchElement.prototype, STATE_ATTR); +reflection.installBool( + StdSwitchElement.prototype, 'default' + STATE_ATTR, + 'default' + STATE_ATTR.charAt(0).toUpperCase() + STATE_ATTR.substring(1)); +face.installPropertiesAndFunctions(StdSwitchElement.prototype, _internals); + +// This is necessary for anyObject.toString.call(switchInstance). +Object.defineProperty(StdSwitchElement.prototype, Symbol.toStringTag, { + configurable: true, + enumerable: false, + value: 'StdSwitchElement', + writable: false +}); + +customElements.define('std-switch', StdSwitchElement); +delete StdSwitchElement.formAssociated; +delete StdSwitchElement.observedAttributes; +delete StdSwitchElement.prototype.attributeChangedCallback; +delete StdSwitchElement.prototype.connectedCallback; diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/style.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/style.mjs new file mode 100644 index 00000000000..6b0bc2eca78 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/style.mjs @@ -0,0 +1,224 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Style constant values. +const COLOR_ON = '#0077FF'; +const TRACK_RADIUS = '13px'; +const TRACK_BORDER_WIDTH = '2px'; +const THUMB_HEIGHT = '22px'; +const THUMB_WIDTH = '22px'; +const THUMB_MARGIN_START = '2px'; +const THUMB_MARGIN_END = '2px'; + +// Returns a function returning a CSSStyleSheet(). +// TODO(tkent): Share this stylesheet factory feature with elements/toast/. +export function styleSheetFactory() { + let styleSheet; + return () => { + if (!styleSheet) { + styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(` +:host { + block-size: 26px; + border: none; + box-sizing: border-box; + display: inline-block; + inline-size: 54px; + user-select: none; + vertical-align: middle; +} + +#container { + align-items: center; + block-size: 100%; + display: inline-flex; + inline-size: 100%; +} + +#thumb { + background: white; + block-size: ${THUMB_HEIGHT}; + border-radius: calc(${THUMB_HEIGHT} / 2); + border: 1px solid black; + box-sizing: border-box; + display: inline-block; + margin-inline-start: calc(-100% + ${THUMB_MARGIN_START}); + inline-size: ${THUMB_WIDTH}; +} + +/* :host::part(thumb-transitioning) doesn't work. crbug.com/980506 */ +#thumb[part~="thumb-transitioning"] { + transition: all linear 0.1s; +} + +:host([on]) #thumb { + border: 1px solid ${COLOR_ON}; + margin-inline-start: calc(0px - ${THUMB_WIDTH} - ${THUMB_MARGIN_END}); +} + +#track { + block-size: 100%; + border-radius: ${TRACK_RADIUS}; + border: ${TRACK_BORDER_WIDTH} solid #dddddd; + box-shadow: 0 0 0 1px #f8f8f8; + box-sizing: border-box; + display: inline-block; + inline-size: 100%; + overflow: hidden; + padding: 0px; +} + +#trackFill { + background: ${COLOR_ON}; + block-size: 100%; + border-radius: calc(${TRACK_RADIUS}) - ${TRACK_BORDER_WIDTH}); + box-shadow: none; + box-sizing: border-box; + display: inline-block; + inline-size: 0%; + vertical-align: top; +} + +#trackFill[part~="track-fill-transitioning"] { + transition: all linear 0.1s; +} + +:host([on]) #track { + border: ${TRACK_BORDER_WIDTH} solid ${COLOR_ON}; +} + +:host(:focus) { + outline-offset: 4px; +} + +:host(:focus) #track { + box-shadow: 0 0 0 2px #f8f8f8; +} + +:host([on]:focus) #track { + box-shadow: 0 0 0 2px #dddddd; +} + +:host(:focus) #thumb { + border: 2px solid black; +} + +:host([on]:focus) #thumb { + border: 2px solid ${COLOR_ON}; +} + +:host(:not(:focus-visible):focus) { + outline: none; +} + +:host(:not(:disabled):hover) #thumb { + inline-size: 26px; +} + +:host([on]:not(:disabled):hover) #thumb { + margin-inline-start: calc(0px - 26px - ${THUMB_MARGIN_END}); +} + +:host(:active) #track { + background: #dddddd; +} + +:host([on]:active) #track { + border: 2px solid #77bbff; + box-shadow: 0 0 0 2px #f8f8f8; +} + +:host([on]:active) #trackFill { + background: #77bbff; +} + +:host(:disabled) { + opacity: 0.38; +} + +/* + * display:inline-block in the :host ruleset overrides 'hidden' handling + * by the user agent. + */ +:host([hidden]) { + display: none; +} + +`); + } + return styleSheet; + }; +} + +/** + * @param {!Element} element + */ +function setupTransitionCounter(element) { + if (element.runningTransitions !== undefined) { + return; + } + element.runningTransitions = 0; + element.addEventListener('transitionrun', e => { + if (e.target === element) { + ++element.runningTransitions; + } + }); + let handleEndOrCancel = e => { + // Need to check runningTransitions>0 due to superfluous transitioncancel + // events; crbug.com/979556. + if (e.target === element && element.runningTransitions > 0) { + --element.runningTransitions; + } + }; + element.addEventListener('transitionend', handleEndOrCancel); + element.addEventListener('transitioncancel', handleEndOrCancel); +} + +/** + * Add '$part-transitioning' part to the element, and remove it on 'transitionend' + * event or remove it immediately if the element has no transitions. + * + * TODO(tkent): We should apply custom state. + * + * @param {!Element} element + */ +export function markTransition(element) { + // Should check hasAttribute() to avoid creating a DOMTokenList instance. + if (!element.hasAttribute('part') || element.part.length < 1) { + return; + } + + setupTransitionCounter(element); + + const partName = element.part[0] + '-transitioning'; + if (element.part.contains(partName)) { + return; + } + // The style with partName might have transitions, and might have no + // transitions. We need to add partName anyway because we have no other + // ways to check existence of transitions. Then, we need to remove + // partName because state change without markTransition() should have + // style without partName. + // + // We add partName in an animation frame, and continue to request + // animation frames until runningTransitions becomes 0. If the style with + // partName has no transitions, runningTransitions keeps 0, and the second + // animation frame removes partName. + window.requestAnimationFrame(() => { + element.part.add(partName); + + // If the element has a transition, it must start on the rendering just + // after this rAF callback. So we check runningTransitions in the next + // frame. + const removeIfNoTransitions = () => { + // No transitions started, or all transitions were completed. + if (element.runningTransitions === 0) { + element.part.remove(partName); + } else { + window.requestAnimationFrame(removeIfNoTransitions); + } + }; + window.requestAnimationFrame(removeIfNoTransitions); + }); +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/track.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/track.mjs new file mode 100644 index 00000000000..aa8fc00c3d1 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/track.mjs @@ -0,0 +1,74 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Private property symbols +// TODO(tkent): Use private fields. +const _value = Symbol(); +const _trackElement = Symbol(); +const _fillElement = Symbol(); +const _slotElement = Symbol(); + +export class SwitchTrack { + + /** + * @param {!Document} factory A factory for elements created for this track. + */ + constructor(factory) { + this[_value] = false; + this._initializeDOM(factory); + } + + /** + * @return {!Element} + */ + get element() { + return this[_trackElement]; + } + + /** + * @param {Boolean} newValue + */ + set value(newValue) { + let oldValue = this[_value]; + this[_value] = Boolean(newValue); + + let bar = this[_fillElement]; + if (bar) { + bar.style.inlineSize = this[_value] ? '100%' : '0%'; + if (oldValue != this[_value]) { + this._addSlot(); + } + } + } + + // TODO(tkent): Use private fields. #initializeDOM = factory => { ...}; + /** + * @param {!Document} factory A factory for elements created for this track. + */ + _initializeDOM(factory) { + this[_trackElement] = factory.createElement('div'); + this[_trackElement].id = 'track'; + this[_trackElement].part.add('track'); + this[_fillElement] = factory.createElement('span'); + this[_fillElement].id = 'trackFill'; + this[_fillElement].part.add('track-fill'); + this[_trackElement].appendChild(this[_fillElement]); + this[_slotElement] = factory.createElement('slot'); + this._addSlot(); + } + + /** + * Add the <slot> + * - next to _fillElement if _value is true + * - as a child of _fillElement if _value is false + * This behavior is helpful to show text in the track. + */ + _addSlot() { + if (this[_value]) { + this[_fillElement].appendChild(this[_slotElement]); + } else { + this[_trackElement].appendChild(this[_slotElement]); + } + } +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/.eslintrc.js b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/.eslintrc.js new file mode 100644 index 00000000000..8f9a67630c4 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/.eslintrc.js @@ -0,0 +1,338 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module.exports = { + root: true, + env: { + es6: true, + browser: true + }, + 'parser': 'babel-eslint', + parserOptions: { + sourceType: 'module', + ecmaVersion: 2019 + }, + rules: { + 'for-direction': 'error', + 'getter-return': 'error', + 'no-async-promise-executor': 'error', + 'no-await-in-loop': 'error', + 'no-compare-neg-zero': 'error', + 'no-cond-assign': ['error', 'except-parens'], + 'no-console': 'error', + 'no-constant-condition': ['error', {checkLoops: false}], + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': 'error', + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-parens': [ + 'error', + 'all', + { + conditionalAssign: false, + nestedBinaryExpressions: false, + returnAssign: false + } + ], + 'no-extra-semi': 'error', + 'no-func-assign': 'error', + 'no-inner-declarations': 'off', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-misleading-character-class': 'error', + 'no-obj-calls': 'error', + 'no-prototype-builtins': 'error', + 'no-regex-spaces': 'error', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'off', + 'no-unsafe-negation': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'block-scoped-var': 'off', + 'class-methods-use-this': 'off', + 'complexity': 'off', + 'consistent-return': 'error', + 'curly': ['error', 'all'], + 'default-case': 'off', + 'dot-location': ['error', 'property'], + 'dot-notation': 'error', + 'eqeqeq': 'error', + 'guard-for-in': 'off', + 'no-alert': 'error', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-div-regex': 'off', + 'no-else-return': 'error', + 'no-empty-function': 'off', + 'no-empty-pattern': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-fallthrough': 'error', + 'no-floating-decimal': 'error', + 'no-global-assign': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-labels': ['error', {allowLoop: true}], + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-magic-numbers': ['error', {ignore: [0, 1]}], + 'no-multi-spaces': ['error', {ignoreEOLComments: true}], + 'no-multi-str': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'off', + 'no-process-env': 'error', + 'no-proto': 'error', + 'no-redeclare': 'error', + 'no-restricted-properties': 'off', + 'no-return-assign': ['error', 'except-parens'], + 'no-return-await': 'error', + 'no-script-url': 'off', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unused-expressions': 'error', + 'no-unused-labels': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-escape': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'no-warning-comments': 'off', + 'no-with': 'error', + 'prefer-promise-reject-errors': 'error', + 'radix': ['error', 'as-needed'], + 'require-await': 'off', + 'vars-on-top': 'off', + 'wrap-iife': ['error', 'outside'], + 'yoda': ['error', 'never'], + 'strict': ['error', 'global'], + 'init-declarations': 'off', + 'no-delete-var': 'error', + 'no-label-var': 'error', + 'no-restricted-globals': 'off', + 'no-shadow': 'error', + 'no-shadow-restricted-names': 'error', + 'no-undef': 'error', + 'no-undef-init': 'error', + 'no-undefined': 'off', + 'no-unused-vars': 'error', + 'no-use-before-define': ['error', 'nofunc'], + 'callback-return': 'off', + 'global-require': 'error', + 'handle-callback-err': 'error', + 'no-buffer-constructor': 'error', + 'no-mixed-requires': ['error', true], + 'no-new-require': 'error', + 'no-path-concat': 'error', + 'no-process-exit': 'error', + 'no-restricted-modules': 'off', + 'no-sync': 'off', + 'array-bracket-newline': ['error', {multiline: true}], + 'array-bracket-spacing': ['error', 'never'], + 'array-element-newline': 'off', + 'block-spacing': ['error', 'always'], + 'brace-style': [ + 'error', + '1tbs', + {allowSingleLine: false} + ], + camelcase: ['error', {properties: 'always'}], + 'capitalized-comments': 'off', + 'comma-dangle': ['error', 'always-multiline'], + 'comma-spacing': [ + 'error', + { + before: false, + after: true + } + ], + 'comma-style': ['error', 'last'], + 'computed-property-spacing': ['error', 'never'], + 'consistent-this': 'off', + 'eol-last': 'error', + 'func-call-spacing': ['error', 'never'], + 'func-name-matching': 'error', + 'func-names': 'off', + 'func-style': ['error', 'declaration'], + 'function-paren-newline': 'off', + 'id-blacklist': 'off', + 'id-length': 'off', + 'id-match': 'off', + indent: 'off', // not really compatible with clang-format + 'jsx-quotes': 'off', + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true, + mode: 'strict' + } + ], + 'keyword-spacing': [ + 'error', + { + before: true, + after: true + } + ], + 'line-comment-position': 'off', + 'linebreak-style': ['error', 'unix'], + 'lines-around-comment': 'off', + 'max-depth': 'off', + 'max-len': 'off', + 'max-lines': 'off', + 'max-nested-callbacks': 'off', + 'max-params': 'off', + 'max-statements': 'off', + 'max-statements-per-line': ['error', {max: 1}], + 'multiline-ternary': ['error', 'always-multiline'], + 'new-cap': 'error', + 'new-parens': 'error', + 'newline-per-chained-call': 'off', + 'no-array-constructor': 'error', + 'no-bitwise': 'off', + 'no-continue': 'off', + 'no-inline-comments': 'off', + 'no-lonely-if': 'error', + 'no-mixed-operators': [ + 'error', + { + groups: [ + ['&', '|', '^', '~', '<<', '>>', '>>>'], + ['==', '!=', '===', '!==', '>', '>=', '<', '<='], + ['&&', '||'], + ['in', 'instanceof'] + ] + } + ], + 'no-mixed-spaces-and-tabs': 'error', + 'no-multi-assign': 'off', + 'no-multiple-empty-lines': 'error', + 'no-negated-condition': 'off', + 'no-nested-ternary': 'error', + 'no-new-object': 'error', + 'no-plusplus': 'off', + 'no-restricted-syntax': 'off', + 'no-tabs': 'error', + 'no-ternary': 'off', + 'no-trailing-spaces': 'error', + 'no-underscore-dangle': 'off', + 'no-unneeded-ternary': 'error', + 'no-whitespace-before-property': 'error', + 'nonblock-statement-body-position': 'error', + 'object-curly-newline': ['error', {consistent: true}], + 'object-curly-spacing': ['error', 'never'], + 'object-property-newline': 'off', + 'one-var': ['error', 'never'], + 'one-var-declaration-per-line': ['error', 'initializations'], + 'operator-assignment': ['error', 'always'], + 'operator-linebreak': ['error', 'after'], + 'padded-blocks': ['error', 'never'], + 'padding-line-between-statements': 'off', + 'quote-props': ['error', 'as-needed'], + quotes: [ + 'error', + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true + } + ], + semi: ['error', 'always'], + 'semi-spacing': 'error', + 'semi-style': 'error', + 'sort-keys': 'off', + 'sort-vars': 'off', + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': [ + 'error', + { + anonymous: 'always', + named: 'never' + } + ], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': [ + 'error', + { + words: true, + nonwords: false + } + ], + 'spaced-comment': ['error', 'always'], + 'switch-colon-spacing': 'error', + 'template-tag-spacing': 'error', + 'unicode-bom': 'error', + 'wrap-regex': 'off', + 'arrow-body-style': 'off', + 'arrow-parens': ['error', 'as-needed'], + 'arrow-spacing': 'error', + 'constructor-super': 'error', + 'generator-star-spacing': ['error', 'neither'], + 'no-class-assign': 'error', + 'no-confusing-arrow': 'off', + 'no-const-assign': 'error', + 'no-dupe-class-members': 'error', + 'no-duplicate-imports': 'error', + 'no-new-symbol': 'error', + 'no-restricted-imports': 'off', + 'no-this-before-super': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': ['error', {ignoreReadBeforeAssign: true}], + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true + }, + AssignmentExpression: { + array: false, + object: false + } + }, + { + enforceForRenamedProperties: false + } + ], + 'prefer-numeric-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'off', + 'require-yield': 'error', + 'rest-spread-spacing': 'error', + 'sort-imports': 'off', + 'symbol-description': 'error', + 'template-curly-spacing': ['error', 'never'], + 'yield-star-spacing': ['error', 'after'] + } +}; diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/OWNERS b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/OWNERS new file mode 100644 index 00000000000..d76cdfa11f0 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/OWNERS @@ -0,0 +1 @@ +fergal@chromium.org diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/index.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/index.mjs new file mode 100644 index 00000000000..69232f8475a --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/toast/index.mjs @@ -0,0 +1,280 @@ +/** + * Copyright 2019 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * @fileoverview This file defines the class for the Standard Toast LAPI + * and the accompanying showToast() function. + * EXPLAINER: https://github.com/jackbsteinberg/std-toast + * TEST PATH: /chromium/src/third_party/blink/web_tests/external/wpt/std-toast/* + * @package + */ + +import * as reflection from '../internal/reflection.mjs'; + +const DEFAULT_DURATION = 3000; +const TYPES = new Set(['success', 'warning', 'error']); + +function stylesheetFactory() { + let stylesheet; + return function generate() { + if (!stylesheet) { + stylesheet = new CSSStyleSheet(); + stylesheet.replaceSync(` + :host { + position: fixed; + bottom: 1em; + right: 1em; + border: solid; + padding: 1em; + background: white; + color: black; + z-index: 1; + } + + :host(:not([open])) { + display: none; + } + + .default-closebutton { + user-select: none; + } + + :host([type=success i]) { + border-color: green; + } + + :host([type=warning i]) { + border-color: orange; + } + + :host([type=error i]) { + border-color: red; + } + `); + // TODO(jacksteinberg): use offset-block-end: / offset-inline-end: over bottom: / right: + // when implemented https://bugs.chromium.org/p/chromium/issues/detail?id=538475 + } + return stylesheet; + }; +} + +const generateStylesheet = stylesheetFactory(); + +export class StdToastElement extends HTMLElement { + static observedAttributes = ['open', 'closebutton']; + #shadow = this.attachShadow({mode: 'closed'}); + #timeoutID; + #actionSlot; + #closeButtonElement; + #setCloseTimeout = duration => { + clearTimeout(this.#timeoutID); + + if (duration === Infinity) { + this.#timeoutID = null; + } else { + this.#timeoutID = setTimeout(() => { + this.removeAttribute('open'); + }, duration); + } + }; + + constructor(message) { + super(); + + this.#shadow.adoptedStyleSheets = [generateStylesheet()]; + + this.#shadow.appendChild(document.createElement('slot')); + + this.#actionSlot = document.createElement('slot'); + this.#actionSlot.setAttribute('name', 'action'); + this.#shadow.appendChild(this.#actionSlot); + + this.#closeButtonElement = document.createElement('button'); + this.#closeButtonElement.setAttribute('part', 'closebutton'); + setDefaultCloseButton(this.#closeButtonElement); + this.#shadow.appendChild(this.#closeButtonElement); + + this.#closeButtonElement.addEventListener('click', () => { + this.hide(); + }); + + if (message !== undefined) { + this.textContent = message; + } + } + + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'status'); + } + // TODO(jacksteinberg): use https://github.com/whatwg/html/pull/4658 when implemented + } + + get action() { + return this.#actionSlot.assignedNodes().length !== 0 ? + this.#actionSlot.assignedNodes()[0] : + null; + } + + set action(val) { + const previousAction = this.action; + if (val !== null) { + if (!isElement(val)) { + throw new TypeError('Invalid argument: must be type Element'); + } + + val.setAttribute('slot', 'action'); + this.insertBefore(val, previousAction); + } + + if (previousAction !== null) { + previousAction.remove(); + } + } + + get closeButton() { + if (this.hasAttribute('closebutton')) { + const closeAttr = this.getAttribute('closebutton'); + return closeAttr === '' ? true : closeAttr; + } + return false; + } + + set closeButton(val) { + if (val === true) { + this.setAttribute('closebutton', ''); + } else if (val === false) { + this.removeAttribute('closebutton'); + } else { + this.setAttribute('closebutton', val); + } + } + + get type() { + const typeAttr = this.getAttribute('type'); + if (typeAttr === null) { + return ''; + } + + const typeAttrLower = typeAttr.toLowerCase(); + + if (TYPES.has(typeAttrLower)) { + return typeAttrLower; + } + + return ''; + } + + set type(val) { + this.setAttribute('type', val); + } + + show({duration = DEFAULT_DURATION} = {}) { + if (duration <= 0) { + throw new RangeError(`Invalid Argument: duration must be greater than 0 [${duration} given]`); + } + + this.setAttribute('open', ''); + this.#setCloseTimeout(duration); + } + + hide() { + this.removeAttribute('open'); + } + + toggle(force) { + this.toggleAttribute('open', force); + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'open': + if (newValue !== null && oldValue === null) { + this.dispatchEvent(new Event('show')); + } else if (newValue === null) { + this.dispatchEvent(new Event('hide')); + this.#setCloseTimeout(Infinity); + } + break; + case 'closebutton': + if (newValue !== null) { + if (newValue === '') { + setDefaultCloseButton(this.#closeButtonElement); + } else { + replaceDefaultCloseButton(this.#closeButtonElement, newValue); + } + } + // if newValue === null we do nothing, since CSS will hide the button + break; + } + } +} + +reflection.installBool(StdToastElement.prototype, 'open'); + +customElements.define('std-toast', StdToastElement); + +delete StdToastElement.prototype.attributeChangedCallback; +delete StdToastElement.prototype.observedAttributes; +delete StdToastElement.prototype.connectedCallback; + +export function showToast(message, options = {}) { + const toast = new StdToastElement(message); + + const { + action, + closeButton, + type, + ...showOptions + } = options; + + if (isElement(action)) { + toast.action = action; + } else if (action !== undefined) { + const actionButton = document.createElement('button'); + + // Unlike String(), this performs the desired JavaScript ToString operation. + // https://gist.github.com/domenic/82adbe7edc4a33a70f42f255479cec39 + actionButton.textContent = `${action}`; + + actionButton.setAttribute('slot', 'action'); + toast.appendChild(actionButton); + } + + if (closeButton !== undefined) { + toast.closeButton = closeButton; + } + + if (type !== undefined) { + toast.type = type; + } + + document.body.append(toast); + toast.show(showOptions); + + return toast; +} + +const idGetter = + Object.getOwnPropertyDescriptor(Element.prototype, 'id').get; +function isElement(value) { + try { + idGetter.call(value); + return true; + } catch { + return false; + } +} + +function setDefaultCloseButton(closeButton) { + closeButton.setAttribute('aria-label', 'close'); + closeButton.setAttribute('class', 'default-closebutton'); + closeButton.textContent = 'Ă—'; +} + +function replaceDefaultCloseButton(closeButton, value) { + closeButton.textContent = value; + closeButton.removeAttribute('aria-label'); + closeButton.removeAttribute('class'); +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/.eslintrc.js b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/.eslintrc.js new file mode 100644 index 00000000000..90e626a41fc --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/.eslintrc.js @@ -0,0 +1,337 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module.exports = { + root: true, + env: { + es6: true, + browser: true + }, + 'parser': 'babel-eslint', + parserOptions: { + sourceType: 'module', + ecmaVersion: 2019 + }, + rules: { + 'for-direction': 'error', + 'getter-return': 'error', + 'no-async-promise-executor': 'error', + 'no-await-in-loop': 'error', + 'no-compare-neg-zero': 'error', + 'no-cond-assign': ['error', 'except-parens'], + 'no-console': 'error', + 'no-constant-condition': ['error', {checkLoops: false}], + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': 'error', + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-parens': [ + 'error', + 'all', + { + conditionalAssign: false, + nestedBinaryExpressions: false, + returnAssign: false + } + ], + 'no-extra-semi': 'error', + 'no-func-assign': 'error', + 'no-inner-declarations': 'off', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-misleading-character-class': 'error', + 'no-obj-calls': 'error', + 'no-prototype-builtins': 'error', + 'no-regex-spaces': 'error', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'off', + 'no-unsafe-negation': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'block-scoped-var': 'off', + 'class-methods-use-this': 'off', + 'complexity': 'off', + 'consistent-return': 'error', + 'curly': ['error', 'all'], + 'default-case': 'off', + 'dot-location': ['error', 'property'], + 'dot-notation': 'error', + 'eqeqeq': 'error', + 'guard-for-in': 'off', + 'no-alert': 'error', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-div-regex': 'off', + 'no-empty-function': 'off', + 'no-empty-pattern': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-fallthrough': 'error', + 'no-floating-decimal': 'error', + 'no-global-assign': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-iterator': 'error', + 'no-labels': ['error', {allowLoop: true}], + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-magic-numbers': ['error', {ignore: [0, 1, 2]}], + 'no-multi-spaces': ['error', {ignoreEOLComments: true}], + 'no-multi-str': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'off', + 'no-process-env': 'error', + 'no-proto': 'error', + 'no-redeclare': 'error', + 'no-restricted-properties': 'off', + 'no-return-assign': ['error', 'except-parens'], + 'no-return-await': 'error', + 'no-script-url': 'off', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unused-expressions': 'error', + 'no-unused-labels': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-escape': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'no-warning-comments': 'off', + 'no-with': 'error', + 'prefer-promise-reject-errors': 'error', + 'radix': ['error', 'as-needed'], + 'require-await': 'off', + 'vars-on-top': 'off', + 'wrap-iife': ['error', 'outside'], + 'yoda': ['error', 'never'], + 'strict': ['error', 'global'], + 'init-declarations': 'off', + 'no-delete-var': 'error', + 'no-label-var': 'error', + 'no-restricted-globals': 'off', + 'no-shadow': 'error', + 'no-shadow-restricted-names': 'error', + 'no-undef': 'error', + 'no-undef-init': 'error', + 'no-undefined': 'off', + 'no-unused-vars': 'error', + 'no-use-before-define': ['error', 'nofunc'], + 'callback-return': 'off', + 'global-require': 'error', + 'handle-callback-err': 'error', + 'no-buffer-constructor': 'error', + 'no-mixed-requires': ['error', true], + 'no-new-require': 'error', + 'no-path-concat': 'error', + 'no-process-exit': 'error', + 'no-restricted-modules': 'off', + 'no-sync': 'off', + 'array-bracket-newline': ['error', {multiline: true}], + 'array-bracket-spacing': ['error', 'never'], + 'array-element-newline': 'off', + 'block-spacing': ['error', 'always'], + 'brace-style': [ + 'error', + '1tbs', + {allowSingleLine: false} + ], + camelcase: ['error', {properties: 'always'}], + 'capitalized-comments': 'off', + 'comma-dangle': ['error', 'always-multiline'], + 'comma-spacing': [ + 'error', + { + before: false, + after: true + } + ], + 'comma-style': ['error', 'last'], + 'computed-property-spacing': ['error', 'never'], + 'consistent-this': 'off', + 'eol-last': 'error', + 'func-call-spacing': ['error', 'never'], + 'func-name-matching': 'error', + 'func-names': 'off', + 'func-style': ['error', 'declaration'], + 'function-paren-newline': 'off', + 'id-blacklist': 'off', + 'id-length': 'off', + 'id-match': 'off', + indent: 'off', // not really compatible with clang-format + 'jsx-quotes': 'off', + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true, + mode: 'strict' + } + ], + 'keyword-spacing': [ + 'error', + { + before: true, + after: true + } + ], + 'line-comment-position': 'off', + 'linebreak-style': ['error', 'unix'], + 'lines-around-comment': 'off', + 'max-depth': 'off', + 'max-len': ['error', { + tabWidth: 2, + ignorePattern: "(^import |// eslint-disable-line )"}], + 'max-lines': 'off', + 'max-nested-callbacks': 'off', + 'max-params': 'off', + 'max-statements': 'off', + 'max-statements-per-line': ['error', {max: 1}], + 'multiline-ternary': ['error', 'always-multiline'], + 'new-cap': 'error', + 'new-parens': 'error', + 'newline-per-chained-call': 'off', + 'no-array-constructor': 'error', + 'no-bitwise': 'off', + 'no-continue': 'off', + 'no-inline-comments': 'off', + 'no-mixed-operators': [ + 'error', + { + groups: [ + ['&', '|', '^', '~', '<<', '>>', '>>>'], + ['==', '!=', '===', '!==', '>', '>=', '<', '<='], + ['&&', '||'], + ['in', 'instanceof'] + ] + } + ], + 'no-mixed-spaces-and-tabs': 'error', + 'no-multi-assign': 'off', + 'no-multiple-empty-lines': 'error', + 'no-negated-condition': 'off', + 'no-nested-ternary': 'error', + 'no-new-object': 'error', + 'no-plusplus': 'off', + 'no-restricted-syntax': 'off', + 'no-tabs': 'error', + 'no-ternary': 'off', + 'no-trailing-spaces': 'error', + 'no-underscore-dangle': 'off', + 'no-unneeded-ternary': 'error', + 'no-whitespace-before-property': 'error', + 'nonblock-statement-body-position': 'error', + 'object-curly-newline': ['error', {consistent: true}], + 'object-curly-spacing': ['error', 'never'], + 'object-property-newline': 'off', + 'one-var': ['error', 'never'], + 'one-var-declaration-per-line': ['error', 'initializations'], + 'operator-assignment': ['error', 'always'], + 'operator-linebreak': ['error', 'after'], + 'padded-blocks': ['error', 'never'], + 'padding-line-between-statements': 'off', + 'quote-props': ['error', 'as-needed'], + quotes: [ + 'error', + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true + } + ], + semi: ['error', 'always'], + 'semi-spacing': 'error', + 'semi-style': 'error', + 'sort-keys': 'off', + 'sort-vars': 'off', + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': [ + 'error', + { + anonymous: 'always', + named: 'never' + } + ], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': [ + 'error', + { + words: true, + nonwords: false + } + ], + 'spaced-comment': ['error', 'always'], + 'switch-colon-spacing': 'error', + 'template-tag-spacing': 'error', + 'unicode-bom': 'error', + 'wrap-regex': 'off', + 'arrow-body-style': 'off', + 'arrow-parens': ['error', 'as-needed'], + 'arrow-spacing': 'error', + 'constructor-super': 'error', + 'generator-star-spacing': ['error', 'neither'], + 'no-class-assign': 'error', + 'no-confusing-arrow': 'off', + 'no-const-assign': 'error', + 'no-dupe-class-members': 'error', + 'no-duplicate-imports': 'error', + 'no-new-symbol': 'error', + 'no-restricted-imports': 'off', + 'no-this-before-super': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': ['error', {ignoreReadBeforeAssign: true}], + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true + }, + AssignmentExpression: { + array: false, + object: false + } + }, + { + enforceForRenamedProperties: false + } + ], + 'prefer-numeric-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'off', + 'require-yield': 'error', + 'rest-spread-spacing': 'error', + 'sort-imports': 'off', + 'symbol-description': 'error', + 'template-curly-spacing': ['error', 'never'], + 'yield-star-spacing': ['error', 'after'] + } +}; diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/OWNERS b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/OWNERS new file mode 100644 index 00000000000..d76cdfa11f0 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/OWNERS @@ -0,0 +1 @@ +fergal@chromium.org diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/README.md b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/README.md new file mode 100644 index 00000000000..c5ea6fc86c3 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/README.md @@ -0,0 +1,297 @@ +# Virtual Scroller Notes + +This is Blink's implementation of the [`<virtual-scroller>`](https://github.com/WICG/virtual-scroller) element. + +## Status + +This is a prototype with many known issues. + +## Principles + +- Avoid operations that are O(number of children) +- Ensure that we only perform operations that cause DOM, style or rendering work in the browser + that is O(number of visible elements). +- Avoid forcing layout from JS. + +## Current Implementation Strategy + +The prototype uses a custom element implemented in JS. +It only handles vertical block layout, +so this talks only about height. +This custom element manages the display-locking status of its child items. +It considers a range of pixels to be the "unlocked range". +This range is determined from the window size. + +TODO(crbug.com/983052): Be smarter about determining the buffer size, +given that the window can be resized. + +Items which are within the unlocked range are left unlocked. +All other items are locked +so that they do not incur style and rendering costs for their contents. +They are locked with a height that is our best guess at their rendered height. +This guess is based on +- a fixed default +- a previously measured height of this item +- an average of previous observed heights of items + +The virtual scroller listens to observers to know when it needs to reconsider which items should be locked. +It keeps a resize observer on itself since if it is resized, +items may need to be newly locked or unlocked. +It keeps an intersection observer on all unlocked child items. +Along with the buffer around the screen, +this allows it to know when the state has changed such that we may need to lock or unlock different items. + +TODO(crbug.com/983050): Observe one item above and below the unlocked items too, +without this, if the edge item is larger than the window, +we can scroll empty space into the visible region. + +TODO(crbug.com/983046): Keep an intersection observer on the scroller. +This allows us to know whether the scroller is offscreen. +In that case we lock all elements +and pause all activity. +This includes if the scroller is contained in a locked element +(e.g. nested in another virtual scroller). + +The virtual scroller keeps a mutation observer on itself +so that it can react to elements being added, removed or moved around. +When dealing with mutations, +elements which are newly added are treated differently +to elements which are removed and re-added. +Newly added elements are immediately locked. +This allows a large number of elements to be added, +without a large rendering/style cost for these elements. + +The virtual scroller does not listen directly for scroll events +and does not know or care whether it is in a scrollable region. +It is tempting to try discover whether the scroller is contained in a scrolling region +and then listen for scroll events, +however the scroller may be contained in any number of nested scrollable regions +and DOM changes can cause it to be reparented. +Also, we only need to change state when an item enters or leaves the visible range +but scroll events may occur much more frequently than that. + +The virtual scroller takes no action directly in response to these events. +It simply ensures that a `requestAnimationFrame` (RAF) callback will run this frame +(max one per frame). +This callback is the "sync" callback. +It will attempt to react to the new state of the world +and try to ensure that the scroller is in sync with it. +This is where we determine which elements to lock or unlock. + +When determining which items should be locked and which should be unlocked, +the virtual scroller uses `getBoundingClientRect` +to get the coordinates of its items, +relative to the root viewport. +It binary searches on the array of items +to find the element at the top and bottom of the visible range. +At the end of the sync, +these and the elements in between them will be unlocked +and all other elements will be locked. +The scroller knows which elements are currently unlocked +and does the minimal amount of locking and unlocking +to bring it to the desired state. +Also the intersection observers will be updated to match this range of elements. + +It is very possible that the range of elements we have unlocked +is too big or too small for the viewport. +We cannot know the rendered size of these elements +until we have unlocked and measured them. +In order to avoid extra forced layouts, +we simply queue another sync callback for the next frame. +We continue to queue sync callbacks until one of them makes no changes. + +Internally, the virtual scroller slots all children into a single slot. +It changes the locked state of the light tree children directly. + +## Known issues specific to the current prototype + +### Display locking items directly + +The current prototype changes the locked state of the light tree children directly. +This means that the scroller's actions can be detected from outside. +It also means that an author cannot (in general) safely use display locking +on the children of a virtual scroller. + +Other approaches do not have this issue, +because they slot the items into more than one slot, +however, this requires either manipulating the `slot` attribute on the items +or [imperative slotting](https://www.chromestatus.com/feature/5711021289242624). + +### Binary searching the items + +The first problem with this is that it requires forced layout (see below). + +The second (potential) problem is that +while JS presents an `Array` interface for the children of the virtual scroller, +the reality is that in Blink, +the elements are stored in a linked list +with some extra logic to remember the most recent indexed access +and use that to speed up subsequent index accesses +(e.g. accessing `[n+1]` after `[n]` is fast). +This makes the binary search actually O(number of children) +because the C++ code must traverse a linked list. +However C++ traversing a link list is very fast +so this tends not to be noticeable. + +### Locked elements still have a cost + +While the descendants of locked elements are skipped for style and layout, +the locked elements themselves are still traversed +and participate in style and layout. +In the scroller, +the elements are locked with a size +and behave just as an empty div with that size. +This means that when we add many children to the virtual scroller, +even though most of them are locked, +there can still be quite a large style and rendering cost +from the locked elements in aggregate. + +## Known issues common to many approaches + +### Intersection observers are slow + +Intersection observers signal the intersection state *after* it has happened. +This means that by only reacting to intersection observers, +the scroller may react late to scrolls and jumps. +Once a scroll begins, +it's likely that the element will keep up, +since it is not using the details of the events to compute the state of the scroller. + +### Forced layouts to measure item sizes + +After unlocking, the scroller needs to know the rendered size of an element. +In order to do that it must call `getBoundingClientRect`. +This forces the browser to have clean style and layout +for all unlocked elements in the document +(whether this is expensive depends on what has changed since the last time layout). +It may be better to perform these measurements in a +[post animation frame callback](https://github.com/WICG/requestPostAnimationFrame/blob/master/explainer.md) +when that feature becomes available +because this callback guarantees that style and layout are clean, +so measuring elements should be inexpensive. + +## Alternative approaches + +### Unlocked tree of divs with `<slot>`s for leaves (rejected) + +We could avoid the O(number of children) run of locked divs +by building a tree of divs (e.g. a binary tree) +and locking all of the slots except those containing the visible items. + +*PROBLEM:* This breaks margin collapsing between items +and maybe other layout or style features. +In order to make margin collapsing work correctly +we would need to make all of the divs in this tree have style `display: contents` +but for layout, this would have exactly the same performance problem as placing them all as siblings in one slot. + +### One visible slot in the middle (rejected) + +We could assign all of the visible items to a single slot, +ensuring they behave correctly with respect to margin collapsing +and keep all other items in other slots. + +We need to ensure that elements are slotted in their light-tree order +for find-in-page, accessibility etc to work correctly. +The simplest form of this is a three-slot approach +with one slot for the invisible items at the top, +another slot for the visible items in the middle +and a third slot for the invisible items at the bottom. +The top and bottom slot remain locked always +and items are moved into the visible slot when they should be visible. +The top and bottom slots are inside a locked div. +These divs are locked with a size estimated to be the correct aggregate size for their slotted elements. + +*PROBLEM:* Gradually scrolling down moves items one at a time +from the bottom slot, to the visible slot and then into the top slot +but long-range jumps require moving O(number of children) items between slots. +E.g. if the top items are visible and we jump to make the bottom items visible, +then we have to assign almost all of the items that were in the bottom slot to the top slot. + +### Roaming visible slot (not preferred) + +This is similar to [One visible slot in the middle](#One-visible-slot-in-the-middle) approach +but rather than a top and bottom slot, +item assignment is divided over many slots. +Each item has a natural "leaf" slot that it should be assigned to. +All of the leaf slots are kept locked. +There is a single visible slot that contains the currently unlocked items +and it roams around between the leaf slots +to ensure that order is correct for find-in-page etc. +If we keep the number of items per slot low, +then long jumps never cause a large number of items to be reassigned, +at worst we need to reassign the currently visible items back to their leaf slots, +move the visible slot to a new place +and fill it with newly visible items. + +It's possible that there will be so many slots +that once again we run into the problem of large numbers of empty divs. +So we may need to maintain a tree of divs and `<slot>`s. +Most of this tree would be locked +but the path from the visible slot to the root would be unlocked. + +It looks like this: + +![Diagram of a sample roaming visible slot tree](roaming-slot.png) + +Original diagram [here](https://docs.google.com/drawings/d/1oZ8U16GzkxO3GaYLDygPsrmXiHO7SLNFXvVUK6WkG4I/edit) + +*CHALLENGES:* This approach does not seem to have any insurmountable problems, +however it is not simple: +- We need to ensure that the tree remains balanced in the face of mutations of the virtual scrollers contents. +- A tree may introduce a large number of tree-only elements. + With a binary tree, this would be equal to the number of items in the scroller. + It might be worth using a larger branching factor, to reduce this cost. + E.g. in a 10-way tree the overhead is only 1/9 the number of leaves. +- There is a lot of re-slotting of items. +- The visible slot may contain a large number of elements (if elements are small) + and Blink has some [optimizations](https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/html_slot_element.cc?q=symbol:HTMLSlotElement::NotifySlottedNodesOfFlatTreeChange) + that only work when slots contain <= 16 elements. +- Adding slots and/or assigning elements to slots currently involves operations that are O(number of children). + Adding N slots can take O(N^2) time. + This was previously not a concern in Blink + because, in general, the style and layout costs dominated. + However display locking eliminates these costs, + leaving the slotting costs as the bottleneck when there are large numbers of items. + +### One slot per item (most promising alternative) + +This is similar to [Roaming visible slot](#Roaming-visible-slot). +We have a tree of divs with slots as leaves +but in this approach we have exactly one slot per item. +For any item that should be unlocked, +we ensure that all of its tree ancestors are unlocked +and have style `display: contents`. +All other nodes in the tree have style `display: block; contain: style layout`, +to allow them to be locked. + +This has the effect of making all of the visible items siblings, +from the perspective of layout, +so that margin collapsing, etc. works correctly. +It also ensures that not only are the invisible items and tree divs locked +but they are *inside* locked ancestors, +so do not incur *any* costs. + +There is no need to maintain spacer divs above and below the visible region. +Instead, when we lock any tree element, +we set its locked size to be its current visible size. +If its contents don't change then this element has the correct aggregate size for its contents. +If this element's parent is unlocked then this correct size will be used in sizing the parent. + +Mutations to the tree require size updates to propagate up the ancestor chain. +E.g. inserting an element causes all of the elements ancestors to grow by our best guess at its size. +So, the initial state of the tree +(which comes from an initial insert) +is that everything is an estimate or sum of estimates, +with the sizes getting more accurate +as more elements are rendered, measured and re-locked with correct sizes. + +It's likely that the sizes above will be off by some amount due to margins +but they should be good enough for maintaining a usable scrollbar. + +*CHALLENGES:* This approach does not seem to have any insurmountable problems, +however it is not so simple. +It has a subset of the challenges of [Roaming visible slot](#Roaming-visible-slot): + +- Needs a balanced tree. +- Possible large number of tree-only elements. +- Slot performance issues. diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/roaming-slot.png b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/roaming-slot.png Binary files differnew file mode 100644 index 00000000000..2f9b6844694 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/docs/roaming-slot.png diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs new file mode 100644 index 00000000000..c25edc0a7fd --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs @@ -0,0 +1,104 @@ +/** + * Copyright 2019 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * @fileoverview Utilities for binary searching by layed-out pixed offset in a + * list of elements. + * @package + */ + +/** Symbols for use with @see findElement */ +export const BIAS_LOW = Symbol('BIAS_LOW'); +export const BIAS_HIGH = Symbol('BIAS_HIGH'); + +function getBound(elements, edgeIndex) { + const element = elements[Math.floor(edgeIndex / 2)]; + const rect = element.getBoundingClientRect(); + return edgeIndex % 2 ? rect.bottom : rect.top; +} + +/** + * Does the actual work of binary searching. This searches amongst the 2*N edges + * of the N elements. Returns the index of an edge found, 2i is the low edge of + * the ith element, 2i+1 is the high edge of the ith element. If |bias| is low + * then we find the index of the lowest edge >= offset. Otherwise we find index + * of the highest edge > offset. + */ +function findEdgeIndex(elements, offset, bias) { + let low = 0; + let high = elements.length * 2 - 1; + while (low < high) { + const i = Math.floor((low + high) / 2); + const bound = getBound(elements, i); + if (bias === BIAS_LOW) { + if (bound < offset) { + low = i + 1; + } else { + high = i; + } + } else { + if (offset < bound) { + high = i; + } else { + low = i + 1; + } + } + } + return low; +} + +/** + * Binary searches inside the array |elements| to find an element containing or + * nearest to |offset| (based on @see Element#getBoundingClientRect()). Assumes + * that the elements are already sorted in increasing pixel order. |bias| + * controls what happens if |offset| is not contained within any element or if + * |offset| is contained with 2 elements (this only happens if there is no + * margin between the elements). If |bias| is BIAS_LOW, then this selects the + * lower element nearest |offset|, otherwise it selects the higher element. + * + * Returns null if |offset| is not within any element. + * + * @param {!Element[]} elements An array of Elements in display order, + * i.e. the pixel offsets of later element are higher than those of earlier + * elements. + * @param {!number} offset The target offset in pixels to search for. + * @param {!Symbol} bias Controls whether we prefer a higher or lower element + * when there is a choice between two elements. + */ +export function findElement(elements, offset, bias) { + if (elements.length === 0) { + return null; + } + // Check if the offset is outside the range entirely. + if (offset < getBound(elements, 0) || + offset > getBound(elements, elements.length * 2 - 1)) { + return null; + } + + let edgeIndex = findEdgeIndex(elements, offset, bias); + + // Fix up edge cases. + if (bias === BIAS_LOW) { + // bound(0)..bound(edgeIndex) < offset <= bound(edgeIndex+1) ... + // If we bias low and we got a low edge and we weren't exactly on the edge + // then we want to select the element that's lower. + if (edgeIndex % 2 === 0) { + const bound = getBound(elements, edgeIndex); + if (offset < bound) { + edgeIndex--; + } + } + } else { + // bound(0)..bound(edgeIndex - 1) <= offset < bound(edgeIndex) ... + // If we bias high and we got a low edge, we need to check if we were + // exactly on the edge of the previous element. + if (edgeIndex % 2 === 0) { + const bound = getBound(elements, edgeIndex - 1); + if (offset === bound) { + edgeIndex--; + } + } + } + return elements[Math.floor(edgeIndex / 2)]; +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/index.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/index.mjs new file mode 100644 index 00000000000..daef60f7232 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/index.mjs @@ -0,0 +1,57 @@ +/** + * Copyright 2019 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * @fileoverview This file defines virtual-scroller element. + * EXPLAINER: https://github.com/fergald/virtual-scroller/blob/master/README.md + * TEST PATH: third_party/blink/web_tests/http/tests/virtual-scroller/* + * third_party/blink/web_tests/wpt_internal/virtual-scroller/* + * @package + */ +import {VisibilityManager} from './visibility-manager.mjs'; + +function styleSheetFactory() { + let styleSheet; + return () => { + if (!styleSheet) { + styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(` +:host { + display: block; +} + +::slotted(*) { + display: block !important; + contain: layout style; +} +`); + } + return styleSheet; + }; +} + +/** + * The class backing the virtual-scroller custom element. + */ +export class VirtualScrollerElement extends HTMLElement { + constructor() { + super(); + + const shadowRoot = this.attachShadow({mode: 'closed'}); + shadowRoot.adoptedStyleSheets = [styleSheetFactory()()]; + shadowRoot.appendChild(document.createElement('slot')); + + const visibilityManager = new VisibilityManager(this.children); + + new ResizeObserver(() => { + visibilityManager.scheduleSync(); + }).observe(this); + + new MutationObserver(records => { + visibilityManager.applyMutationObserverRecords(records); + }).observe(this, {childList: true}); + } +} + +customElements.define('virtual-scroller', VirtualScrollerElement); diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/sets.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/sets.mjs new file mode 100644 index 00000000000..9b1aff8eca1 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/sets.mjs @@ -0,0 +1,24 @@ +/** + * Copyright 2019 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * @fileoverview Utility functions for set operations. + * @package + */ + +/* + * Returns the set of elements in |a| that are not in |b|. + * + * @param {!Set} a A set of elements. + * @param {!Set} b A set of elements. +*/ +export function difference(a, b) { + const result = new Set(); + for (const element of a) { + if (!b.has(element)) { + result.add(element); + } + } + return result; +} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/visibility-manager.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/visibility-manager.mjs new file mode 100644 index 00000000000..ecc160f9f0d --- /dev/null +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/visibility-manager.mjs @@ -0,0 +1,407 @@ +/** + * Copyright 2019 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * @fileoverview This file provides the class backing the virtual-scroller + * element. + * @package + */ +import * as sets from './sets.mjs'; +import * as findElement from './find-element.mjs'; + + +// This controls how much above and below the current screen we +// reveal, e.g. 1 = 1 screen of content. +const BUFFER = 0.2; +// When we know about the heights of elements we default this height. +const DEFAULT_HEIGHT_ESTIMATE_PX = 100; +// When we lock an element, we use this as the width. We use 1px because locked +// items will not resize when their container changes and so could result in a +// horizontal scroll-bar appearing if it they are wide enough. +const LOCKED_WIDTH_PX = 1; + +/** + * Represents a range of elements from |low| to |high|, inclusive. + * If either |low| or |high| are null then we treat this as an empty range. + */ +class ElementBounds { + /** @const {Element} */ + low; + /** @const {Element} */ + high; + + constructor(low, high) { + this.low = low; + this.high = high; + } + + // Returns a Set containing all of the elements from low to high. + elementSet() { + const result = new Set(); + if (this.low === null || this.high === null) { + return result; + } + let element = this.low; + while (element) { + result.add(element); + if (element === this.high) { + break; + } + element = element.nextElementSibling; + } + return result; + } +} + +const EMPTY_ELEMENT_BOUNDS = new ElementBounds(null, null); + +/** + * Manages measuring and estimating sizes of elements. + * + * This tracks an average measured element size as elements are added + * and removed. +*/ +class SizeManager { + #sizes = new WeakMap(); + + #totalMeasuredSize = 0; + #measuredCount = 0; + + /** + * Measures and stores |element|'s size. If |element| was measured + * previously, this updates everything to use the new current size. + * + * @param {!Element} element The element to measure. + */ + measure(element) { + let oldSize = this.#sizes.get(element); + if (oldSize === undefined) { + oldSize = 0; + this.#measuredCount++; + } + const newSize = element.getBoundingClientRect().height; + this.#totalMeasuredSize += newSize - oldSize; + this.#sizes.set(element, newSize); + } + + /** + * Returns a size for |element|, either the last stored size or an + * estimate based on all other previously measured elements or a + * default. + * + * @param {!Element} element The element to produce a size for. + */ + getHopefulSize(element) { + const size = this.#sizes.get(element); + return size === undefined ? this.#getAverageSize() : size; + } + + #getAverageSize = () => { + return this.#measuredCount > 0 ? + this.#totalMeasuredSize / this.#measuredCount : + DEFAULT_HEIGHT_ESTIMATE_PX; + } + + /** + * Removes all data related to |element| from the manager. + * + * @param {!Element} element The element to remove. + */ + remove(element) { + const oldSize = this.#sizes.get(element); + if (oldSize === undefined) { + return; + } + this.#totalMeasuredSize -= oldSize; + this.#measuredCount--; + this.#sizes.delete(element); + } +} + +/** + * Manages the visibility (locked/unlocked state) of a list of + * elements. This list of elements is assumed to be in vertical + * display order (e.g. from lowest to highest offset). + * + * It uses resize and intersection observers on all of the visible + * elements to ensure that changes that impact visibility cause us to + * recalulate things (e.g. scrolling, restyling). +*/ +export class VisibilityManager { + #sizeManager = new SizeManager(); + #elements; + #syncRAFToken; + + #elementIntersectionObserver; + #elementResizeObserver; + + #revealed = new Set(); + + constructor(elements) { + this.#elements = elements; + + // We want to sync if any element's size changes or if it becomes + // more/less visible. + this.#elementIntersectionObserver = new IntersectionObserver(() => { + this.scheduleSync(); + }); + // TODO(fergal): Remove this? I'm not sure that we need the resize + // observer. Any resize that is important to us seems like it will + // also involve an intersection change. + this.#elementResizeObserver = new ResizeObserver(() => { + this.scheduleSync(); + }); + + for (const element of this.#elements) { + this.#didAdd(element); + } + this.scheduleSync(); + } + + /** + * Attempts to unlock a range of elements suitable for the current + * viewport. This causes one forced layout. + */ + #sync = () => { + if (this.#elements.length === 0) { + return; + } + + // The basic idea is ... + // The forced layout occurs at the start. We then use the laid out + // coordinates (which are based on a mix of real sizes for + // unlocked elements and the estimated sizes at the time of + // locking for locked elements) to calculate a set of elements + // which should be revealed. We use unlock/lock to move to this + // new set of revealed elements. We will check in the next frame + // whether we got it correct. + + // This causes a forced layout and takes measurements of all + // currently revealed elements. + this.#measureRevealed(); + + // Compute the pixel bounds of what we would like to reveal. Then + // find the elements corresponding to these bounds. + // TODO(fergal): Use nearest scrolling ancestor? + const desiredLow = 0 - window.innerHeight * BUFFER; + const desiredHigh = window.innerHeight + window.innerHeight * BUFFER; + const newBounds = this.#findElementBounds(desiredLow, desiredHigh); + const newRevealed = newBounds.elementSet(); + + // TODO(fergal): We need to observe 1 element off the end of the + // list, to cope with e.g. the scrolling region suddenly growing. + + // Lock and unlock the minimal set of elements to get us to the + // new state. + const toHide = sets.difference(this.#revealed, newRevealed); + toHide.forEach(e => this.#hide(e)); + const toReveal = sets.difference(newRevealed, this.#revealed); + toReveal.forEach(e => this.#reveal(e)); + + // Now we have revealed what we hope will fill the screen. It + // could be incorrect. Rather than measuring now and correcting it + // which would involve an unknown number of forced layouts, we + // come back next frame and try to make it better. We know we can + // stop when we didn't hide or reveal any elements. + if (toHide.size > 0 || toReveal.size > 0) { + this.scheduleSync(); + } + } + + /** + * Searches within the managed elements and returns an ElementBounds + * object. This object may represent an empty range or a range whose low + * element contains or is lower than |low| (or the lowest element + * possible). Similarly for |high|. + * + * @param {!number} low The lower bound to locate. + * @param {!number} high The upper bound to locate. + */ + #findElementBounds = (low, high) => { + const lowElement = findElement.findElement( + this.#elements, low, findElement.BIAS_LOW); + const highElement = findElement.findElement( + this.#elements, high, findElement.BIAS_HIGH); + + if (lowElement === null) { + if (highElement === null) { + return EMPTY_ELEMENT_BOUNDS; + } else { + return new ElementBounds(this.#elements[0], highElement); + } + } else if (highElement === null) { + return new ElementBounds( + lowElement, this.#elements[this.#elements.length - 1]); + } + return new ElementBounds(lowElement, highElement); + } + + /** + * Updates the size manager with all of the currently revealed + * elements' sizes. This will cause a forced layout. + */ + #measureRevealed = () => { + for (const element of this.#revealed) { + this.#sizeManager.measure(element); + } + } + + /** + * Reveals |element| so that it can be rendered. This includes + * unlocking and adding to various observers. + * + * @param {!Element} element The element to reveal. + */ + #reveal = element => { + this.#revealed.add(element); + this.#elementIntersectionObserver.observe(element); + this.#elementResizeObserver.observe(element); + this.#unlock(element); + } + + #logLockingError = (operation, reason, element) => { + // TODO: Figure out the LAPIs error/warning logging story. + console.error('Rejected: ', operation, element, reason); // eslint-disable-line no-console + } + + /** + * Unlocks |element|. + * + * @param {!Element} element The element to unlock. + */ + #unlock = element => { + element.displayLock.commit().catch(reason => { + // Only warn if the unlocked failed and we should be revealed. + if (this.#revealed.has(element)) { + this.#logLockingError('Commit', reason, element); + } + }); + } + + /** + * Hides |element| so that it cannot be rendered. This includes + * locking and removing from various observers. + * + * @param {!Element} element The element to hide. + */ + #hide = element => { + this.#revealed.delete(element); + this.#elementIntersectionObserver.unobserve(element); + this.#elementResizeObserver.unobserve(element); + element.displayLock.acquire({ + timeout: Infinity, + activatable: true, + size: [LOCKED_WIDTH_PX, this.#sizeManager.getHopefulSize(element)], + }).catch(reason => { + // Only warn if the lock failed and we should be locked. + if (!this.#revealed.has(element)) { + this.#logLockingError('Acquire', reason, element); + } + }); + } + + /** + * Notify the manager that |element| has been added to the list of + * managed elements. + * + * @param {!Element} element The element that was added. + */ + #didAdd = element => { + // Added children should be invisible initially. We want to make them + // invisible at this MutationObserver timing, so that there is no + // frame where the browser is asked to render all of the children + // (which could be a lot). + this.#hide(element); + } + + /** + * Notify the manager that |element| has been removed from the list + * of managed elements. + * + * @param {!Element} element The element that was removed. + */ + #didRemove = element => { + // Removed children should be made visible again. We should stop + // observing them and discard any size info we have for them as it + // may have become incorrect. + // + // TODO(fergal): Decide whether to also unlock if + // displayLock.locked is true. That would only be necessary if we + // got out of sync between this.#revealed and the locked state. So + // for now, assume are not buggy. + if (this.#revealed.has(element)) { + this.#unlock(element); + } + this.#revealed.delete(element); + this.#elementIntersectionObserver.unobserve(element); + this.#elementResizeObserver.unobserve(element); + this.#sizeManager.remove(element); + } + + /** + * Ensures that @see #sync() will be called at the next animation frame. + */ + scheduleSync() { + if (this.#syncRAFToken !== undefined) { + return; + } + + this.#syncRAFToken = window.requestAnimationFrame(() => { + this.#syncRAFToken = undefined; + this.#sync(); + }); + } + + /** + * Applys |records| generated by a mutation event to the manager. + * This computes the elements that were newly added/removed and + * notifies the managers for each. + * + * @param {!Object} records The mutations records. + */ + applyMutationObserverRecords(records) { + // It's unclear if we can support children which are not + // elements. We cannot control their visibility using display + // locking but we can just leave them alone. + // + // Relevant mutations are any additions or removals, including + // non-elements and also elements that are removed and then + // re-added as this may impact element bounds. + let relevantMutation = false; + const toRemove = new Set(); + for (const record of records) { + relevantMutation = relevantMutation || record.removedNodes.length > 0; + for (const node of record.removedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + toRemove.add(node); + } + } + } + + const toAdd = new Set(); + for (const record of records) { + relevantMutation = relevantMutation || record.addedNodes.length > 0; + for (const node of record.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + if (toRemove.has(node)) { + toRemove.delete(node); + } else { + toAdd.add(node); + } + } + } + } + for (const node of toRemove) { + this.#didRemove(node); + } + for (const node of toAdd) { + this.#didAdd(node); + } + + if (relevantMutation) { + this.scheduleSync(); + } + } +} + diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/async_iterator.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/async_iterator.mjs index 0da4854b99d..a83dba41c20 100644 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/async_iterator.mjs +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/async_iterator.mjs @@ -16,6 +16,8 @@ const AsyncIteratorPrototype = Object.getPrototypeOf( const StorageAreaAsyncIteratorPrototype = { __proto__: AsyncIteratorPrototype, + [Symbol.toStringTag]: 'StorageArea AsyncIterator', + next() { const performDatabaseOperation = _performDatabaseOperation.get(this); if (!performDatabaseOperation) { @@ -42,6 +44,12 @@ const StorageAreaAsyncIteratorPrototype = { }, }; +Object.defineProperty( + StorageAreaAsyncIteratorPrototype, + Symbol.toStringTag, + {writable: false, enumerable: false} +); + function getNextIterResult(iter, performDatabaseOperation) { return performDatabaseOperation(async (transaction, store) => { const lastKey = _lastKey.get(iter); diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/index.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/index.mjs index b5272ea5d7b..88ef25e818a 100644 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/index.mjs +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/kv-storage/index.mjs @@ -5,16 +5,18 @@ import {createStorageAreaAsyncIterator} from './async_iterator.mjs'; import {promiseForRequest, promiseForTransaction, throwForDisallowedKey} from './idb_utils.mjs'; -// TODOs/spec-noncompliances: +// Overall TODOs/spec-noncompliances: // - Susceptible to tampering of built-in prototypes and globals. We want to // work on tooling to ameliorate that. const DEFAULT_STORAGE_AREA_NAME = 'default'; const DEFAULT_IDB_STORE_NAME = 'store'; +// TODO(crbug.com/977470): this should be handled via infrastructure that +// avoids putting it in the module map entirely, not as a runtime check. +// Several web platform tests fail because of this. if (!self.isSecureContext) { - throw new DOMException( - 'KV Storage is only available in secure contexts', 'SecurityError'); + throw new TypeError('KV Storage is only available in secure contexts'); } export class StorageArea { @@ -132,8 +134,25 @@ export class StorageArea { } StorageArea.prototype[Symbol.asyncIterator] = StorageArea.prototype.entries; - -export const storage = new StorageArea(DEFAULT_STORAGE_AREA_NAME); +StorageArea.prototype[Symbol.toStringTag] = 'StorageArea'; + +// Override the defaults that are implied by using class declarations and +// assignment, to be more Web IDL-ey. +// https://github.com/heycam/webidl/issues/738 may modify these a bit. +Object.defineProperties(StorageArea.prototype, { + set: {enumerable: true}, + get: {enumerable: true}, + delete: {enumerable: true}, + clear: {enumerable: true}, + keys: {enumerable: true}, + values: {enumerable: true}, + entries: {enumerable: true}, + backingStore: {enumerable: true}, + [Symbol.asyncIterator]: {enumerable: false}, + [Symbol.toStringTag]: {writable: false, enumerable: false} +}); + +export default new StorageArea(DEFAULT_STORAGE_AREA_NAME); async function performDatabaseOperation( promise, setPromise, name, mode, steps) { @@ -145,7 +164,9 @@ async function performDatabaseOperation( const transaction = database.transaction(DEFAULT_IDB_STORE_NAME, mode); const store = transaction.objectStore(DEFAULT_IDB_STORE_NAME); - return steps(transaction, store); + const result = steps(transaction, store); + transaction.commit(); + return result; } function initializeDatabasePromise(setPromise, databaseName) { diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/resources.grdp b/chromium/third_party/blink/renderer/core/script/resources/layered_api/resources.grdp index 2a7e3b38f14..255d05543d1 100644 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/resources.grdp +++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/resources.grdp @@ -7,14 +7,17 @@ third_party/blink/public/blink_resources.grd. --> <include name="IDR_LAYERED_API_BLANK_INDEX_MJS" file="../renderer/core/script/resources/layered_api/blank/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_INTERNAL_REFLECTION_MJS" file="../renderer/core/script/resources/layered_api/elements/internal/reflection.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_SWITCH_FACE_UTILS_MJS" file="../renderer/core/script/resources/layered_api/elements/switch/face_utils.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_SWITCH_INDEX_MJS" file="../renderer/core/script/resources/layered_api/elements/switch/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_SWITCH_STYLE_MJS" file="../renderer/core/script/resources/layered_api/elements/switch/style.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_SWITCH_TRACK_MJS" file="../renderer/core/script/resources/layered_api/elements/switch/track.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_TOAST_INDEX_MJS" file="../renderer/core/script/resources/layered_api/elements/toast/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_FIND_ELEMENT_MJS" file="../renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_INDEX_MJS" file="../renderer/core/script/resources/layered_api/elements/virtual-scroller/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_SETS_MJS" file="../renderer/core/script/resources/layered_api/elements/virtual-scroller/sets.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> + <include name="IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_VISIBILITY_MANAGER_MJS" file="../renderer/core/script/resources/layered_api/elements/virtual-scroller/visibility-manager.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> <include name="IDR_LAYERED_API_KV_STORAGE_ASYNC_ITERATOR_MJS" file="../renderer/core/script/resources/layered_api/kv-storage/async_iterator.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> <include name="IDR_LAYERED_API_KV_STORAGE_IDB_UTILS_MJS" file="../renderer/core/script/resources/layered_api/kv-storage/idb_utils.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> <include name="IDR_LAYERED_API_KV_STORAGE_INDEX_MJS" file="../renderer/core/script/resources/layered_api/kv-storage/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_INDEX_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/index.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_ITEM_SOURCE_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/item-source.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_VIRTUAL_REPEATER_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/virtual-repeater.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_VIRTUAL_SCROLLER_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_BASE_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-base.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_GRID_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-grid.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> - <include name="IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_MJS" file="../renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d.mjs" type="BINDATA" skip_minify="true" compress="gzip"/> </grit-part> diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/README.chromium b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/README.chromium deleted file mode 100644 index e8b4d5d0007..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/README.chromium +++ /dev/null @@ -1,12 +0,0 @@ -Name: virtual-scroller Layered API -URL: https://github.com/valdrinkoshi/virtual-scroller -Version: 58659cee10c5d9237821d5c475dac89720bd995d -Security Critical: no - -Description: -Temporarily, the files under this directory are authored by Chromium Authors -on a github repository, and then imported to Chromium repository directly here, -until a long-term Layered API development plan is settled. - -Local Modifications: -None (except for renaming virtual-scroller-element.mjs to index.mjs) diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/index.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/index.mjs deleted file mode 100644 index 8cdad8091f7..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/index.mjs +++ /dev/null @@ -1,213 +0,0 @@ -import {_item, _key, ItemSource} from './item-source.mjs'; -import {default as Layout1dGrid} from './layouts/layout-1d-grid.mjs'; -import {default as Layout1d} from './layouts/layout-1d.mjs'; -import {VirtualScroller} from './virtual-scroller.mjs'; - -export {ItemSource}; - -/** Properties */ -const _scroller = Symbol(); -const _createElement = Symbol(); -const _updateElement = Symbol(); -const _recycleElement = Symbol(); -const _nodePool = Symbol(); -const _rawItemSource = Symbol(); -const _itemSource = Symbol(); -const _elementSource = Symbol(); -const _firstConnected = Symbol(); -/** Functions */ -const _render = Symbol(); - -export class VirtualScrollerElement extends HTMLElement { - constructor() { - super(); - this[_scroller] = null; - // Default create/update/recycleElement. - this[_nodePool] = []; - let childTemplate = null; - this[_createElement] = () => { - if (this[_nodePool] && this[_nodePool].length) { - return this[_nodePool].pop(); - } - if (!childTemplate) { - const template = this.querySelector('template'); - childTemplate = template && template.content.firstElementChild ? - template.content.firstElementChild : - document.createElement('div'); - } - return childTemplate.cloneNode(true); - }; - this[_updateElement] = (element, item) => element.textContent = - item.toString(); - this[_recycleElement] = (element) => this[_nodePool].push(element); - - this[_itemSource] = this[_rawItemSource] = null; - this[_elementSource] = {}; - - this[_firstConnected] = false; - } - - connectedCallback() { - if (!this[_firstConnected]) { - this.attachShadow({mode: 'open'}).innerHTML = ` -<style> - :host { - display: block; - position: relative; - contain: strict; - height: 150px; - overflow: auto; - } - :host([hidden]) { - display: none; - } - ::slotted(*) { - box-sizing: border-box; - } - :host([layout=vertical]) ::slotted(*) { - width: 100%; - } - :host([layout=horizontal]) ::slotted(*) { - height: 100%; - } -</style> -<slot></slot>`; - // Set default values. - if (!this.layout) { - this.layout = 'vertical'; - } - // Enables rendering. - this[_firstConnected] = true; - } - this[_render](); - } - - static get observedAttributes() { - return ['layout']; - } - - attributeChangedCallback(name, oldVal, newVal) { - this[_render](); - } - - get layout() { - return this.getAttribute('layout'); - } - set layout(layout) { - this.setAttribute('layout', layout); - } - - get itemSource() { - return this[_itemSource]; - } - set itemSource(itemSource) { - // No Change. - if (this[_rawItemSource] === itemSource) { - return; - } - this[_rawItemSource] = itemSource; - this[_itemSource] = Array.isArray(itemSource) ? - ItemSource.fromArray(itemSource) : - itemSource; - this[_render](); - } - - get createElement() { - return this[_createElement]; - } - set createElement(fn) { - // Resets default recycling. - if (this[_nodePool]) { - this.recycleElement = null; - } - this[_createElement] = fn; - // Invalidate wrapped function. - this[_elementSource].createElement = null; - this[_render](); - } - - get updateElement() { - return this[_updateElement]; - } - set updateElement(fn) { - this[_updateElement] = fn; - // Invalidate wrapped function. - this[_elementSource].updateElement = null; - this[_render](); - } - - get recycleElement() { - return this[_recycleElement]; - } - set recycleElement(fn) { - // Marks default recycling changed. - this[_nodePool] = null; - this[_recycleElement] = fn; - // Invalidate wrapped function. - this[_elementSource].recycleElement = null; - this[_render](); - } - - itemsChanged() { - if (this[_scroller]) { - // Render because length might have changed. - this[_render](); - // Request reset because items might have changed. - this[_scroller].requestReset(); - } - } - - scrollToIndex(index, { position = 'start' } = {}) { - if (this[_scroller]) { - this[_scroller].layout.scrollToIndex(index, position); - } - } - - [_render]() { - // Wait first connected as scroller needs to measure - // sizes of container and children. - if (!this[_firstConnected] || !this.createElement) { - return; - } - if (!this[_scroller]) { - this[_scroller] = - new VirtualScroller({container: this, scrollTarget: this}); - } - const scroller = this[_scroller]; - - const layoutAttr = this.layout; - const Layout = layoutAttr.endsWith('-grid') ? Layout1dGrid : Layout1d; - const direction = - layoutAttr.startsWith('horizontal') ? 'horizontal' : 'vertical'; - const layout = scroller.layout instanceof Layout && - scroller.layout.direction === direction ? - scroller.layout : - new Layout({direction}); - - let {createElement, updateElement, recycleElement} = this[_elementSource]; - if (!createElement) { - createElement = this[_elementSource].createElement = (index) => - this.createElement(this.itemSource[_item](index), index); - } - if (this.updateElement && !updateElement) { - updateElement = this[_elementSource].updateElement = (element, index) => - this.updateElement(element, this.itemSource[_item](index), index); - } - if (this.recycleElement && !recycleElement) { - recycleElement = this[_elementSource].recycleElement = (element, index) => - this.recycleElement(element, this.itemSource[_item](index), index); - } - - const elementKey = this.itemSource ? this.itemSource[_key] : null; - const totalItems = this.itemSource ? this.itemSource.length : 0; - Object.assign(scroller, { - layout, - createElement, - updateElement, - recycleElement, - elementKey, - totalItems - }); - } -} -customElements.define('virtual-scroller', VirtualScrollerElement); diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/item-source.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/item-source.mjs deleted file mode 100644 index 255e97b245c..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/item-source.mjs +++ /dev/null @@ -1,47 +0,0 @@ -export const _getLength = Symbol(); -export const _item = Symbol(); -export const _key = Symbol(); - -export class ItemSource { - constructor({getLength, item, key}) { - if (typeof getLength !== 'function') { - throw new TypeError('getLength option must be a function'); - } - if (typeof item !== 'function') { - throw new TypeError('item option must be a function'); - } - if (typeof key !== 'function') { - throw new TypeError('key option must be a function'); - } - - this[_getLength] = getLength; - this[_item] = item; - this[_key] = key; - } - - static fromArray(array, key) { - if (!Array.isArray(array)) { - throw new TypeError('First argument to fromArray() must be an array'); - } - if (typeof key !== 'function' && key !== undefined) { - throw new TypeError( - 'Second argument to fromArray() must be a function or undefined'); - } - - return new this({ - getLength() { - return array.length; - }, - item(index) { - return array[index]; - }, - key(index) { - return key ? key(array[index], index) : array[index]; - } - }); - } - - get length() { - return this[_getLength](); - } -} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-base.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-base.mjs deleted file mode 100644 index 89a9a9e8106..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-base.mjs +++ /dev/null @@ -1,320 +0,0 @@ -export default class Layout extends EventTarget { - constructor(config) { - super(); - - this._physicalMin = 0; - this._physicalMax = 0; - - this._first = -1; - this._last = -1; - - this._latestCoords = {left: 0, top: 0}; - - this._itemSize = {width: 100, height: 100}; - this._spacing = 0; - - this._sizeDim = 'height'; - this._secondarySizeDim = 'width'; - this._positionDim = 'top'; - this._secondaryPositionDim = 'left'; - this._direction = 'vertical'; - - this._scrollPosition = 0; - this._scrollError = 0; - this._viewportSize = {width: 0, height: 0}; - this._totalItems = 0; - - this._scrollSize = 1; - - this._overhang = 150; - - this._pendingReflow = false; - - this._scrollToIndex = -1; - this._scrollToAnchor = 0; - - Object.assign(this, config); - } - - // public properties - - get totalItems() { - return this._totalItems; - } - set totalItems(num) { - if (num !== this._totalItems) { - this._totalItems = num; - this._maxIdx = num - 1; - this._scheduleReflow(); - } - } - - get direction() { - return this._direction; - } - set direction(dir) { - // Force it to be either horizontal or vertical. - dir = (dir === 'horizontal') ? dir : 'vertical'; - if (dir !== this._direction) { - this._direction = dir; - this._sizeDim = (dir === 'horizontal') ? 'width' : 'height'; - this._secondarySizeDim = (dir === 'horizontal') ? 'height' : 'width'; - this._positionDim = (dir === 'horizontal') ? 'left' : 'top'; - this._secondaryPositionDim = (dir === 'horizontal') ? 'top' : 'left'; - this._scheduleReflow(); - } - } - - get itemSize() { - return this._itemSize; - } - set itemSize(dims) { - const {_itemDim1, _itemDim2} = this; - Object.assign(this._itemSize, dims); - if (_itemDim1 !== this._itemDim1 || _itemDim2 !== this._itemDim2) { - if (_itemDim2 !== this._itemDim2) { - this._itemDim2Changed(); - } else { - this._scheduleReflow(); - } - } - } - - get spacing() { - return this._spacing; - } - set spacing(px) { - if (px !== this._spacing) { - this._spacing = px; - this._scheduleReflow(); - } - } - - get viewportSize() { - return this._viewportSize; - } - set viewportSize(dims) { - const {_viewDim1, _viewDim2} = this; - Object.assign(this._viewportSize, dims); - if (_viewDim2 !== this._viewDim2) { - this._viewDim2Changed(); - } else if (_viewDim1 !== this._viewDim1) { - this._checkThresholds(); - } - } - - get viewportScroll() { - return this._latestCoords; - } - set viewportScroll(coords) { - Object.assign(this._latestCoords, coords); - const oldPos = this._scrollPosition; - this._scrollPosition = this._latestCoords[this._positionDim]; - if (oldPos !== this._scrollPosition) { - this._scrollPositionChanged(oldPos, this._scrollPosition); - } - this._checkThresholds(); - } - - // private properties - - get _delta() { - return this._itemDim1 + this._spacing; - } - - get _itemDim1() { - return this._itemSize[this._sizeDim]; - } - - get _itemDim2() { - return this._itemSize[this._secondarySizeDim]; - } - - get _viewDim1() { - return this._viewportSize[this._sizeDim]; - } - - get _viewDim2() { - return this._viewportSize[this._secondarySizeDim]; - } - - get _num() { - if (this._first === -1 || this._last === -1) { - return 0; - } - return this._last - this._first + 1; - } - - // public methods - - reflowIfNeeded() { - if (this._pendingReflow) { - this._pendingReflow = false; - this._reflow(); - } - } - - scrollToIndex(index, position = 'start') { - if (!Number.isFinite(index)) - return; - index = Math.min(this.totalItems, Math.max(0, index)); - this._scrollToIndex = index; - if (position === 'nearest') { - position = index > this._first + this._num / 2 ? 'end' : 'start'; - } - switch (position) { - case 'start': - this._scrollToAnchor = 0; - break; - case 'center': - this._scrollToAnchor = 0.5; - break; - case 'end': - this._scrollToAnchor = 1; - break; - default: - throw new TypeError( - 'position must be one of: start, center, end, nearest'); - } - this._scheduleReflow(); - this.reflowIfNeeded(); - } - - /// - - _scheduleReflow() { - this._pendingReflow = true; - } - - _reflow() { - const {_first, _last, _scrollSize} = this; - - this._updateScrollSize(); - this._getActiveItems(); - this._scrollIfNeeded(); - - if (this._scrollSize !== _scrollSize) { - this._emitScrollSize(); - } - - if (this._first === -1 && this._last === -1) { - this._emitRange(); - } else if ( - this._first !== _first || this._last !== _last || - this._spacingChanged) { - this._emitRange(); - this._emitChildPositions(); - } - this._emitScrollError(); - } - - _updateScrollSize() { - // Ensure we have at least 1px - this allows getting at least 1 item to be - // rendered. - this._scrollSize = Math.max(1, this._totalItems * this._delta); - } - - _checkThresholds() { - if (this._viewDim1 === 0 && this._num > 0) { - this._scheduleReflow(); - } else { - const min = Math.max(0, this._scrollPosition - this._overhang); - const max = Math.min( - this._scrollSize, - this._scrollPosition + this._viewDim1 + this._overhang); - if (this._physicalMin > min || this._physicalMax < max) { - this._scheduleReflow(); - } - } - } - - _scrollIfNeeded() { - if (this._scrollToIndex === -1) { - return; - } - const index = this._scrollToIndex; - const anchor = this._scrollToAnchor; - const pos = this._getItemPosition(index)[this._positionDim]; - const size = this._getItemSize(index)[this._sizeDim]; - - const curAnchorPos = this._scrollPosition + this._viewDim1 * anchor; - const newAnchorPos = pos + size * anchor; - // Ensure scroll position is an integer within scroll bounds. - const scrollPosition = Math.floor(Math.min( - this._scrollSize - this._viewDim1, - Math.max(0, this._scrollPosition - curAnchorPos + newAnchorPos))); - this._scrollError += this._scrollPosition - scrollPosition; - this._scrollPosition = scrollPosition; - } - - _emitRange(inProps) { - const detail = Object.assign( - { - first: this._first, - last: this._last, - num: this._num, - stable: true, - }, - inProps); - this.dispatchEvent(new CustomEvent('rangechange', {detail})); - } - - _emitScrollSize() { - const detail = { - [this._sizeDim]: this._scrollSize, - }; - this.dispatchEvent(new CustomEvent('scrollsizechange', {detail})); - } - - _emitScrollError() { - if (this._scrollError) { - const detail = { - [this._positionDim]: this._scrollError, - [this._secondaryPositionDim]: 0, - }; - this.dispatchEvent(new CustomEvent('scrollerrorchange', {detail})); - this._scrollError = 0; - } - } - - _emitChildPositions() { - const detail = {}; - for (let idx = this._first; idx <= this._last; idx++) { - detail[idx] = this._getItemPosition(idx); - } - this.dispatchEvent(new CustomEvent('itempositionchange', {detail})); - } - - _itemDim2Changed() { - // Override - } - - _viewDim2Changed() { - // Override - } - - _scrollPositionChanged(oldPos, newPos) { - // When both values are bigger than the max scroll position, keep the - // current _scrollToIndexx, otherwise invalidate it. - const maxPos = this._scrollSize - this._viewDim1; - if (oldPos < maxPos || newPos < maxPos) { - this._scrollToIndex = -1; - } - } - - _getActiveItems() { - // Override - } - - _getItemPosition(idx) { - // Override. - } - - _getItemSize(idx) { - // Override. - return { - [this._sizeDim]: this._itemDim1, - [this._secondarySizeDim]: this._itemDim2, - }; - } -}
\ No newline at end of file diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-grid.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-grid.mjs deleted file mode 100644 index c9c00d021da..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d-grid.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import Layout1dBase from './layout-1d-base.mjs'; - -export default class Layout extends Layout1dBase { - constructor(config) { - super(config); - this._rolumns = 1; - } - - updateItemSizes(sizes) { - // Assume all items have the same size. - const size = Object.values(sizes)[0]; - if (size) { - this.itemSize = size; - } - } - - _viewDim2Changed() { - this._defineGrid(); - } - - _itemDim2Changed() { - this._defineGrid(); - } - - _getActiveItems() { - const min = Math.max(0, this._scrollPosition - this._overhang); - const max = Math.min( - this._scrollSize, - this._scrollPosition + this._viewDim1 + this._overhang); - const firstCow = Math.floor(min / this._delta); - const lastCow = Math.ceil(max / this._delta) - 1; - - this._first = firstCow * this._rolumns; - this._last = - Math.min(((lastCow + 1) * this._rolumns) - 1, this._totalItems); - this._physicalMin = this._delta * firstCow; - this._physicalMax = this._delta * (lastCow + 1); - } - - _getItemPosition(idx) { - return { - [this._positionDim]: Math.floor(idx / this._rolumns) * this._delta, - [this._secondaryPositionDim]: this._spacing + - ((idx % this._rolumns) * (this._spacing + this._itemDim2)) - } - } - - - _defineGrid() { - const {_spacing} = this; - this._rolumns = Math.max(1, Math.floor(this._viewDim2 / this._itemDim2)); - if (this._rolumns > 1) { - this._spacing = (this._viewDim2 % (this._rolumns * this._itemDim2)) / - (this._rolumns + 1); - } - this._spacingChanged = !(_spacing === this._spacing); - this._scheduleReflow(); - } - - _updateScrollSize() { - this._scrollSize = - Math.max(1, Math.ceil(this._totalItems / this._rolumns) * this._delta); - } -}
\ No newline at end of file diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d.mjs deleted file mode 100644 index 6f5d181cb0e..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/layouts/layout-1d.mjs +++ /dev/null @@ -1,349 +0,0 @@ -import Layout1dBase from './layout-1d-base.mjs'; - -export default class Layout extends Layout1dBase { - constructor(config) { - super(config); - this._physicalItems = new Map(); - this._newPhysicalItems = new Map(); - - this._metrics = new Map(); - - this._anchorIdx = null; - this._anchorPos = null; - this._stable = true; - - this._needsRemeasure = false; - - this._nMeasured = 0; - this._tMeasured = 0; - - this._estimate = true; - } - - updateItemSizes(sizes) { - Object.keys(sizes).forEach((key) => { - const metrics = sizes[key], mi = this._getMetrics(key), - prevSize = mi[this._sizeDim]; - - // TODO(valdrin) Handle margin collapsing. - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing - mi.width = metrics.width + (metrics.marginLeft || 0) + - (metrics.marginRight || 0); - mi.height = metrics.height + (metrics.marginTop || 0) + - (metrics.marginBottom || 0); - - const size = mi[this._sizeDim]; - const item = this._getPhysicalItem(Number(key)); - if (item) { - let delta; - - if (size !== undefined) { - item.size = size; - if (prevSize === undefined) { - delta = size; - this._nMeasured++; - } else { - delta = size - prevSize; - } - } - this._tMeasured = this._tMeasured + delta; - } else { - // console.debug(`Could not find physical item for key ${key}`); - } - }); - if (!this._nMeasured) { - console.warn(`No items measured yet.`); - } else { - this._updateItemSize(); - this._scheduleReflow(); - } - } - - _updateItemSize() { - // Keep integer values. - this._itemSize[this._sizeDim] = - Math.round(this._tMeasured / this._nMeasured); - } - - // - - _getMetrics(idx) { - return (this._metrics[idx] = this._metrics[idx] || {}); - } - - _getPhysicalItem(idx) { - return this._newPhysicalItems.get(idx) || this._physicalItems.get(idx); - } - - _getSize(idx) { - const item = this._getPhysicalItem(idx); - return item && item.size; - } - - _getPosition(idx) { - const item = this._physicalItems.get(idx); - return item ? item.pos : (idx * (this._delta)) + this._spacing; - } - - _calculateAnchor(lower, upper) { - if (lower === 0) { - return 0; - } - if (upper > this._scrollSize - this._viewDim1) { - return this._maxIdx; - } - return Math.max( - 0, - Math.min( - this._maxIdx, Math.floor(((lower + upper) / 2) / this._delta))); - } - - _getAnchor(lower, upper) { - if (this._physicalItems.size === 0) { - return this._calculateAnchor(lower, upper); - } - if (this._first < 0) { - console.error('_getAnchor: negative _first'); - return this._calculateAnchor(lower, upper); - } - if (this._last < 0) { - console.error('_getAnchor: negative _last'); - return this._calculateAnchor(lower, upper); - } - - const firstItem = this._getPhysicalItem(this._first), - lastItem = this._getPhysicalItem(this._last), - firstMin = firstItem.pos, firstMax = firstMin + firstItem.size, - lastMin = lastItem.pos, lastMax = lastMin + lastItem.size; - - if (lastMax < lower) { - // Window is entirely past physical items, calculate new anchor - return this._calculateAnchor(lower, upper); - } - if (firstMin > upper) { - // Window is entirely before physical items, calculate new anchor - return this._calculateAnchor(lower, upper); - } - if (firstMin >= lower || firstMax >= lower) { - // First physical item overlaps window, choose it - return this._first; - } - if (lastMax <= upper || lastMin <= upper) { - // Last physical overlaps window, choose it - return this._last; - } - // Window contains a physical item, but not the first or last - let maxIdx = this._last, minIdx = this._first; - - while (true) { - let candidateIdx = Math.round((maxIdx + minIdx) / 2), - candidate = this._physicalItems.get(candidateIdx), - cMin = candidate.pos, cMax = cMin + candidate.size; - - if ((cMin >= lower && cMin <= upper) || - (cMax >= lower && cMax <= upper)) { - return candidateIdx; - } else if (cMax < lower) { - minIdx = candidateIdx + 1; - } else if (cMin > upper) { - maxIdx = candidateIdx - 1; - } - } - } - - _getActiveItems() { - if (this._viewDim1 === 0 || this._totalItems === 0) { - this._clearItems(); - } else { - const upper = Math.min( - this._scrollSize, - this._scrollPosition + this._viewDim1 + this._overhang), - lower = Math.max(0, upper - this._viewDim1 - (2 * this._overhang)); - - this._getItems(lower, upper); - } - } - - _clearItems() { - this._first = -1; - this._last = -1; - this._physicalMin = 0; - this._physicalMax = 0; - const items = this._newPhysicalItems; - this._newPhysicalItems = this._physicalItems; - this._newPhysicalItems.clear(); - this._physicalItems = items; - this._stable = true; - } - - _getItems(lower, upper) { - const items = this._newPhysicalItems; - - // The anchorIdx is the anchor around which we reflow. - // It is designed to allow jumping to any point of the scroll size. - // We choose it once and stick with it until stable. first and last are - // deduced around it. - if (this._anchorIdx === null || this._anchorPos === null) { - this._anchorIdx = this._getAnchor(lower, upper); - this._anchorPos = this._getPosition(this._anchorIdx); - } - - let anchorSize = this._getSize(this._anchorIdx); - if (anchorSize === undefined) { - anchorSize = this._itemDim1; - } - - // Anchor might be outside bounds, so prefer correcting the error and keep - // that anchorIdx. - let anchorErr = 0; - - if (this._anchorPos + anchorSize + this._spacing < lower) { - anchorErr = lower - (this._anchorPos + anchorSize + this._spacing); - } - - if (this._anchorPos > upper) { - anchorErr = upper - this._anchorPos; - } - - if (anchorErr) { - this._scrollPosition -= anchorErr; - lower -= anchorErr; - upper -= anchorErr; - this._scrollError += anchorErr; - } - - items.set(this._anchorIdx, {pos: this._anchorPos, size: anchorSize}); - - this._first = (this._last = this._anchorIdx); - this._physicalMin = (this._physicalMax = this._anchorPos); - - this._stable = true; - - while (this._physicalMin > lower && this._first > 0) { - let size = this._getSize(--this._first); - if (size === undefined) { - this._stable = false; - size = this._itemDim1; - } - const pos = (this._physicalMin -= size + this._spacing); - items.set(this._first, {pos, size}); - if (this._stable === false && this._estimate === false) { - break; - } - } - - while (this._physicalMax < upper && this._last < this._totalItems) { - let size = this._getSize(this._last); - if (size === undefined) { - this._stable = false; - size = this._itemDim1; - } - items.set(this._last++, {pos: this._physicalMax, size}); - if (this._stable === false && this._estimate === false) { - break; - } else { - this._physicalMax += size + this._spacing; - } - } - - this._last--; - - // This handles the cases where we were relying on estimated sizes. - const extentErr = this._calculateError(); - if (extentErr) { - this._physicalMin -= extentErr; - this._physicalMax -= extentErr; - this._anchorPos -= extentErr; - this._scrollPosition -= extentErr; - items.forEach(item => item.pos -= extentErr); - this._scrollError += extentErr; - } - - if (this._stable) { - this._newPhysicalItems = this._physicalItems; - this._newPhysicalItems.clear(); - this._physicalItems = items; - } - } - - _calculateError() { - if (this._first === 0) { - return this._physicalMin; - } else if (this._physicalMin <= 0) { - return this._physicalMin - (this._first * this._delta); - } else if (this._last === this._maxIdx) { - return this._physicalMax - this._scrollSize; - } else if (this._physicalMax >= this._scrollSize) { - return ( - (this._physicalMax - this._scrollSize) + - ((this._maxIdx - this._last) * this._delta)); - } - return 0; - } - - _updateScrollSize() { - // Reuse previously calculated physical max, as it might be - // higher than the estimated size. - super._updateScrollSize(); - this._scrollSize = Math.max(this._physicalMax, this._scrollSize); - } - - // TODO: Can this be made to inherit from base, with proper hooks? - _reflow() { - const {_first, _last, _scrollSize} = this; - - this._updateScrollSize(); - this._getActiveItems(); - this._scrollIfNeeded(); - - if (this._scrollSize !== _scrollSize) { - this._emitScrollSize(); - } - - this._emitRange(); - if (this._first === -1 && this._last === -1) { - this._resetReflowState(); - } else if ( - this._first !== _first || this._last !== _last || - this._needsRemeasure) { - this._emitChildPositions(); - this._emitScrollError(); - } else { - this._emitChildPositions(); - this._emitScrollError(); - this._resetReflowState(); - } - } - - _resetReflowState() { - this._anchorIdx = null; - this._anchorPos = null; - this._stable = true; - } - - _getItemPosition(idx) { - return { - [this._positionDim]: this._getPosition(idx), - [this._secondaryPositionDim]: 0 - } - } - - _getItemSize(idx) { - return { - [this._sizeDim]: this._getSize(idx) || this._itemDim1, - [this._secondarySizeDim]: this._itemDim2, - }; - } - - _viewDim2Changed() { - this._needsRemeasure = true; - this._scheduleReflow(); - } - - _emitRange() { - const remeasure = this._needsRemeasure; - const stable = this._stable; - this._needsRemeasure = false; - super._emitRange({remeasure, stable}); - } -} diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-repeater.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-repeater.mjs deleted file mode 100644 index 3d3cb6aa7c1..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-repeater.mjs +++ /dev/null @@ -1,531 +0,0 @@ -export const Repeats = Superclass => class extends Superclass { - constructor(config) { - super(); - - this._createElementFn = null; - this._updateElementFn = null; - this._recycleElementFn = null; - this._elementKeyFn = null; - - this._measureCallback = null; - - this._totalItems = 0; - // Consider renaming this. firstVisibleIndex? - this._first = 0; - // Consider renaming this. count? visibleElements? - this._num = Infinity; - - this.__incremental = false; - - // used only internally.. - // legacy from 1st approach to preact integration - this._manageDom = true; - // used to check if it is more perf if you don't care of dom order? - this._maintainDomOrder = true; - - this._last = 0; - this._prevFirst = 0; - this._prevLast = 0; - - this._needsReset = false; - this._needsRemeasure = false; - this._pendingRender = null; - - // Contains child nodes in the rendered order. - this._ordered = []; - // this._pool = []; - this._active = new Map(); - this._prevActive = new Map(); - // Both used for recycling purposes. - this._keyToChild = new Map(); - this._childToKey = new WeakMap(); - // Used to keep track of measures by index. - this._indexToMeasure = {}; - // Used to debounce _measureChildren calls. - this._measuringId = -1; - - if (config) { - Object.assign(this, config); - } - } - - // API - - get container() { - return this._container; - } - set container(container) { - if (container === this._container) { - return; - } - if (this._container) { - // Remove children from old container. - this._ordered.forEach((child) => this._removeChild(child)); - } - - this._container = container; - - if (container) { - // Insert children in new container. - this._ordered.forEach((child) => this._insertBefore(child, null)); - } else { - this._ordered.length = 0; - this._active.clear(); - this._prevActive.clear(); - } - this.requestReset(); - } - - get createElement() { - return this._createElementFn; - } - set createElement(fn) { - if (fn !== this._createElementFn) { - this._createElementFn = fn; - this._keyToChild.clear(); - this.requestReset(); - } - } - - get updateElement() { - return this._updateElementFn; - } - set updateElement(fn) { - if (fn !== this._updateElementFn) { - this._updateElementFn = fn; - this.requestReset(); - } - } - - get recycleElement() { - return this._recycleElementFn; - } - set recycleElement(fn) { - if (fn !== this._recycleElementFn) { - this._recycleElementFn = fn; - this.requestReset(); - } - } - - get elementKey() { - return this._elementKeyFn; - } - set elementKey(fn) { - if (fn !== this._elementKeyFn) { - this._elementKeyFn = fn; - this._keyToChild.clear(); - this.requestReset(); - } - } - - get first() { - return this._first; - } - - set first(idx) { - if (typeof idx === 'number') { - const newFirst = Math.max(0, Math.min(idx, this._totalItems - this._num)); - if (newFirst !== this._first) { - this._first = newFirst; - this._scheduleRender(); - } - } - } - - get num() { - return this._num; - } - - set num(n) { - if (typeof n === 'number') { - if (n !== this._num) { - this._num = n; - this.first = this._first; - this._scheduleRender(); - } - } - } - - get totalItems() { - return this._totalItems; - } - - set totalItems(num) { - // TODO(valdrin) should we check if it is a finite number? - // Technically, Infinity would break Layout, not VirtualRepeater. - if (typeof num === 'number' && num !== this._totalItems) { - this._totalItems = num; - this.first = this._first; - this.requestReset(); - } - } - - get _incremental() { - return this.__incremental; - } - - set _incremental(inc) { - if (inc !== this.__incremental) { - this.__incremental = inc; - this._scheduleRender(); - } - } - - requestReset() { - this._needsReset = true; - this._scheduleRender(); - } - - requestRemeasure() { - this._needsRemeasure = true; - this._scheduleRender(); - } - - // Core functionality - - /** - * @protected - */ - _shouldRender() { - return Boolean(this.container && this.createElement); - } - - /** - * @private - */ - _scheduleRender() { - if (!this._pendingRender) { - this._pendingRender = requestAnimationFrame(() => { - this._pendingRender = null; - if (this._shouldRender()) { - this._render(); - } - }); - } - } - - /** - * Returns those children that are about to be displayed and that - * require to be positioned. If reset or remeasure has been triggered, - * all children are returned. - * @return {{indices:Array<number>,children:Array<Element>}} - * @private - */ - get _toMeasure() { - return this._ordered.reduce((toMeasure, c, i) => { - const idx = this._first + i; - if (this._needsReset || this._needsRemeasure || idx < this._prevFirst || - idx > this._prevLast) { - toMeasure.indices.push(idx); - toMeasure.children.push(c); - } - return toMeasure; - }, {indices: [], children: []}); - } - - /** - * Measures each child bounds and builds a map of index/bounds to be passed to - * the `_measureCallback` - * @private - */ - _measureChildren({indices, children}) { - let pm = children.map( - (c, i) => this._indexToMeasure[indices[i]] || this._measureChild(c)); - const mm = /** @type {{ number: { width: number, height: number } }} */ - (pm.reduce((out, cur, i) => { - out[indices[i]] = this._indexToMeasure[indices[i]] = cur; - return out; - }, {})); - this._measureCallback(mm); - } - - /** - * @protected - */ - _render() { - const rangeChanged = - this._first !== this._prevFirst || this._num !== this._prevNum; - // Create/update/recycle DOM. - if (rangeChanged || this._needsReset) { - this._last = - this._first + Math.min(this._num, this._totalItems - this._first) - 1; - if (this._num || this._prevNum) { - if (this._needsReset) { - this._reset(this._first, this._last); - } else { - this._discardHead(); - this._discardTail(); - this._addHead(); - this._addTail(); - } - } - } - if (this._needsRemeasure || this._needsReset) { - this._indexToMeasure = {}; - } - // Retrieve DOM to be measured. - // Do it right before cleanup and reset of properties. - const shouldMeasure = this._num > 0 && this._measureCallback && - (rangeChanged || this._needsRemeasure || this._needsReset); - const toMeasure = shouldMeasure ? this._toMeasure : null; - - // Cleanup. - if (!this._incremental) { - this._prevActive.forEach((idx, child) => this._unassignChild(child, idx)); - this._prevActive.clear(); - } - // Reset internal properties. - this._prevFirst = this._first; - this._prevLast = this._last; - this._prevNum = this._num; - this._needsReset = false; - this._needsRemeasure = false; - - // Notify render completed. - this._didRender(); - // Measure DOM. - if (toMeasure) { - this._measureChildren(toMeasure); - } - } - - /** - * Invoked after DOM is updated, and before it gets measured. - * @protected - */ - _didRender() { - } - - /** - * @private - */ - _discardHead() { - const o = this._ordered; - for (let idx = this._prevFirst; o.length && idx < this._first; idx++) { - this._unassignChild(o.shift(), idx); - } - } - - /** - * @private - */ - _discardTail() { - const o = this._ordered; - for (let idx = this._prevLast; o.length && idx > this._last; idx--) { - this._unassignChild(o.pop(), idx); - } - } - - /** - * @private - */ - _addHead() { - const start = this._first; - const end = Math.min(this._last, this._prevFirst - 1); - for (let idx = end; idx >= start; idx--) { - const child = this._assignChild(idx); - if (this._manageDom) { - if (this._maintainDomOrder || !this._childIsAttached(child)) { - this._insertBefore(child, this._firstChild); - } - } - if (this.updateElement) { - this.updateElement(child, idx); - } - this._ordered.unshift(child); - } - } - - /** - * @private - */ - _addTail() { - const start = Math.max(this._first, this._prevLast + 1); - const end = this._last; - for (let idx = start; idx <= end; idx++) { - const child = this._assignChild(idx); - if (this._manageDom) { - if (this._maintainDomOrder || !this._childIsAttached(child)) { - this._insertBefore(child, null); - } - } - if (this.updateElement) { - this.updateElement(child, idx); - } - this._ordered.push(child); - } - } - - /** - * @param {number} first - * @param {number} last - * @private - */ - _reset(first, last) { - const len = last - first + 1; - // Explain why swap prevActive with active - affects _assignChild. - const prevActive = this._active; - this._active = this._prevActive; - this._prevActive = prevActive; - let currentMarker = this._manageDom && this._firstChild; - this._ordered.length = 0; - for (let n = 0; n < len; n++) { - const idx = first + n; - const child = this._assignChild(idx); - this._ordered.push(child); - if (this._manageDom) { - if (currentMarker && this._maintainDomOrder) { - if (currentMarker === this._node(child)) { - currentMarker = this._nextSibling(child); - } else { - this._insertBefore(child, currentMarker); - } - } else if (!this._childIsAttached(child)) { - this._insertBefore(child, null); - } - } - if (this.updateElement) { - this.updateElement(child, idx); - } - } - } - - /** - * @param {number} idx - * @private - */ - _assignChild(idx) { - const key = this.elementKey ? this.elementKey(idx) : idx; - let child; - if (child = this._keyToChild.get(key)) { - this._prevActive.delete(child); - } else { - child = this.createElement(idx); - this._keyToChild.set(key, child); - this._childToKey.set(child, key); - } - this._showChild(child); - this._active.set(child, idx); - return child; - } - - /** - * @param {*} child - * @param {number} idx - * @private - */ - _unassignChild(child, idx) { - this._hideChild(child); - if (this._incremental) { - this._active.delete(child); - this._prevActive.set(child, idx); - } else { - const key = this._childToKey.get(child); - this._childToKey.delete(child); - this._keyToChild.delete(key); - this._active.delete(child); - if (this.recycleElement) { - this.recycleElement(child, idx); - } else if (this._node(child).parentNode) { - this._removeChild(child); - } - } - } - - // TODO: Is this the right name? - /** - * @private - */ - get _firstChild() { - return this._ordered.length && this._childIsAttached(this._ordered[0]) ? - this._node(this._ordered[0]) : - null; - } - - // Overridable abstractions for child manipulation - /** - * @protected - */ - _node(child) { - return child; - } - /** - * @protected - */ - _nextSibling(child) { - return child.nextSibling; - } - /** - * @protected - */ - _insertBefore(child, referenceNode) { - this._container.insertBefore(child, referenceNode); - } - /** - * @protected - */ - _childIsAttached(child) { - const node = this._node(child); - return node && node.parentNode === this._container; - } - /** - * @protected - */ - _hideChild(child) { - if (child.style) { - child.style.display = 'none'; - } - } - /** - * @protected - */ - _showChild(child) { - if (child.style) { - child.style.display = null; - } - } - - /** - * - * @param {!Element} child - * @return {{width: number, height: number, marginTop: number, marginBottom: number, marginLeft: number, marginRight: number}} childMeasures - * @protected - */ - _measureChild(child) { - // offsetWidth doesn't take transforms in consideration, - // so we use getBoundingClientRect which does. - const {width, height} = child.getBoundingClientRect(); - // console.debug(`_measureChild #${this._container.id} > #${ - // child.id}: height: ${height}px`); - return Object.assign({width, height}, getMargins(child)); - } - - /** - * Remove child. - * Override to control child removal. - * - * @param {*} child - * @protected - */ - _removeChild(child) { - child.parentNode.removeChild(child); - } -} - -function getMargins(el) { - const style = window.getComputedStyle(el); - // console.log(el.id, style.position); - return { - marginLeft: getMarginValue(style.marginLeft), - marginRight: getMarginValue(style.marginRight), - marginTop: getMarginValue(style.marginTop), - marginBottom: getMarginValue(style.marginBottom), - }; -} - -function getMarginValue(value) { - value = value ? parseFloat(value) : NaN; - return value !== value ? 0 : value; -} - -export const VirtualRepeater = Repeats(class {});
\ No newline at end of file diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.mjs b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.mjs deleted file mode 100644 index 049501e6ec9..00000000000 --- a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.mjs +++ /dev/null @@ -1,444 +0,0 @@ -import {Repeats} from './virtual-repeater.mjs'; - -export class RangeChangeEvent extends Event { - constructor(type, init) { - super(type, init); - this._first = Math.floor(init.first || 0); - this._last = Math.floor(init.last || 0); - } - get first() { - return this._first; - } - get last() { - return this._last; - } -} - -export const RepeatsAndScrolls = Superclass => class extends Repeats -(Superclass) { - constructor(config) { - super(); - this._num = 0; - this._first = -1; - this._last = -1; - this._prevFirst = -1; - this._prevLast = -1; - - this._needsUpdateView = false; - this._containerElement = null; - this._layout = null; - this._scrollTarget = null; - // Keep track of original inline style of the container, - // so it can be restored when container is changed. - this._containerInlineStyle = null; - // A sentinel element that sizes the container when - // it is a scrolling element. - this._sizer = null; - // Layout provides these values, we set them on _render(). - this._scrollSize = null; - this._scrollErr = null; - this._childrenPos = null; - - this._containerSize = null; - this._containerRO = new ResizeObserver( - (entries) => this._containerSizeChanged(entries[0].contentRect)); - - this._skipNextChildrenSizeChanged = false; - this._childrenRO = - new ResizeObserver((entries) => this._childrenSizeChanged(entries)); - - if (config) { - Object.assign(this, config); - } - } - - get container() { - return this._container; - } - set container(container) { - super.container = container; - - const oldEl = this._containerElement; - // Consider document fragments as shadowRoots. - const newEl = - (container && container.nodeType === Node.DOCUMENT_FRAGMENT_NODE) ? - container.host : - container; - if (oldEl === newEl) { - return; - } - - this._containerRO.disconnect(); - this._containerSize = null; - - if (oldEl) { - if (this._containerInlineStyle) { - oldEl.setAttribute('style', this._containerInlineStyle); - } else { - oldEl.removeAttribute('style'); - } - this._containerInlineStyle = null; - if (oldEl === this._scrollTarget) { - oldEl.removeEventListener('scroll', this, {passive: true}); - this._sizer && this._sizer.remove(); - } - } else { - // First time container was setup, add listeners only now. - addEventListener('scroll', this, {passive: true}); - } - - this._containerElement = newEl; - - if (newEl) { - this._containerInlineStyle = newEl.getAttribute('style') || null; - if (newEl === this._scrollTarget) { - this._sizer = this._sizer || this._createContainerSizer(); - this._container.prepend(this._sizer); - } - this._scheduleUpdateView(); - this._containerRO.observe(newEl); - } - } - - get layout() { - return this._layout; - } - set layout(layout) { - if (layout === this._layout) { - return; - } - - if (this._layout) { - this._measureCallback = null; - this._layout.removeEventListener('scrollsizechange', this); - this._layout.removeEventListener('scrollerrorchange', this); - this._layout.removeEventListener('itempositionchange', this); - this._layout.removeEventListener('rangechange', this); - // Reset container size so layout can get correct viewport size. - if (this._containerElement) { - this._sizeContainer(); - } - } - - this._layout = layout; - - if (this._layout) { - if (typeof this._layout.updateItemSizes === 'function') { - this._measureCallback = this._layout.updateItemSizes.bind(this._layout); - this.requestRemeasure(); - } - this._layout.addEventListener('scrollsizechange', this); - this._layout.addEventListener('scrollerrorchange', this); - this._layout.addEventListener('itempositionchange', this); - this._layout.addEventListener('rangechange', this); - this._scheduleUpdateView(); - } - } - - /** - * The element that generates scroll events and defines the container - * viewport. The value `null` (default) corresponds to `window` as scroll - * target. - * @type {Element|null} - */ - get scrollTarget() { - return this._scrollTarget; - } - /** - * @param {Element|null} target - */ - set scrollTarget(target) { - // Consider window as null. - if (target === window) { - target = null; - } - if (this._scrollTarget === target) { - return; - } - if (this._scrollTarget) { - this._scrollTarget.removeEventListener('scroll', this, {passive: true}); - if (this._sizer && this._scrollTarget === this._containerElement) { - this._sizer.remove(); - } - } - - this._scrollTarget = target; - - if (target) { - target.addEventListener('scroll', this, {passive: true}); - if (target === this._containerElement) { - this._sizer = this._sizer || this._createContainerSizer(); - this._container.prepend(this._sizer); - } - } - } - - /** - * @protected - */ - _render() { - // console.time(`render ${this._containerElement.localName}#${ - // this._containerElement.id}`); - - this._childrenRO.disconnect(); - - // Update layout properties before rendering to have correct - // first, num, scroll size, children positions. - this._layout.totalItems = this.totalItems; - if (this._needsUpdateView) { - this._needsUpdateView = false; - this._updateView(); - } - this._layout.reflowIfNeeded(); - // Keep rendering until there is no more scheduled renders. - while (true) { - if (this._pendingRender) { - cancelAnimationFrame(this._pendingRender); - this._pendingRender = null; - } - // Update scroll size and correct scroll error before rendering. - this._sizeContainer(this._scrollSize); - if (this._scrollErr) { - // This triggers a 'scroll' event (async) which triggers another - // _updateView(). - this._correctScrollError(this._scrollErr); - this._scrollErr = null; - } - // Position children (_didRender()), and provide their measures to layout. - super._render(); - this._layout.reflowIfNeeded(); - // If layout reflow did not provoke another render, we're done. - if (!this._pendingRender) { - break; - } - } - // We want to skip the first ResizeObserver callback call as we already - // measured the children. - this._skipNextChildrenSizeChanged = true; - this._kids.forEach(child => this._childrenRO.observe(child)); - - // console.timeEnd(`render ${this._containerElement.localName}#${ - // this._containerElement.id}`); - } - - /** - * Position children before they get measured. - * Measuring will force relayout, so by positioning - * them first, we reduce computations. - * @protected - */ - _didRender() { - if (this._childrenPos) { - this._positionChildren(this._childrenPos); - this._childrenPos = null; - } - } - - /** - * @param {!Event} event - * @private - */ - handleEvent(event) { - switch (event.type) { - case 'scroll': - if (!this._scrollTarget || event.target === this._scrollTarget) { - this._scheduleUpdateView(); - } - break; - case 'scrollsizechange': - this._scrollSize = event.detail; - this._scheduleRender(); - break; - case 'scrollerrorchange': - this._scrollErr = event.detail; - this._scheduleRender(); - break; - case 'itempositionchange': - this._childrenPos = event.detail; - this._scheduleRender(); - break; - case 'rangechange': - this._adjustRange(event.detail); - break; - default: - console.warn('event not handled', event); - } - } - /** - * @return {!Element} - * @private - */ - _createContainerSizer() { - const sizer = document.createElement('div'); - // When the scrollHeight is large, the height - // of this element might be ignored. - // Setting content and font-size ensures the element - // has a size. - Object.assign(sizer.style, { - position: 'absolute', - margin: '-2px 0 0 0', - padding: 0, - visibility: 'hidden', - fontSize: '2px', - }); - sizer.innerHTML = ' '; - return sizer; - } - - // Rename _ordered to _kids? - /** - * @protected - */ - get _kids() { - return this._ordered; - } - /** - * @private - */ - _scheduleUpdateView() { - this._needsUpdateView = true; - this._scheduleRender(); - } - /** - * @private - */ - _updateView() { - let width, height, top, left; - if (this._scrollTarget === this._containerElement) { - width = this._containerSize.width; - height = this._containerSize.height; - left = this._containerElement.scrollLeft; - top = this._containerElement.scrollTop; - } else { - const containerBounds = this._containerElement.getBoundingClientRect(); - const scrollBounds = this._scrollTarget ? - this._scrollTarget.getBoundingClientRect() : - {top: 0, left: 0, width: innerWidth, height: innerHeight}; - const scrollerWidth = scrollBounds.width; - const scrollerHeight = scrollBounds.height; - const xMin = Math.max( - 0, Math.min(scrollerWidth, containerBounds.left - scrollBounds.left)); - const yMin = Math.max( - 0, Math.min(scrollerHeight, containerBounds.top - scrollBounds.top)); - const xMax = this._layout.direction === 'vertical' ? - Math.max( - 0, - Math.min( - scrollerWidth, containerBounds.right - scrollBounds.left)) : - scrollerWidth; - const yMax = this._layout.direction === 'vertical' ? - scrollerHeight : - Math.max( - 0, - Math.min( - scrollerHeight, containerBounds.bottom - scrollBounds.top)); - width = xMax - xMin; - height = yMax - yMin; - left = Math.max(0, -(containerBounds.x - scrollBounds.left)); - top = Math.max(0, -(containerBounds.y - scrollBounds.top)); - } - this._layout.viewportSize = {width, height}; - this._layout.viewportScroll = {top, left}; - } - /** - * @private - */ - _sizeContainer(size) { - if (this._scrollTarget === this._containerElement) { - const left = size && size.width ? size.width - 1 : 0; - const top = size && size.height ? size.height - 1 : 0; - this._sizer.style.transform = `translate(${left}px, ${top}px)`; - } else { - const style = this._containerElement.style; - style.minWidth = size && size.width ? size.width + 'px' : null; - style.minHeight = size && size.height ? size.height + 'px' : null; - } - } - /** - * @private - */ - _positionChildren(pos) { - const kids = this._kids; - Object.keys(pos).forEach(key => { - const idx = key - this._first; - const child = kids[idx]; - if (child) { - const {top, left} = pos[key]; - // console.debug(`_positionChild #${this._container.id} > - // #${child.id}: top ${top}`); - child.style.position = 'absolute'; - child.style.transform = `translate(${left}px, ${top}px)`; - } - }); - } - /** - * @private - */ - _adjustRange(range) { - this.num = range.num; - this.first = range.first; - this._incremental = !(range.stable); - if (range.remeasure) { - this.requestRemeasure(); - } else if (range.stable) { - this._notifyStable(); - } - } - /** - * @protected - */ - _shouldRender() { - if (!super._shouldRender() || !this._layout) { - return false; - } - // NOTE: we're about to render, but the ResizeObserver didn't execute yet. - // Since we want to keep rAF timing, we compute _containerSize now. - // Would be nice to have a way to flush ResizeObservers - if (this._containerSize === null) { - const {width, height} = this._containerElement.getBoundingClientRect(); - this._containerSize = {width, height}; - } - return this._containerSize.width > 0 || this._containerSize.height > 0; - } - /** - * @private - */ - _correctScrollError(err) { - if (this._scrollTarget) { - this._scrollTarget.scrollTop -= err.top; - this._scrollTarget.scrollLeft -= err.left; - } else { - window.scroll(window.scrollX - err.left, window.scrollY - err.top); - } - } - /** - * @protected - */ - _notifyStable() { - const {first, num} = this; - const last = first + num - 1; - this._container.dispatchEvent( - new RangeChangeEvent('rangechange', {first, last})); - } - /** - * @private - */ - _containerSizeChanged(size) { - const {width, height} = size; - this._containerSize = {width, height}; - // console.debug('container changed size', this._containerSize); - this._scheduleUpdateView(); - } - /** - * @private - */ - _childrenSizeChanged() { - if (this._skipNextChildrenSizeChanged) { - this._skipNextChildrenSizeChanged = false; - } else { - this.requestRemeasure(); - } - } -}; - -export const VirtualScroller = RepeatsAndScrolls(class {}); |