Commit 00ef7646 authored by Hiroshige Hayashizaki's avatar Hiroshige Hayashizaki Committed by Commit Bot

[Layered API] Implement std:virtual-list behind a flag

This CL imports the virtual-list implementation as-is except for
renaming virtual-list-element.js to index.js.

Original author:
    Valdrin Koshi (valdrin@google.com) and
    Gray Norton (graynorton@google.com)
https://github.com/valdrinkoshi/virtual-list

All code were written and contributed by Googlers
who are Chromium Contributors.

Tentatively this CL imports the files under core/script/resources
until a long-term plan for repository location etc. is settled.

This is a still experimental feature behind a flag, and its
API interface and implementation can be changed significantly
before shipped.

Change-Id: I07071ae2dd17481d3240198f91f28cf0cef5536d
Reviewed-on: https://chromium-review.googlesource.com/1031749
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarKouhei Ueno <kouhei@chromium.org>
Reviewed-by: default avatarAlex Russell <slightlyoff@chromium.org>
Reviewed-by: default avatarOjan Vafai <ojan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#555963}
parent 222512a0
......@@ -51,6 +51,13 @@
<!-- Layered API scripts. Should be consistent with kLayeredAPIResources
in renderer/core/script/layered_api.cc -->
<include name="IDR_LAYERED_API_BLANK_INDEX_JS" file="../renderer/core/script/resources/layered_api/blank/index.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_INDEX_JS" file="../renderer/core/script/resources/layered_api/virtual-list/index.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_BASE_JS" file="../renderer/core/script/resources/layered_api/virtual-list/layouts/layout-1d-base.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_GRID_JS" file="../renderer/core/script/resources/layered_api/virtual-list/layouts/layout-1d-grid.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_JS" file="../renderer/core/script/resources/layered_api/virtual-list/layouts/layout-1d.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_VIRTUAL_LIST_JS" file="../renderer/core/script/resources/layered_api/virtual-list/virtual-list.js" type="BINDATA" skip_minify="true"/>
<include name="IDR_LAYERED_API_VIRTUAL_LIST_VIRTUAL_REPEATER_JS" file="../renderer/core/script/resources/layered_api/virtual-list/virtual-repeater.js" type="BINDATA" skip_minify="true"/>
</includes>
</release>
</grit>
......@@ -25,6 +25,18 @@ struct LayeredAPIResource {
const LayeredAPIResource kLayeredAPIResources[] = {
{"blank/index.js", IDR_LAYERED_API_BLANK_INDEX_JS},
{"virtual-list/index.js", IDR_LAYERED_API_VIRTUAL_LIST_INDEX_JS},
{"virtual-list/layouts/layout-1d-base.js",
IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_BASE_JS},
{"virtual-list/layouts/layout-1d-grid.js",
IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_GRID_JS},
{"virtual-list/layouts/layout-1d.js",
IDR_LAYERED_API_VIRTUAL_LIST_LAYOUTS_LAYOUT_1D_JS},
{"virtual-list/virtual-list.js",
IDR_LAYERED_API_VIRTUAL_LIST_VIRTUAL_LIST_JS},
{"virtual-list/virtual-repeater.js",
IDR_LAYERED_API_VIRTUAL_LIST_VIRTUAL_REPEATER_JS},
};
int GetResourceIDFromPath(const String& path) {
......
Name: virtual-list Layered API
URL: https://github.com/valdrinkoshi/virtual-list
Version: 4bbc08999515cdf3ce944a150bc6d9949add8978
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-list-element.js to index.js)
import {VirtualList} from './virtual-list.js';
/** Properties */
const _items = Symbol();
const _list = Symbol();
const _newChild = Symbol();
const _updateChild = Symbol();
const _recycleChild = Symbol();
const _itemKey = Symbol();
const _grid = Symbol();
const _horizontal = Symbol();
const _pendingRender = Symbol();
/** Functions */
const _render = Symbol();
const _scheduleRender = Symbol();
// Lazily loaded Layout classes.
const dynamicImports = {};
const importLayoutClass = async (url) => {
if (!dynamicImports[url]) {
dynamicImports[url] = import(url).then(module => module.default);
}
return await dynamicImports[url];
};
export class VirtualListElement extends HTMLElement {
constructor() {
super();
this[_items] = null;
this[_list] = null;
this[_newChild] = null;
this[_updateChild] = null;
this[_recycleChild] = null;
this[_itemKey] = null;
this[_grid] = false;
this[_horizontal] = false;
this[_pendingRender] = null;
}
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({mode: 'open'}).innerHTML = `
<style>
:host {
display: block;
position: relative;
contain: strict;
}
::slotted(*) {
box-sizing: border-box;
max-width: 100%;
max-height: 100%;
}
</style>
<slot></slot>`;
}
this[_scheduleRender]();
}
static get observedAttributes() {
return ['layout'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'layout') {
this.layout = newVal;
}
}
get newChild() {
return this[_newChild];
}
set newChild(fn) {
this[_newChild] = fn;
this[_scheduleRender]();
}
get updateChild() {
return this[_updateChild];
}
set updateChild(fn) {
this[_updateChild] = fn;
this[_scheduleRender]();
}
get recycleChild() {
return this[_recycleChild];
}
set recycleChild(fn) {
this[_recycleChild] = fn;
this[_scheduleRender]();
}
get itemKey() {
return this[_itemKey];
}
set itemKey(fn) {
this[_itemKey] = fn;
this[_scheduleRender]();
}
get layout() {
const prefix = this[_horizontal] ? 'horizontal' : 'vertical';
const suffix = this[_grid] ? '-grid' : '';
return prefix + suffix;
}
set layout(layout) {
this[_horizontal] = layout && layout.startsWith('horizontal');
this[_grid] = layout && layout.endsWith('-grid');
this[_scheduleRender]();
}
get items() {
return this[_items];
}
set items(items) {
this[_items] = items;
this[_scheduleRender]();
}
requestReset() {
if (this[_list]) {
this[_list].requestReset();
}
}
[_scheduleRender]() {
if (!this[_pendingRender]) {
this[_pendingRender] = Promise.resolve().then(() => {
this[_pendingRender] = null;
this[_render]();
});
}
}
async[_render]() {
if (!this.newChild) {
return;
}
// Delay init to first connected as list needs to measure
// sizes of container and children.
if (!this[_list] && !this.isConnected) {
return;
}
if (!this[_list]) {
this[_list] = new VirtualList({container: this});
}
const list = this[_list];
const {newChild, updateChild, recycleChild, itemKey, items} = this;
Object.assign(list, {newChild, updateChild, recycleChild, itemKey, items});
const Layout = await importLayoutClass(
this[_grid] ? './layouts/layout-1d-grid.js' : './layouts/layout-1d.js');
const direction = this[_horizontal] ? 'horizontal' : 'vertical';
if (list.layout instanceof Layout === false ||
list.layout.direction !== direction) {
list.layout = new Layout({direction});
}
}
}
customElements.define('virtual-list', VirtualListElement);
\ No newline at end of file
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._virtualScroll = false;
this._sizeDim = 'height';
this._secondarySizeDim = 'width';
this._positionDim = 'top';
this._secondaryPositionDim = 'left';
this._direction = 'vertical';
this._scrollPosition = 0;
this._viewportSize = {width: 0, height: 0};
this._totalItems = 0;
this._scrollSize = 1;
this._overhang = 150;
Object.assign(this, config);
}
// public properties
set virtualScroll(bool) {
this._virtualScroll = bool;
}
get virtualScroll() {
return this._virtualScroll;
}
set spacing(px) {
if (px !== this._spacing) {
this._spacing = px;
this._scheduleReflow();
}
}
get spacing() {
return this._spacing;
}
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();
}
}
}
_itemDim2Changed() {
// Override
}
get _delta() {
return this._itemDim1 + this._spacing;
}
get _itemDim1() {
return this._itemSize[this._sizeDim];
}
get _itemDim2() {
return this._itemSize[this._secondarySizeDim];
}
get itemSize() {
return this._itemSize;
}
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 direction() {
return this._direction;
}
set viewportSize(dims) {
const {_viewDim1, _viewDim2} = this;
Object.assign(this._viewportSize, dims);
if (_viewDim1 !== this._viewDim1 || _viewDim2 !== this._viewDim2) {
if (_viewDim2 !== this._viewDim2) {
this._viewDim2Changed();
} else {
this._checkThresholds();
}
}
}
_viewDim2Changed() {
// Override
}
get _viewDim1() {
return this._viewportSize[this._sizeDim];
}
get _viewDim2() {
return this._viewportSize[this._secondarySizeDim];
}
get viewportSize() {
return this._viewportSize;
}
set totalItems(num) {
if (num !== this._totalItems) {
this._totalItems = num;
this._maxIdx = num - 1;
this._scheduleReflow();
}
}
get totalItems() {
return this._totalItems;
}
// private properties
get _num() {
if (this._first === -1 || this._last === -1) {
return 0;
}
return this._last - this._first + 1;
}
// public methods
scrollTo(coords) {
this._latestCoords = coords;
this._scroll();
}
//
_scroll() {
this._scrollPosition = this._latestCoords[this._positionDim];
this._checkThresholds();
}
_getActiveItems() {
// Override
}
// TODO: Does this need to be public?
_reflow() {
const {_first, _last, _scrollSize} = this;
this._updateScrollSize();
this._getActiveItems();
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._pendingReflow = null;
}
_scheduleReflow() {
if (!this._pendingReflow) {
this._pendingReflow = Promise.resolve().then(() => this._reflow());
}
}
_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();
}
}
}
///
_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}));
}
_getItemPosition(idx) {
// Override.
}
}
\ No newline at end of file
import Layout1dBase from './layout-1d-base.js';
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 {_scrollPosition, _scrollSize} = this;
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
import Layout1dBase from './layout-1d-base.js';
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._scrollError = 0;
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() {
this._itemSize[this._sizeDim] = 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)));
}
_setAnchor(lower, upper) {
if (this._physicalItems.size === 0) {
return this._calculateAnchor(lower, upper);
}
if (this._first < 0) {
console.error('_setAnchor: negative _first');
return this._calculateAnchor(lower, upper);
}
if (this._last < 0) {
console.error('_setAnchor: 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;
if (this._anchorIdx === null || this._anchorPos === null) {
this._anchorIdx = this._setAnchor(lower, upper);
this._anchorPos = this._getPosition(this._anchorIdx);
}
let anchorSize = this._getSize(this._anchorIdx);
if (anchorSize === undefined) {
anchorSize = this._itemDim1;
}
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--;
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;
}
// TODO: Can this be made to inherit from base, with proper hooks?
_reflow() {
const {_first, _last, _scrollSize} = this;
this._updateScrollSize();
this._getActiveItems();
if (this._scrollSize !== _scrollSize) {
this._emitScrollSize();
}
if (this._first === -1 && this._last === -1) {
this._emitRange();
this._resetReflowState();
} else if (
this._first !== _first || this._last !== _last ||
this._needsRemeasure) {
this._emitRange();
this._emitScrollError();
this._emitChildPositions();
} else {
this._emitRange();
this._emitScrollError();
this._emitChildPositions();
this._resetReflowState();
}
this._pendingReflow = null;
}
_resetReflowState() {
this._anchorIdx = null;
this._anchorPos = null;
this._stable = true;
}
_getItemPosition(idx) {
return {
[this._positionDim]: this._getPosition(idx),
[this._secondaryPositionDim]: 0
}
}
_viewDim2Changed() {
this._needsRemeasure = true;
this._scheduleReflow();
}
_emitRange() {
const remeasure = this._needsRemeasure;
const stable = this._stable;
this._needsRemeasure = false;
super._emitRange({remeasure, stable});
}
}
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._pendingUpdateView = null;
this._isContainerVisible = false;
this._containerElement = null;
if (config) {
Object.assign(this, config);
}
}
get container() {
return this._container;
}
set container(container) {
if (container === this._container) {
return;
}
removeEventListener('scroll', this);
removeEventListener('resize', this);
super.container = container;
if (container) {
addEventListener('scroll', this);
addEventListener('resize', this);
this._scheduleUpdateView();
}
// Update the containerElement, copy min-width/height styles to new
// container.
let containerStyle = null;
if (this._containerElement) {
containerStyle = this._containerElement.getAttribute('style');
this._containerElement.removeAttribute('style');
}
// Consider document fragments as shadowRoots.
this._containerElement =
(container && container.nodeType === Node.DOCUMENT_FRAGMENT_NODE) ?
container.host :
container;
if (this._containerElement && containerStyle) {
this._containerElement.setAttribute('style', containerStyle);
}
}
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);
// Remove min-width/height from containerElement so
// layout can get correct viewport size.
if (this._containerElement) {
this._containerElement.removeAttribute('style');
this.requestRemeasure();
}
}
this._layout = layout;
if (this._layout) {
if (typeof this._layout.updateItemSizes === 'function') {
this._measureCallback = this._layout.updateItemSizes.bind(this._layout);
}
this._layout.addEventListener('scrollsizechange', this);
this._layout.addEventListener('scrollerrorchange', this);
this._layout.addEventListener('itempositionchange', this);
this._layout.addEventListener('rangechange', this);
this._scheduleUpdateView();
}
}
requestReset() {
super.requestReset();
this._scheduleUpdateView();
}
/**
* @param {!Event} event
* @private
*/
handleEvent(event) {
switch (event.type) {
case 'scroll':
case 'resize':
this._scheduleUpdateView();
break;
case 'scrollsizechange':
this._sizeContainer(event.detail);
break;
case 'scrollerrorchange':
this._correctScrollError(event.detail);
break;
case 'itempositionchange':
this._positionChildren(event.detail);
break;
case 'rangechange':
this._adjustRange(event.detail);
break;
default:
console.warn('event not handled', event);
}
}
// Rename _ordered to _kids?
/**
* @protected
*/
get _kids() {
return this._ordered;
}
/**
* @private
*/
_scheduleUpdateView() {
if (!this._pendingUpdateView && this._container && this._layout) {
this._pendingUpdateView =
Promise.resolve().then(() => this._updateView());
}
}
/**
* @private
*/
_updateView() {
this._pendingUpdateView = null;
this._layout.totalItems = this._items ? this._items.length : 0;
const listBounds = this._containerElement.getBoundingClientRect();
// Avoid updating viewport if container is not visible.
this._isContainerVisible = Boolean(
listBounds.width || listBounds.height || listBounds.top ||
listBounds.left);
if (!this._isContainerVisible) {
return;
}
const scrollerWidth = window.innerWidth;
const scrollerHeight = window.innerHeight;
const xMin = Math.max(0, Math.min(scrollerWidth, listBounds.left));
const yMin = Math.max(0, Math.min(scrollerHeight, listBounds.top));
const xMax = this._layout.direction === 'vertical' ?
Math.max(0, Math.min(scrollerWidth, listBounds.right)) :
scrollerWidth;
const yMax = this._layout.direction === 'vertical' ?
scrollerHeight :
Math.max(0, Math.min(scrollerHeight, listBounds.bottom));
const width = xMax - xMin;
const height = yMax - yMin;
this._layout.viewportSize = {width, height};
const left = Math.max(0, -listBounds.x);
const top = Math.max(0, -listBounds.y);
this._layout.scrollTo({top, left});
}
/**
* @private
*/
_sizeContainer(size) {
const style = this._containerElement.style;
style.minWidth = size.width ? size.width + 'px' : null;
style.minHeight = size.height ? size.height + 'px' : null;
}
/**
* @private
*/
async _positionChildren(pos) {
await Promise.resolve();
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() {
return Boolean(
this._isContainerVisible && this._layout && super._shouldRender());
}
/**
* @private
*/
_correctScrollError(err) {
window.scroll(window.scrollX - err.left, window.scrollY - err.top);
}
/**
* @protected
*/
_notifyStable() {
const {first, num} = this;
const last = first + num;
this._container.dispatchEvent(
new RangeChangeEvent('rangechange', {first, last}));
}
};
export const VirtualList = RepeatsAndScrolls(class {});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment