Commit f1ddcc97 authored by Fergal Daly's avatar Fergal Daly Committed by Commit Bot

Add a virtual-content element that uses display locking to make content

low cost when off-screen.

Exposes .mjs files as http-available files for web_tests. This required
adding .mjs to the mime config and various things in BUILD files.

Add .mjs as javascript to the mime configs for webtest server.

Delete old virtual-scroller code.

Bug: 979108
Change-Id: Idfde0bb23263b571fc3abf98845bb7d0327baf07
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1680170
Commit-Queue: Fergal Daly <fergal@chromium.org>
Reviewed-by: default avatarTakuto Ikuta <tikuta@chromium.org>
Reviewed-by: default avatarKent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#676308}
parent ae1771a0
...@@ -1029,6 +1029,7 @@ if (!is_ios) { ...@@ -1029,6 +1029,7 @@ if (!is_ios) {
"//testing/buildbot/filters:blink_web_tests_filter", "//testing/buildbot/filters:blink_web_tests_filter",
"//third_party/blink/public:blink_devtools_frontend_resources_files", "//third_party/blink/public:blink_devtools_frontend_resources_files",
"//third_party/blink/public/mojom:mojom_platform_js_data_deps", "//third_party/blink/public/mojom:mojom_platform_js_data_deps",
"//third_party/blink/renderer/core/script:js_files_for_web_tests",
"//third_party/mesa_headers", "//third_party/mesa_headers",
"//tools/imagediff", "//tools/imagediff",
] ]
......
...@@ -75,3 +75,24 @@ blink_core_sources("script") { ...@@ -75,3 +75,24 @@ blink_core_sources("script") {
jumbo_excluded_sources = [ "modulator.cc" ] # https://crbug.com/716395 jumbo_excluded_sources = [ "modulator.cc" ] # https://crbug.com/716395
} }
copy("layered_apis_elements_virtual_scroller_js") {
testonly = true
sources = [
"resources/layered_api/elements/virtual-scroller/find-element.mjs",
"resources/layered_api/elements/virtual-scroller/sets.mjs",
"resources/layered_api/elements/virtual-scroller/visibility-manager.mjs",
]
outputs = [
"{{source_gen_dir}}/{{source_file_part}}",
]
}
group("js_files_for_web_tests") {
testonly = true
data_deps = [
":layered_apis_elements_virtual_scroller_js",
]
}
...@@ -18,8 +18,8 @@ enum class Module { ...@@ -18,8 +18,8 @@ enum class Module {
kElementsInternal, kElementsInternal,
kElementsSwitch, kElementsSwitch,
kElementsToast, kElementsToast,
kElementsVirtualScroller,
kKvStorage, kKvStorage,
kVirtualScroller,
}; };
} // namespace layered_api } // namespace layered_api
......
...@@ -45,6 +45,19 @@ const LayeredAPIResource kLayeredAPIResources[] = { ...@@ -45,6 +45,19 @@ const LayeredAPIResource kLayeredAPIResources[] = {
{"elements/toast/index.mjs", IDR_LAYERED_API_ELEMENTS_TOAST_INDEX_MJS, {"elements/toast/index.mjs", IDR_LAYERED_API_ELEMENTS_TOAST_INDEX_MJS,
Module::kElementsToast}, Module::kElementsToast},
{"elements/virtual-scroller/find-element.mjs",
IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_FIND_ELEMENT_MJS,
Module::kElementsVirtualScroller},
{"elements/virtual-scroller/index.mjs",
IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_INDEX_MJS,
Module::kElementsVirtualScroller},
{"elements/virtual-scroller/sets.mjs",
IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_SETS_MJS,
Module::kElementsVirtualScroller},
{"elements/virtual-scroller/visibility-manager.mjs",
IDR_LAYERED_API_ELEMENTS_VIRTUAL_SCROLLER_VISIBILITY_MANAGER_MJS,
Module::kElementsVirtualScroller},
{"kv-storage/async_iterator.mjs", {"kv-storage/async_iterator.mjs",
IDR_LAYERED_API_KV_STORAGE_ASYNC_ITERATOR_MJS, Module::kKvStorage}, IDR_LAYERED_API_KV_STORAGE_ASYNC_ITERATOR_MJS, Module::kKvStorage},
{"kv-storage/idb_utils.mjs", IDR_LAYERED_API_KV_STORAGE_IDB_UTILS_MJS, {"kv-storage/idb_utils.mjs", IDR_LAYERED_API_KV_STORAGE_IDB_UTILS_MJS,
...@@ -52,28 +65,6 @@ const LayeredAPIResource kLayeredAPIResources[] = { ...@@ -52,28 +65,6 @@ const LayeredAPIResource kLayeredAPIResources[] = {
{"kv-storage/index.mjs", IDR_LAYERED_API_KV_STORAGE_INDEX_MJS, {"kv-storage/index.mjs", IDR_LAYERED_API_KV_STORAGE_INDEX_MJS,
Module::kKvStorage}, Module::kKvStorage},
{"virtual-scroller/index.mjs", IDR_LAYERED_API_VIRTUAL_SCROLLER_INDEX_MJS,
Module::kVirtualScroller},
{"virtual-scroller/item-source.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_ITEM_SOURCE_MJS,
Module::kVirtualScroller},
{"virtual-scroller/virtual-repeater.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_VIRTUAL_REPEATER_MJS,
Module::kVirtualScroller},
{"virtual-scroller/virtual-scroller.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_VIRTUAL_SCROLLER_MJS,
Module::kVirtualScroller},
{"virtual-scroller/layouts/layout-1d-base.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_BASE_MJS,
Module::kVirtualScroller},
{"virtual-scroller/layouts/layout-1d-grid.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_GRID_MJS,
Module::kVirtualScroller},
{"virtual-scroller/layouts/layout-1d.mjs",
IDR_LAYERED_API_VIRTUAL_SCROLLER_LAYOUTS_LAYOUT_1D_MJS,
Module::kVirtualScroller},
}; };
} // namespace } // namespace
......
...@@ -59,8 +59,6 @@ bool ModulatorImplBase::BuiltInModuleEnabled(layered_api::Module module) const { ...@@ -59,8 +59,6 @@ bool ModulatorImplBase::BuiltInModuleEnabled(layered_api::Module module) const {
switch (module) { switch (module) {
case layered_api::Module::kBlank: case layered_api::Module::kBlank:
return true; return true;
case layered_api::Module::kVirtualScroller:
return false;
case layered_api::Module::kKvStorage: case layered_api::Module::kKvStorage:
return RuntimeEnabledFeatures::BuiltInModuleKvStorageEnabled( return RuntimeEnabledFeatures::BuiltInModuleKvStorageEnabled(
GetExecutionContext()); GetExecutionContext());
...@@ -71,6 +69,8 @@ bool ModulatorImplBase::BuiltInModuleEnabled(layered_api::Module module) const { ...@@ -71,6 +69,8 @@ bool ModulatorImplBase::BuiltInModuleEnabled(layered_api::Module module) const {
return RuntimeEnabledFeatures::BuiltInModuleSwitchElementEnabled(); return RuntimeEnabledFeatures::BuiltInModuleSwitchElementEnabled();
case layered_api::Module::kElementsToast: case layered_api::Module::kElementsToast:
return RuntimeEnabledFeatures::BuiltInModuleAllEnabled(); return RuntimeEnabledFeatures::BuiltInModuleAllEnabled();
case layered_api::Module::kElementsVirtualScroller:
return false;
} }
} }
...@@ -90,7 +90,7 @@ void ModulatorImplBase::BuiltInModuleUseCount( ...@@ -90,7 +90,7 @@ void ModulatorImplBase::BuiltInModuleUseCount(
case layered_api::Module::kElementsToast: case layered_api::Module::kElementsToast:
UseCounter::Count(GetExecutionContext(), WebFeature::kBuiltInModuleToast); UseCounter::Count(GetExecutionContext(), WebFeature::kBuiltInModuleToast);
break; break;
case layered_api::Module::kVirtualScroller: case layered_api::Module::kElementsVirtualScroller:
UseCounter::Count(GetExecutionContext(), UseCounter::Count(GetExecutionContext(),
WebFeature::kBuiltInModuleVirtualScroller); WebFeature::kBuiltInModuleVirtualScroller);
break; break;
......
...@@ -8,8 +8,7 @@ import sys ...@@ -8,8 +8,7 @@ import sys
def _CommonChecks(input_api, output_api): def _CommonChecks(input_api, output_api):
results = [] results = []
# We don't apply eslint to virtual-scroller/, which is not developed here. # TODO(tkent): {kv-storage,toast,virtual-scroller}/.eslintrc.js specify babel-eslint parser, which
# TODO(tkent): kv-storage/.eslintrc.js and toast/.eslintrc.js specify babel-eslint parser, which
# is not in third_party/node/node_modules/. # is not in third_party/node/node_modules/.
mjs_files = input_api.AffectedFiles( mjs_files = input_api.AffectedFiles(
file_filter=lambda f: (f.LocalPath().endswith('.mjs') and file_filter=lambda f: (f.LocalPath().endswith('.mjs') and
......
/**
* 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)];
}
/**
* 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);
/**
* 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;
}
...@@ -13,14 +13,11 @@ ...@@ -13,14 +13,11 @@
<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_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_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_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_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_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_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> </grit-part>
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)
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);
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]();
}
}
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
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
...@@ -357,7 +357,7 @@ application/x-futuresplash spl ...@@ -357,7 +357,7 @@ application/x-futuresplash spl
application/x-gtar gtar application/x-gtar gtar
application/x-gzip application/x-gzip
application/x-hdf hdf application/x-hdf hdf
application/x-javascript js application/x-javascript js mjs
application/x-java-jnlp-file jnlp application/x-java-jnlp-file jnlp
application/x-koan skp skd skt skm application/x-koan skp skd skt skm
application/x-latex latex application/x-latex latex
......
...@@ -20,7 +20,7 @@ content_types = utils.invert_dict({ ...@@ -20,7 +20,7 @@ content_types = utils.invert_dict({
"text/css": ["css"], "text/css": ["css"],
"text/event-stream": ["event_stream"], "text/event-stream": ["event_stream"],
"text/html": ["htm", "html"], "text/html": ["htm", "html"],
"text/javascript": ["js"], "text/javascript": ["js", "mjs"],
"text/plain": ["txt", "md"], "text/plain": ["txt", "md"],
"text/vtt": ["vtt"], "text/vtt": ["vtt"],
"video/mp4": ["mp4", "m4v"], "video/mp4": ["mp4", "m4v"],
......
...@@ -91,6 +91,7 @@ class ApacheHTTP(server_base.ServerBase): ...@@ -91,6 +91,7 @@ class ApacheHTTP(server_base.ServerBase):
'-c', 'Alias /webaudio-resources "%s"' % webaudio_resources_dir, '-c', 'Alias /webaudio-resources "%s"' % webaudio_resources_dir,
'-c', 'Alias /inspector-sources "%s"' % inspector_sources_dir, '-c', 'Alias /inspector-sources "%s"' % inspector_sources_dir,
'-c', 'Alias /gen "%s"' % generated_sources_dir, '-c', 'Alias /gen "%s"' % generated_sources_dir,
'-c', 'Alias /wpt_internal "%s/wpt_internal"' % test_dir,
'-c', 'TypesConfig "%s"' % mime_types_path, '-c', 'TypesConfig "%s"' % mime_types_path,
'-c', 'CustomLog "%s" common' % self._access_log_path, '-c', 'CustomLog "%s" common' % self._access_log_path,
'-c', 'ErrorLog "%s"' % self._error_log_path, '-c', 'ErrorLog "%s"' % self._error_log_path,
......
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 1, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 1, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 2, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 2, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 3, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 3, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 62, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 62, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 63, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 63, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 64, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 64, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 65, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 65, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 66, margin: '0px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as helpers from './resources/helpers.mjs';
helpers.testFindElement({elementCount: 66, margin: '10px'});
</script>
<!DOCTYPE html>
<title>Test the find-element.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as findElement from '/gen/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs';
import * as helpers from './resources/helpers.mjs';
import * as wptHelpers from '/wpt_internal/virtual-scroller/resources/helpers.mjs';
test(() => {
helpers.assertFindsElement([], 100, findElement.BIAS_LOW, null);
helpers.assertFindsElement([], 100, findElement.BIAS_HIGH, null);
}, 'Test findElement on empty array');
test(() => {
wptHelpers.withElement('div', containerDiv => {
// We set up a first and last div and between them there are 100
// 0px divs (no height, no margin). Depending on the bias we
// should always find either the first or last div, never a 0px
// div.
const firstDiv = wptHelpers.div('first');
firstDiv.style.margin = "0px";
containerDiv.appendChild(firstDiv);
for (let i = 0; i < 100; i++) {
const d = wptHelpers.div('d' + i);
d.style.margin = "0px";
d.style.height = "0px";
containerDiv.appendChild(d);
}
const lastDiv = wptHelpers.div('lastDiv');
lastDiv.style.margin = "0px";
containerDiv.appendChild(lastDiv);
const elements = containerDiv.children;
const offset = helpers.bottomOf(firstDiv); // Also helpers.topOf(lastDiv).
helpers.assertFindsElement(elements, offset, findElement.BIAS_LOW, firstDiv);
helpers.assertFindsElement(elements, offset, findElement.BIAS_HIGH, lastDiv);
});
}, 'Test findElement skips 0px elements');
</script>
<!DOCTYPE html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<div id=contentDiv>
</div>
<script type="module">
'use strict';
import * as findElement from '/gen/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs';
import * as helpers from './resources/helpers.mjs';
import * as wptHelpers from '/wpt_internal/virtual-scroller/resources/helpers.mjs';
/**
* Asserts that we find |element| when we search in |elements| with this |offset| and |bias|.
*/
function assertFindsElement(elements, offset, bias, element) {
const found = helpers.simpleFindElement(elements, offset, bias);
assert_equals(found, element, `Searching offset=${offset}, bias=${bias.toString()}`);
}
// Since even simpleFindElement isn't that simple, we have some tests for it too.
test(() => {
assertFindsElement([], 100, findElement.BIAS_LOW, null);
assertFindsElement([], 100, findElement.BIAS_HIGH, null);
}, 'Test simpleFindElement no elements');
test(() => {
wptHelpers.withElement('div', containerDiv => {
wptHelpers.appendDivs(containerDiv, 2, "10px");
const elements = containerDiv.children;
// Check all boundaries of the first element.
// Check 1px before the top.
assertFindsElement(
elements, helpers.topOf(elements[0]) - 1, findElement.BIAS_LOW, null);
assertFindsElement(
elements, helpers.topOf(elements[0]) - 1, findElement.BIAS_HIGH, null);
// Check at the top.
assertFindsElement(
elements, helpers.topOf(elements[0]), findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.topOf(elements[0]), findElement.BIAS_HIGH, elements[0]);
// Check 1px after the top.
assertFindsElement(
elements, helpers.topOf(elements[0]) + 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.topOf(elements[0]) + 1, findElement.BIAS_HIGH, elements[0]);
// Check 1px before the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[0]) - 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.bottomOf(elements[0]) - 1, findElement.BIAS_HIGH, elements[0]);
// Check at the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[0]), findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.bottomOf(elements[0]), findElement.BIAS_HIGH, elements[0]);
// Check 1px after the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[0]) + 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.bottomOf(elements[0]) + 1, findElement.BIAS_HIGH, elements[1]);
// Check all boundaries of the second (last) element.
// Check 1px before the top.
assertFindsElement(
elements, helpers.topOf(elements[1]) - 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.topOf(elements[1]) - 1, findElement.BIAS_HIGH, elements[1]);
// Check at the top.
assertFindsElement(
elements, helpers.topOf(elements[1]), findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.topOf(elements[1]), findElement.BIAS_HIGH, elements[1]);
// Check 1px after the top.
assertFindsElement(
elements, helpers.topOf(elements[1]) + 1, findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.topOf(elements[1]) + 1, findElement.BIAS_HIGH, elements[1]);
// Check 1px before the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]) - 1, findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.bottomOf(elements[1]) - 1, findElement.BIAS_HIGH, elements[1]);
// Check at the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]), findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.bottomOf(elements[1]), findElement.BIAS_HIGH, elements[1]);
// Check 1px after the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]) + 1, findElement.BIAS_LOW, null);
assertFindsElement(
elements, helpers.bottomOf(elements[1]) + 1, findElement.BIAS_HIGH, null);
});
}, 'Test simpleFindElement 10px margin');
test(() => {
wptHelpers.withElement('div', containerDiv => {
wptHelpers.appendDivs(containerDiv, 2, "10px");
const elements = containerDiv.children;
// Check all boundaries of the first element.
// Check 1px before the top.
assertFindsElement(
elements, helpers.topOf(elements[0]) - 1, findElement.BIAS_LOW, null);
assertFindsElement(
elements, helpers.topOf(elements[0]) - 1, findElement.BIAS_HIGH, null);
// Check at the top.
assertFindsElement(
elements, helpers.topOf(elements[0]), findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.topOf(elements[0]), findElement.BIAS_HIGH, elements[0]);
// Check 1px after the top.
assertFindsElement(
elements, helpers.topOf(elements[0]) + 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.topOf(elements[0]) + 1, findElement.BIAS_HIGH, elements[0]);
// Check 1px before the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[0]) - 1, findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.bottomOf(elements[0]) - 1, findElement.BIAS_HIGH, elements[0]);
// Check at the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[0]), findElement.BIAS_LOW, elements[0]);
assertFindsElement(
elements, helpers.bottomOf(elements[0]), findElement.BIAS_HIGH, elements[0]);
// Bottom of first == top of second.
// Check all boundaries of the second (last) element.
// Check 1px after the top.
assertFindsElement(
elements, helpers.topOf(elements[1]) + 1, findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.topOf(elements[1]) + 1, findElement.BIAS_HIGH, elements[1]);
// Check 1px before the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]) - 1, findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.bottomOf(elements[1]) - 1, findElement.BIAS_HIGH, elements[1]);
// Check at the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]), findElement.BIAS_LOW, elements[1]);
assertFindsElement(
elements, helpers.bottomOf(elements[1]), findElement.BIAS_HIGH, elements[1]);
// Check 1px after the bottom.
assertFindsElement(
elements, helpers.bottomOf(elements[1]) + 1, findElement.BIAS_LOW, null);
assertFindsElement(
elements, helpers.bottomOf(elements[1]) + 1, findElement.BIAS_HIGH, null);
});
}, 'Test simpleFindElement 0px margin');
</script>
/**
* 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 Helpers for testing virtual-scroller code.
* @package
*/
import * as wptHelpers from '/wpt_internal/virtual-scroller/resources/helpers.mjs';
import * as findElement from '/gen/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/find-element.mjs';
export function topOf(element) {
return element.getBoundingClientRect().top;
}
export function bottomOf(element) {
return element.getBoundingClientRect().bottom;
}
/**
* An inefficient but simple implementation of findElement that searches
* linearly.
*/
export function simpleFindElement(elements, offset, bias) {
if (elements.length === 0) {
return null;
}
const lastIndex = elements.length - 1;
if (bias === findElement.BIAS_LOW) {
if (offset < topOf(elements[0])) {
// Lower than first element.
return null;
}
// We bias low, so we accept the first element that either
// contains offset or is followed by an element that is fully
// higher than offset.
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
// We already know that offset >= top.
if (offset <= bottomOf(element) ||
(i < lastIndex && offset < topOf(elements[i + 1]))) {
return element;
}
}
} else {
if (offset > bottomOf(elements[lastIndex])) {
// Higher than last element.
return null;
}
// We bias high, so we iterate backwards and accept the first
// element that either contains offset or is followed by an
// element that is fully lower than offset.
for (let i = lastIndex; i >= 0; i--) {
const element = elements[i];
// We already know that offset <= bottom.
if (offset >= topOf(element) ||
(i > 0 && offset > bottomOf(elements[i - 1]))) {
return element;
}
}
}
// We went through all the elements without accepting any.
return null;
}
/**
* Asserts that we find |element| when we search in |elements| with this
* |offset| and |bias|.
*/
export function assertFindsElement(elements, offset, bias, element) {
const found = findElement.findElement(elements, offset, bias);
window.assert_equals(
found, element, `Searching offset=${offset}, bias=${bias.toString()}`);
}
/**
* Starts 10px before the first element and iterates until 10px after the last
* element. At each point compares the results of simpleFindElement with the
* real findElement.
*/
function testFindElementAtAllOffsets(elements, margin, bias) {
const startPx = topOf(elements[0]);
const endPx = bottomOf(elements[elements.length - 1]);
const BUFFER_PX = 10;
// index will go from -1 to length (both are out of bounds)
for (let offset = startPx - BUFFER_PX;
offset <= endPx + BUFFER_PX;
offset++) {
const element = simpleFindElement(elements, offset, bias);
assertFindsElement(elements, offset, bias, element);
}
}
/**
* Constructs a container with |elementCount| divs each with margin |margin| and
* exhaustively tests findElement against this.
*/
export function testFindElement({elementCount, margin}) {
window.test(() => {
wptHelpers.withElement('div', containerDiv => {
wptHelpers.appendDivs(containerDiv, elementCount, margin);
const elements = containerDiv.children;
testFindElementAtAllOffsets(elements, margin, findElement.BIAS_LOW);
testFindElementAtAllOffsets(elements, margin, findElement.BIAS_HIGH);
});
}, `Test findElement elementCount=${elementCount}, margin=${margin}`);
}
/**
* Asserts that the elements of actual and expected are the same, ignoring
* order. This uses Array#sort() so will only work for elements that can be
* sorted.
*/
export function assertElementsEqual(actual, expected, description) {
const actualArray = Array.from(actual);
actualArray.sort();
const expectedArray = Array.from(expected);
expectedArray.sort();
window.assert_array_equals(actualArray, expectedArray, description);
}
<!DOCTYPE html>
<title>Test the sets.mjs functions.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import * as sets from '/gen/third_party/blink/renderer/core/script/resources/layered_api/elements/virtual-scroller/sets.mjs';
import * as helpers from './resources/helpers.mjs';
test(() => {
const s1 = new Set([1, 2, 3]);
const s2 = new Set([1, 2, 3]);
helpers.assertElementsEqual(sets.difference(s1, s2), []);
}, 'diff self is empty');
test(() => {
const s1 = new Set([1, 2, 3, 4]);
const s2 = new Set([3, 4, 5, 6]);
helpers.assertElementsEqual(sets.difference(s1, s2), [1, 2]);
helpers.assertElementsEqual(sets.difference(s2, s1), [5, 6]);
}, 'two overlapping');
test(() => {
const s1 = new Set([3, 4]);
const s2 = new Set([3, 4, 5, 6]);
helpers.assertElementsEqual(sets.difference(s1, s2), []);
helpers.assertElementsEqual(sets.difference(s2, s1), [5, 6]);
}, 'one-sided overlapping');
test(() => {
const s1 = new Set([]);
const s2 = new Set([3, 4]);
helpers.assertElementsEqual(sets.difference(s1, s2), []);
helpers.assertElementsEqual(sets.difference(s2, s1), [3, 4]);
}, 'empty set');
</script>
<!DOCTYPE html>
<title>Test the virtual-scroller element.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script type="module">
import {VirtualScrollerElement} from 'std:elements/virtual-scroller';
import * as helpers from './resources/helpers.mjs';
import * as wptHelpers from '/wpt_internal/virtual-scroller/resources/helpers.mjs';
test(() => {
wptHelpers.withElement('virtual-scroller', vc => {
assert_not_equals(vc, null);
assert_true(vc instanceof VirtualScrollerElement);});
}, 'Test no content');
test(() => {
wptHelpers.withElement('virtual-scroller', vc => {
helpers.assertElementsEqual(
Object.getOwnPropertyNames(vc.__proto__),
['constructor']);
});
}, 'Only exposes certain properties');
</script>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>Creates more than a screenful of divs and scrolls down.</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>Creates more than a screenful of divs.</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>
Creates more than a screenful of divs and moves the first to the
end and scrolls to it.
</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>Creates more than a screenful of divs and moves the first to the end.</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>Creates less than a screenful of divs.</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<style>body { overflow:hidden; }</style>
<script type="module">
import * as refTests from './resources/ref-tests.mjs';
refTests.withDiv(refTests.testFullScroll500px);
</script>
</html>
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf8">
<title>Creates more than a screenful of divs and then resizes the container.</title>
<style>body { overflow:hidden; }</style>
<link rel="author" title="Fergal Daly" href="mailto:fergal@chromium.org">
<link rel="match" href="full-scroll-500px-ref.html">
<script type="module">
import 'std:elements/virtual-scroller';
import * as refTests from './resources/ref-tests.mjs';
refTests.withVirtualScroller(refTests.testFullScroll500px);
</script>
</html>
/**
* 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 Helpers for testing virtual-scroller code.
* @package
*/
/**
* Creates a DIV with id and textContent set to |id|.
*/
export function div(id) {
const d = document.createElement('div');
d.id = id;
d.textContent = id;
return d;
}
/**
* Creates a container DIV with |n| child DIVs with their margin=|margin|.
*/
export function appendDivs(container, n, margin) {
for (let i = 0; i < n; i++) {
const d = div('d' + i);
d.style.margin = margin;
container.append(d);
}
}
/*
* Creates an element, appends it to the BODY, passes it to |callback| and
* removes it in a finally.
*/
export function withElement(name, callback) {
const element = document.createElement(name);
try {
document.body.appendChild(element);
callback(element);
} finally {
element.remove();
}
}
/**
* Remove the reftest-wait class from the HTML element.
*/
export function stopWaiting() {
document.documentElement.classList.remove('reftest-wait');
}
/**
* Generate a string with |n| words.
*/
export function words(n) {
let w = '';
for (let i = 0; i < n; i++) {
w += 'word ';
}
return w;
}
/**
* Allow the current frame to end and then call |callback| asap in the next
* frame.
*/
export function nextFrame(callback) {
window.requestAnimationFrame(() => {
window.setTimeout(callback, 0);
});
}
/**
* 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 Runs a ref-test based on the filename.
* @package
*/
import * as helpers from './helpers.mjs';
const LESS_THAN_SCREENFUL = 5;
const MORE_THAN_SCREENFUL = 100;
const WORDS_IN_PARAGRAPH = 50;
export function testFull(target) {
helpers.appendDivs(target, MORE_THAN_SCREENFUL, '10px');
helpers.stopWaiting();
}
export function testFullScroll500px(target) {
helpers.appendDivs(target, MORE_THAN_SCREENFUL, '10px');
helpers.nextFrame(() => {
window.scrollBy(0, 500); // eslint-disable-line no-magic-numbers
helpers.stopWaiting();
});
};
export function testMoveElement(target) {
helpers.appendDivs(target, MORE_THAN_SCREENFUL, '10px');
helpers.nextFrame(() => {
const element = target.firstElementChild;
target.appendChild(element);
helpers.stopWaiting();
});
}
export function testMoveElementScrollIntoView(target) {
helpers.appendDivs(target, MORE_THAN_SCREENFUL, '10px');
const element = target.firstElementChild;
target.appendChild(element);
helpers.nextFrame(() => {
element.scrollIntoView();
helpers.stopWaiting();
});
}
export function testPart(target) {
helpers.appendDivs(target, LESS_THAN_SCREENFUL, '10px');
helpers.stopWaiting();
}
export function testResize(target) {
target.style.overflow = 'hidden';
target.style.width = '500px';
helpers.appendDivs(target, MORE_THAN_SCREENFUL, '10px');
const text = helpers.words(WORDS_IN_PARAGRAPH);
for (const e of target.children) {
e.innerText = text;
}
helpers.nextFrame(() => {
target.style.width = '300px';
helpers.stopWaiting();
});
}
/**
* Runs |test| with a <virtual-scroller>, waiting until the custom element is
* defined.
*/
export function withVirtualScroller(test) {
customElements.whenDefined('virtual-scroller').then(() => {
runTest("virtual-scroller", test);
});
}
/**
* Runs |test| with a <div>.
*/
export function withDiv(test) {
runTest("div", test);
}
function runTest(elementName, test) {
const element = document.createElement(elementName);
document.body.appendChild(element);
test(element);
}
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