summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js')
-rw-r--r--chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js444
1 files changed, 444 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js
new file mode 100644
index 00000000000..b0d240498c9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/script/resources/layered_api/virtual-scroller/virtual-scroller.js
@@ -0,0 +1,444 @@
+import {Repeats} from './virtual-repeater.js';
+
+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 {});