UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

287 lines (211 loc) • 7.93 kB
import { Cache } from "../../core/cache/Cache.js"; import List from '../../core/collection/list/List.js'; import Signal from "../../core/events/signal/Signal.js"; import { passThrough } from "../../core/function/passThrough.js"; import { strictEquals } from "../../core/function/strictEquals.js"; import { max2 } from "../../core/math/max2.js"; import { frameThrottle } from '../../engine/graphics/FrameThrottle.js'; import dom from '../DOM.js'; import View from '../View.js'; export const AlignmentOption = { Start: 'start', Center: 'center', End: 'end' }; class VirtualListView extends View { /** * @template T * @param {List.<T>} list * @param {number} [lineSize=20] * @param {number} [lineSpacing=0] * @param {function(element:T, index:number):View} elementFactory * @param classList * @param {number} [cacheSize=1000] * @param {AlignmentOption} [verticalAlignment] * @constructor */ constructor(list, { lineSize = 20, lineSpacing = 0, elementFactory, classList = [], cacheSize = 1000, verticalAlignment = AlignmentOption.Start }) { super(); this.on.scroll_y_span_changed = new Signal(); this.data = list; if (elementFactory === undefined) { throw new Error("Element factory was not supplied"); } const dRoot = dom('div'); dRoot.addClass('list'); // dRoot.css({ // overflowY: "visible", // overflowX: "visible" // }); this.el = dRoot.el; this.addClasses(classList); const vScrollArea = new View(); const dScrollArea = dom('div'); dScrollArea.addClass('scroll-area'); dScrollArea.css({ userSelect: "none" }); vScrollArea.el = dScrollArea.el; this.addChild(vScrollArea); this.renderedViews = new List(); this.__v_scroll_area = vScrollArea; this.__first_visible_line = -1; this.__last_visible_line = -1; this.__is_scroll_bar_visible = false; /** * * @type {number} * @private */ this.__line_size = lineSize; /** * * @type {number} * @private */ this.__line_spacing = lineSpacing; /** * * @type {function(T, number): View} * @private */ this.__element_factory = elementFactory; /** * * @type {AlignmentOption} * @private */ this.__alignment_vertical = verticalAlignment; this.__index_cache = new Cache({ maxWeight: cacheSize, keyEqualityFunction: strictEquals, keyHashFunction: passThrough }); this.__throttledUpdate = frameThrottle(this.update, this); this.el.addEventListener('scroll', this.__throttledUpdate); vScrollArea.el.addEventListener('scroll', this.__throttledUpdate); this.bindSignal(this.data.on.added, this.__handleElementAdded, this); this.bindSignal(this.data.on.removed, this.__handleElementRemoved, this); } __handleElementAdded(el, index) { const last_element_index = this.data.length - 1; if (index < last_element_index) { // inserted somewhere in the middle, invalidate cache this.__index_cache.clear(); } this.__throttledUpdate(); } __handleElementRemoved(el, index) { // invalidate cache // TODO this can be done in a smarter way this.__index_cache.clear(); this.__throttledUpdate(); } update() { const vScrollArea = this.__v_scroll_area; const lineSize = this.__line_size; const lineSpacing = this.__line_spacing; const numTotalElements = this.data.length; const maxLength = lineSize * numTotalElements + lineSpacing * max2(0, numTotalElements - 1); const row_height = lineSize + lineSpacing; const old_scroll_span_y = vScrollArea.size.y; vScrollArea.size.setY(maxLength); if (old_scroll_span_y !== maxLength) { this.on.scroll_y_span_changed.send0(); } //figure out currently visible lines const scrollY = this.el.scrollTop; const y0 = scrollY; const y1 = Math.min(scrollY + this.size.y, maxLength); const l0 = Math.floor(y0 / row_height); const l1 = Math.min(Math.ceil(y1 / row_height), numTotalElements - 1); //update cache this.__first_visible_line = l0; this.__last_visible_line = l1; //clear existing lines this.renderedViews.forEach(vScrollArea.removeChild, vScrollArea); this.renderedViews.reset(); let rowWidth = this.size.x; if (this.__first_visible_line === 0 && this.__last_visible_line === numTotalElements - 1 && row_height * (this.__last_visible_line - this.__first_visible_line) < this.size.y) { //entire set of data is visible, disable scroll bar this.__setScrollBar(false); } else { rowWidth -= 17; this.__setScrollBar(true); } const total_lines_height = numTotalElements * row_height; let line_offset_y = 0; if (total_lines_height < this.size.y) { if (this.__alignment_vertical === AlignmentOption.End) { line_offset_y = -(total_lines_height - this.size.y); } } //generate views for visible lines for (let i = this.__first_visible_line; i <= this.__last_visible_line; i++) { const lineView = this.__obtainElementView(i); if (lineView === undefined) { continue; } lineView.el.style.position = "absolute"; //mark odd rows if (i % 2 === 1) { lineView.el.classList.add('odd-row'); } const line_position_y = i * row_height + line_offset_y; lineView.position.setY(line_position_y); lineView.size.set(rowWidth, lineSize); vScrollArea.addChild(lineView); this.renderedViews.add(lineView); } } __obtainElementView(index) { const element = this.data.get(index); const existingView = this.__index_cache.get(index); if (existingView !== null) { return existingView; } const view = this.__element_factory(element, index); if (view === undefined) { console.error('Line view produced by element factory was undefined'); return undefined; } this.__index_cache.put(index, view); return view; } /** * * @param {number} index * @returns {number} * @private */ __computeElementYPosition(index) { const rowHeight = this.__line_size + this.__line_spacing; return rowHeight * index; } __setScrollBar(flag) { this.setClass('scroll-enabled', flag); this.__is_scroll_bar_visible = flag; } scrollToEnd() { const target = this.__computeElementYPosition(this.data.length); this.el.scrollTop = target; this.__throttledUpdate(); } link() { super.link(); this.size.onChanged.add(this.__throttledUpdate); this.data.forEach(this.__handleElementAdded, this); } unlink() { super.unlink(); this.size.onChanged.remove(this.__throttledUpdate); } } export default VirtualListView;