@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
283 lines (221 loc) • 6.48 kB
JavaScript
import { assert } from "../../core/assert.js";
import { Cache } from "../../core/cache/Cache.js";
import List from "../../core/collection/list/List.js";
import { noop } from "../../core/function/noop.js";
import { returnTrue } from "../../core/function/returnTrue.js";
import View from "../View.js";
/**
* @template T
*/
export class CacheKey {
/**
* @template T
* @param {T} thing
*/
constructor(thing) {
/**
*
* @type {T}
*/
this.thing = thing;
/**
* @type {number}
*/
this.__hash = thing.hash();
}
/**
*
* @return {number}
*/
hash() {
return this.thing.hash();
}
/**
*
* @param {T} other
* @return {boolean}
*/
equals(other) {
return this.thing.equals(other);
}
}
export class ListView extends View {
/**
* List representation
* @template E
* @param {List<E>|E[]} model List to be represented
* @param {string[]} [classList] collection of CSS classes
* @param {function(E):View} elementFactory factory function, takes a list element and returns a view
* @param {function(E, View, number, ListView<E>)} [addHook] hook function to be called when a view is created
* @param {function(E, View, number, ListView<E>)} [removeHook] hook function to be called when a view is created
* @param {function(E):boolean} [filter]
* @param {number} [cacheSize]
* @param {boolean} [useCache]
* @constructor
*/
constructor(model, {
classList = [],
elementFactory,
addHook = noop,
removeHook = noop,
filter = returnTrue,
cacheSize = 100,
useCache = false
}) {
super();
/**
* @private
* @type {Cache<E, View>}
*/
this.__cache = new Cache({
maxWeight: cacheSize
});
/**
*
* @type {boolean}
* @private
*/
this.__useCache = useCache;
assert.isFunction(elementFactory, 'elementFactory');
if (Array.isArray(model)) {
// wrap array as a list
model = new List(model);
}
/**
*
* @type {List<E>}
*/
this.model = model;
this.elementFactory = elementFactory;
this.filter = filter;
this.hooks = {
add: addHook,
remove: removeHook
};
this.el = document.createElement('div');
this.addClass('ui-list-view');
classList.forEach((className) => {
this.addClass(className);
});
this.bindSignal(model.on.added, this.insertOne, this);
this.bindSignal(model.on.removed, this.removeOne, this);
/**
*
* @type {Map<E, View>}
*/
this.viewMapping = new Map();
// initialization
this.on.linked.add(() => {
const l = model.length;
for (let i = 0; i < l; i++) {
const el = model.get(i);
if (!this.filter(el)) {
continue;
}
this.addOne(el, i);
}
});
// cleanup
this.on.unlinked.add(() => {
model.forEach(this.removeOne, this);
});
}
/**
*
* @param {E} el
* @returns {View}
*/
acquireView(el) {
if (this.__useCache) {
let view = this.__cache.get(el);
if (view === null) {
view = this.elementFactory(el);
} else {
this.__cache.remove(el);
}
return view;
} else {
return this.elementFactory(el);
}
}
/**
* @private
* @param el
* @param index
*/
insertOne(el, index) {
if (!this.filter(el)) {
return;
}
const elementView = this.addOne(el, index);
if (this.model.length !== index + 1) {
//this is not the last element in the list, we need to patch it into the right place inside the DOM
const nextElement = this.model.get(index + 1);
const nextChild = this.getChildByElement(nextElement);
this.el.insertBefore(elementView.el, nextChild.el);
}
}
/**
* @private
* @param {E} el
* @param {number} index
* @returns {View}
*/
addOne(el, index) {
/**
* @type {View}
*/
const elementView = this.acquireView(el);
assert.notEqual(elementView, undefined, 'elementFactory produced undefined instead of a view');
assert.notEqual(elementView, null, 'elementFactory produced a null instead of a view');
this.viewMapping.set(el, elementView);
this.addChild(elementView);
this.hooks.add(el, elementView, index, self);
return elementView;
}
/**
* @private
* @param {E} el
* @param {number} index
*/
removeOne(el, index) {
const children = this.children;
const i = this.getChildIndexByElement(el);
//clear mapping
this.viewMapping.delete(el);
if (i === -1) {
if (this.filter(el)) {
//element fits the filter, but was not in the mapping
console.error('Failed to find view for element ', el);
}
} else {
const child = children[i];
this.removeChild(child);
if (this.__useCache) {
this.__cache.put(el, child);
}
this.hooks.remove(el, child, index, this);
}
}
/**
*
* @param {E} el
* @returns {number} -1 if not found
*/
getChildIndexByElement(el) {
const view = this.viewMapping.get(el);
if (view === undefined) {
return -1;
}
return this.children.indexOf(view);
}
/**
*
* @param {E} el
* @returns {View|null}
*/
getChildByElement(el) {
return this.viewMapping.get(el);
}
}
export default ListView;