@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
1,991 lines (1,677 loc) • 145 kB
JavaScript
import * as LogManager from 'aurelia-logging';
import {PLATFORM,DOM} from 'aurelia-pal';
import {TaskQueue} from 'aurelia-task-queue';
import {metadata} from 'aurelia-metadata';
const map = Object.create(null);
export function camelCase(name) {
if (name in map) {
return map[name];
}
const result = name.charAt(0).toLowerCase()
+ name.slice(1).replace(/[_.-](\w|$)/g, (_, x) => x.toUpperCase());
map[name] = result;
return result;
}
interface OverrideContext {
parentOverrideContext: OverrideContext;
bindingContext: any;
}
// view instances implement this interface
interface Scope {
bindingContext: any;
overrideContext: OverrideContext;
}
export function createOverrideContext(bindingContext?: any, parentOverrideContext?: OverrideContext): OverrideContext {
return {
bindingContext: bindingContext,
parentOverrideContext: parentOverrideContext || null
};
}
export function getContextFor(name: string, scope: Scope, ancestor: number): any {
let oc = scope.overrideContext;
if (ancestor) {
// jump up the required number of ancestor contexts (eg $parent.$parent requires two jumps)
while (ancestor && oc) {
ancestor--;
oc = oc.parentOverrideContext;
}
if (ancestor || !oc) {
return undefined;
}
return name in oc ? oc : oc.bindingContext;
}
// traverse the context and it's ancestors, searching for a context that has the name.
while (oc && !(name in oc) && !(oc.bindingContext && name in oc.bindingContext)) {
oc = oc.parentOverrideContext;
}
if (oc) {
// we located a context with the property. return it.
return name in oc ? oc : oc.bindingContext;
}
// the name wasn't found. return the root binding context.
return scope.bindingContext || scope.overrideContext;
}
export function createScopeForTest(bindingContext: any, parentBindingContext?: any): Scope {
if (parentBindingContext) {
return {
bindingContext,
overrideContext: createOverrideContext(bindingContext, createOverrideContext(parentBindingContext))
};
}
return {
bindingContext,
overrideContext: createOverrideContext(bindingContext)
};
}
export const sourceContext = 'Binding:source';
const slotNames = [];
const versionSlotNames = [];
for (let i = 0; i < 100; i++) {
slotNames.push(`_observer${i}`);
versionSlotNames.push(`_observerVersion${i}`);
}
function addObserver(observer) {
// find the observer.
let observerSlots = this._observerSlots === undefined ? 0 : this._observerSlots;
let i = observerSlots;
while (i-- && this[slotNames[i]] !== observer) {
// Do nothing
}
// if we are not already observing, put the observer in an open slot and subscribe.
if (i === -1) {
i = 0;
while (this[slotNames[i]]) {
i++;
}
this[slotNames[i]] = observer;
observer.subscribe(sourceContext, this);
// increment the slot count.
if (i === observerSlots) {
this._observerSlots = i + 1;
}
}
// set the "version" when the observer was used.
if (this._version === undefined) {
this._version = 0;
}
this[versionSlotNames[i]] = this._version;
}
function observeProperty(obj, propertyName) {
let observer = this.observerLocator.getObserver(obj, propertyName);
addObserver.call(this, observer);
}
function observeArray(array) {
let observer = this.observerLocator.getArrayObserver(array);
addObserver.call(this, observer);
}
function unobserve(all) {
let i = this._observerSlots;
while (i--) {
if (all || this[versionSlotNames[i]] !== this._version) {
let observer = this[slotNames[i]];
this[slotNames[i]] = null;
if (observer) {
observer.unsubscribe(sourceContext, this);
}
}
}
}
export function connectable() {
return function(target) {
target.prototype.observeProperty = observeProperty;
target.prototype.observeArray = observeArray;
target.prototype.unobserve = unobserve;
target.prototype.addObserver = addObserver;
};
}
const bindings = new Map(); // the connect queue
const minimumImmediate = 100; // number of bindings we should connect immediately before resorting to queueing
const frameBudget = 15; // milliseconds allotted to each frame for flushing queue
let isFlushRequested = false; // whether a flush of the connect queue has been requested
let immediate = 0; // count of bindings that have been immediately connected
function flush(animationFrameStart) {
let i = 0;
let keys = bindings.keys();
let item;
while (item = keys.next()) { // eslint-disable-line no-cond-assign
if (item.done) {
break;
}
let binding = item.value;
bindings.delete(binding);
binding.connect(true);
i++;
// periodically check whether the frame budget has been hit.
// this ensures we don't call performance.now a lot and prevents starving the connect queue.
if (i % 100 === 0 && PLATFORM.performance.now() - animationFrameStart > frameBudget) {
break;
}
}
if (bindings.size) {
PLATFORM.requestAnimationFrame(flush);
} else {
isFlushRequested = false;
immediate = 0;
}
}
export function enqueueBindingConnect(binding) {
if (immediate < minimumImmediate) {
immediate++;
binding.connect(false);
} else {
bindings.set(binding);
}
if (!isFlushRequested) {
isFlushRequested = true;
PLATFORM.requestAnimationFrame(flush);
}
}
function addSubscriber(context, callable) {
if (this.hasSubscriber(context, callable)) {
return false;
}
if (!this._context0) {
this._context0 = context;
this._callable0 = callable;
return true;
}
if (!this._context1) {
this._context1 = context;
this._callable1 = callable;
return true;
}
if (!this._context2) {
this._context2 = context;
this._callable2 = callable;
return true;
}
if (!this._contextsRest) {
this._contextsRest = [context];
this._callablesRest = [callable];
return true;
}
this._contextsRest.push(context);
this._callablesRest.push(callable);
return true;
}
function removeSubscriber(context, callable) {
if (this._context0 === context && this._callable0 === callable) {
this._context0 = null;
this._callable0 = null;
return true;
}
if (this._context1 === context && this._callable1 === callable) {
this._context1 = null;
this._callable1 = null;
return true;
}
if (this._context2 === context && this._callable2 === callable) {
this._context2 = null;
this._callable2 = null;
return true;
}
let rest = this._contextsRest;
let index;
if (!rest || !rest.length || (index = rest.indexOf(context)) === -1 || this._callablesRest[index] !== callable) { // eslint-disable-line no-cond-assign
return false;
}
rest.splice(index, 1);
this._callablesRest.splice(index, 1);
return true;
}
let arrayPool1 = [];
let arrayPool2 = [];
let poolUtilization = [];
function callSubscribers(newValue, oldValue) {
let context0 = this._context0;
let callable0 = this._callable0;
let context1 = this._context1;
let callable1 = this._callable1;
let context2 = this._context2;
let callable2 = this._callable2;
let length = this._contextsRest ? this._contextsRest.length : 0;
let contextsRest;
let callablesRest;
let poolIndex;
let i;
if (length) {
// grab temp arrays from the pool.
poolIndex = poolUtilization.length;
while (poolIndex-- && poolUtilization[poolIndex]) {
// Do nothing
}
if (poolIndex < 0) {
poolIndex = poolUtilization.length;
contextsRest = [];
callablesRest = [];
poolUtilization.push(true);
arrayPool1.push(contextsRest);
arrayPool2.push(callablesRest);
} else {
poolUtilization[poolIndex] = true;
contextsRest = arrayPool1[poolIndex];
callablesRest = arrayPool2[poolIndex];
}
// copy the contents of the "rest" arrays.
i = length;
while (i--) {
contextsRest[i] = this._contextsRest[i];
callablesRest[i] = this._callablesRest[i];
}
}
if (context0) {
if (callable0) {
callable0.call(context0, newValue, oldValue);
} else {
context0(newValue, oldValue);
}
}
if (context1) {
if (callable1) {
callable1.call(context1, newValue, oldValue);
} else {
context1(newValue, oldValue);
}
}
if (context2) {
if (callable2) {
callable2.call(context2, newValue, oldValue);
} else {
context2(newValue, oldValue);
}
}
if (length) {
for (i = 0; i < length; i++) {
let callable = callablesRest[i];
let context = contextsRest[i];
if (callable) {
callable.call(context, newValue, oldValue);
} else {
context(newValue, oldValue);
}
contextsRest[i] = null;
callablesRest[i] = null;
}
poolUtilization[poolIndex] = false;
}
}
function hasSubscribers() {
return !!(
this._context0
|| this._context1
|| this._context2
|| this._contextsRest && this._contextsRest.length);
}
function hasSubscriber(context, callable) {
let has = this._context0 === context && this._callable0 === callable
|| this._context1 === context && this._callable1 === callable
|| this._context2 === context && this._callable2 === callable;
if (has) {
return true;
}
let index;
let contexts = this._contextsRest;
if (!contexts || (index = contexts.length) === 0) { // eslint-disable-line no-cond-assign
return false;
}
let callables = this._callablesRest;
while (index--) {
if (contexts[index] === context && callables[index] === callable) {
return true;
}
}
return false;
}
export function subscriberCollection() {
return function(target) {
target.prototype.addSubscriber = addSubscriber;
target.prototype.removeSubscriber = removeSubscriber;
target.prototype.callSubscribers = callSubscribers;
target.prototype.hasSubscribers = hasSubscribers;
target.prototype.hasSubscriber = hasSubscriber;
};
}
@connectable()
@subscriberCollection()
export class ExpressionObserver {
constructor(scope, expression, observerLocator, lookupFunctions) {
this.scope = scope;
this.expression = expression;
this.observerLocator = observerLocator;
this.lookupFunctions = lookupFunctions;
}
getValue() {
return this.expression.evaluate(this.scope, this.lookupFunctions);
}
setValue(newValue) {
this.expression.assign(this.scope, newValue);
}
subscribe(context, callable) {
if (!this.hasSubscribers()) {
this.oldValue = this.expression.evaluate(this.scope, this.lookupFunctions);
this.expression.connect(this, this.scope);
}
this.addSubscriber(context, callable);
if (arguments.length === 1 && context instanceof Function) {
return {
dispose: () => {
this.unsubscribe(context, callable);
}
};
}
}
unsubscribe(context, callable) {
if (this.removeSubscriber(context, callable) && !this.hasSubscribers()) {
this.unobserve(true);
this.oldValue = undefined;
}
}
call() {
let newValue = this.expression.evaluate(this.scope, this.lookupFunctions);
let oldValue = this.oldValue;
if (newValue !== oldValue) {
this.oldValue = newValue;
this.callSubscribers(newValue, oldValue);
}
this._version++;
this.expression.connect(this, this.scope);
this.unobserve(false);
}
}
function isIndex(s) {
return +s === s >>> 0;
}
function toNumber(s) {
return +s;
}
function newSplice(index, removed, addedCount) {
return {
index: index,
removed: removed,
addedCount: addedCount
};
}
const EDIT_LEAVE = 0;
const EDIT_UPDATE = 1;
const EDIT_ADD = 2;
const EDIT_DELETE = 3;
function ArraySplice() {}
ArraySplice.prototype = {
// Note: This function is *based* on the computation of the Levenshtein
// "edit" distance. The one change is that "updates" are treated as two
// edits - not one. With Array splices, an update is really a delete
// followed by an add. By retaining this, we optimize for "keeping" the
// maximum array items in the original array. For example:
//
// 'xxxx123' -> '123yyyy'
//
// With 1-edit updates, the shortest path would be just to update all seven
// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
// leaves the substring '123' intact.
calcEditDistances: function(current, currentStart, currentEnd, old, oldStart, oldEnd) {
// "Deletion" columns
let rowCount = oldEnd - oldStart + 1;
let columnCount = currentEnd - currentStart + 1;
let distances = new Array(rowCount);
let north;
let west;
// "Addition" rows. Initialize null column.
for (let i = 0; i < rowCount; ++i) {
distances[i] = new Array(columnCount);
distances[i][0] = i;
}
// Initialize null row
for (let j = 0; j < columnCount; ++j) {
distances[0][j] = j;
}
for (let i = 1; i < rowCount; ++i) {
for (let j = 1; j < columnCount; ++j) {
if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) {
distances[i][j] = distances[i - 1][j - 1];
} else {
north = distances[i - 1][j] + 1;
west = distances[i][j - 1] + 1;
distances[i][j] = north < west ? north : west;
}
}
}
return distances;
},
// This starts at the final weight, and walks "backward" by finding
// the minimum previous weight recursively until the origin of the weight
// matrix.
spliceOperationsFromEditDistances: function(distances) {
let i = distances.length - 1;
let j = distances[0].length - 1;
let current = distances[i][j];
let edits = [];
while (i > 0 || j > 0) {
if (i === 0) {
edits.push(EDIT_ADD);
j--;
continue;
}
if (j === 0) {
edits.push(EDIT_DELETE);
i--;
continue;
}
let northWest = distances[i - 1][j - 1];
let west = distances[i - 1][j];
let north = distances[i][j - 1];
let min;
if (west < north) {
min = west < northWest ? west : northWest;
} else {
min = north < northWest ? north : northWest;
}
if (min === northWest) {
if (northWest === current) {
edits.push(EDIT_LEAVE);
} else {
edits.push(EDIT_UPDATE);
current = northWest;
}
i--;
j--;
} else if (min === west) {
edits.push(EDIT_DELETE);
i--;
current = west;
} else {
edits.push(EDIT_ADD);
j--;
current = north;
}
}
edits.reverse();
return edits;
},
/**
* Splice Projection functions:
*
* A splice map is a representation of how a previous array of items
* was transformed into a new array of items. Conceptually it is a list of
* tuples of
*
* <index, removed, addedCount>
*
* which are kept in ascending index order of. The tuple represents that at
* the |index|, |removed| sequence of items were removed, and counting forward
* from |index|, |addedCount| items were added.
*/
/**
* Lacking individual splice mutation information, the minimal set of
* splices can be synthesized given the previous state and final state of an
* array. The basic approach is to calculate the edit distance matrix and
* choose the shortest path through it.
*
* Complexity: O(l * p)
* l: The length of the current array
* p: The length of the old array
*/
calcSplices: function(current, currentStart, currentEnd, old, oldStart, oldEnd) {
let prefixCount = 0;
let suffixCount = 0;
let minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
if (currentStart === 0 && oldStart === 0) {
prefixCount = this.sharedPrefix(current, old, minLength);
}
if (currentEnd === current.length && oldEnd === old.length) {
suffixCount = this.sharedSuffix(current, old, minLength - prefixCount);
}
currentStart += prefixCount;
oldStart += prefixCount;
currentEnd -= suffixCount;
oldEnd -= suffixCount;
if ((currentEnd - currentStart) === 0 && (oldEnd - oldStart) === 0) {
return [];
}
if (currentStart === currentEnd) {
let splice = newSplice(currentStart, [], 0);
while (oldStart < oldEnd) {
splice.removed.push(old[oldStart++]);
}
return [ splice ];
} else if (oldStart === oldEnd) {
return [ newSplice(currentStart, [], currentEnd - currentStart) ];
}
let ops = this.spliceOperationsFromEditDistances(
this.calcEditDistances(current, currentStart, currentEnd,
old, oldStart, oldEnd));
let splice = undefined;
let splices = [];
let index = currentStart;
let oldIndex = oldStart;
for (let i = 0; i < ops.length; ++i) {
switch (ops[i]) {
case EDIT_LEAVE:
if (splice) {
splices.push(splice);
splice = undefined;
}
index++;
oldIndex++;
break;
case EDIT_UPDATE:
if (!splice) {
splice = newSplice(index, [], 0);
}
splice.addedCount++;
index++;
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
case EDIT_ADD:
if (!splice) {
splice = newSplice(index, [], 0);
}
splice.addedCount++;
index++;
break;
case EDIT_DELETE:
if (!splice) {
splice = newSplice(index, [], 0);
}
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
// no default
}
}
if (splice) {
splices.push(splice);
}
return splices;
},
sharedPrefix: function(current, old, searchLength) {
for (let i = 0; i < searchLength; ++i) {
if (!this.equals(current[i], old[i])) {
return i;
}
}
return searchLength;
},
sharedSuffix: function(current, old, searchLength) {
let index1 = current.length;
let index2 = old.length;
let count = 0;
while (count < searchLength && this.equals(current[--index1], old[--index2])) {
count++;
}
return count;
},
calculateSplices: function(current, previous) {
return this.calcSplices(current, 0, current.length, previous, 0,
previous.length);
},
equals: function(currentValue, previousValue) {
return currentValue === previousValue;
}
};
let arraySplice = new ArraySplice();
export function calcSplices(current, currentStart, currentEnd, old, oldStart, oldEnd) {
return arraySplice.calcSplices(current, currentStart, currentEnd, old, oldStart, oldEnd);
}
function intersect(start1, end1, start2, end2) {
// Disjoint
if (end1 < start2 || end2 < start1) {
return -1;
}
// Adjacent
if (end1 === start2 || end2 === start1) {
return 0;
}
// Non-zero intersect, span1 first
if (start1 < start2) {
if (end1 < end2) {
return end1 - start2; // Overlap
}
return end2 - start2; // Contained
}
// Non-zero intersect, span2 first
if (end2 < end1) {
return end2 - start1; // Overlap
}
return end1 - start1; // Contained
}
export function mergeSplice(splices, index, removed, addedCount) {
let splice = newSplice(index, removed, addedCount);
let inserted = false;
let insertionOffset = 0;
for (let i = 0; i < splices.length; i++) {
let current = splices[i];
current.index += insertionOffset;
if (inserted) {
continue;
}
let intersectCount = intersect(splice.index,
splice.index + splice.removed.length,
current.index,
current.index + current.addedCount);
if (intersectCount >= 0) {
// Merge the two splices
splices.splice(i, 1);
i--;
insertionOffset -= current.addedCount - current.removed.length;
splice.addedCount += current.addedCount - intersectCount;
let deleteCount = splice.removed.length +
current.removed.length - intersectCount;
if (!splice.addedCount && !deleteCount) {
// merged splice is a noop. discard.
inserted = true;
} else {
let currentRemoved = current.removed;
if (splice.index < current.index) {
// some prefix of splice.removed is prepended to current.removed.
let prepend = splice.removed.slice(0, current.index - splice.index);
Array.prototype.push.apply(prepend, currentRemoved);
currentRemoved = prepend;
}
if (splice.index + splice.removed.length > current.index + current.addedCount) {
// some suffix of splice.removed is appended to current.removed.
let append = splice.removed.slice(current.index + current.addedCount - splice.index);
Array.prototype.push.apply(currentRemoved, append);
}
splice.removed = currentRemoved;
if (current.index < splice.index) {
splice.index = current.index;
}
}
} else if (splice.index < current.index) {
// Insert splice here.
inserted = true;
splices.splice(i, 0, splice);
i++;
let offset = splice.addedCount - splice.removed.length;
current.index += offset;
insertionOffset += offset;
}
}
if (!inserted) {
splices.push(splice);
}
}
function createInitialSplices(array, changeRecords) {
let splices = [];
for (let i = 0; i < changeRecords.length; i++) {
let record = changeRecords[i];
switch (record.type) {
case 'splice':
mergeSplice(splices, record.index, record.removed.slice(), record.addedCount);
break;
case 'add':
case 'update':
case 'delete':
if (!isIndex(record.name)) {
continue;
}
let index = toNumber(record.name);
if (index < 0) {
continue;
}
mergeSplice(splices, index, [record.oldValue], record.type === 'delete' ? 0 : 1);
break;
default:
console.error('Unexpected record type: ' + JSON.stringify(record)); // eslint-disable-line no-console
break;
}
}
return splices;
}
export function projectArraySplices(array, changeRecords) {
let splices = [];
createInitialSplices(array, changeRecords).forEach(function(splice) {
if (splice.addedCount === 1 && splice.removed.length === 1) {
if (splice.removed[0] !== array[splice.index]) {
splices.push(splice);
}
return;
}
splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount,
splice.removed, 0, splice.removed.length));
});
return splices;
}
function newRecord(type, object, key, oldValue) {
return {
type: type,
object: object,
key: key,
oldValue: oldValue
};
}
export function getChangeRecords(map) {
let entries = new Array(map.size);
let keys = map.keys();
let i = 0;
let item;
while (item = keys.next()) { // eslint-disable-line no-cond-assign
if (item.done) {
break;
}
entries[i] = newRecord('added', map, item.value);
i++;
}
return entries;
}
@subscriberCollection()
export class ModifyCollectionObserver {
constructor(taskQueue, collection) {
this.taskQueue = taskQueue;
this.queued = false;
this.changeRecords = null;
this.oldCollection = null;
this.collection = collection;
this.lengthPropertyName = collection instanceof Map || collection instanceof Set ? 'size' : 'length';
}
subscribe(context, callable) {
this.addSubscriber(context, callable);
}
unsubscribe(context, callable) {
this.removeSubscriber(context, callable);
}
addChangeRecord(changeRecord) {
if (!this.hasSubscribers() && !this.lengthObserver) {
return;
}
if (changeRecord.type === 'splice') {
let index = changeRecord.index;
let arrayLength = changeRecord.object.length;
if (index > arrayLength) {
index = arrayLength - changeRecord.addedCount;
} else if (index < 0) {
index = arrayLength + changeRecord.removed.length + index - changeRecord.addedCount;
}
if (index < 0) {
index = 0;
}
changeRecord.index = index;
}
if (this.changeRecords === null) {
this.changeRecords = [changeRecord];
} else {
this.changeRecords.push(changeRecord);
}
if (!this.queued) {
this.queued = true;
this.taskQueue.queueMicroTask(this);
}
}
flushChangeRecords() {
if ((this.changeRecords && this.changeRecords.length) || this.oldCollection) {
this.call();
}
}
reset(oldCollection) {
this.oldCollection = oldCollection;
if (this.hasSubscribers() && !this.queued) {
this.queued = true;
this.taskQueue.queueMicroTask(this);
}
}
getLengthObserver() {
return this.lengthObserver || (this.lengthObserver = new CollectionLengthObserver(this.collection));
}
call() {
let changeRecords = this.changeRecords;
let oldCollection = this.oldCollection;
let records;
this.queued = false;
this.changeRecords = [];
this.oldCollection = null;
if (this.hasSubscribers()) {
if (oldCollection) {
// TODO (martingust) we might want to refactor this to a common, independent of collection type, way of getting the records
if (this.collection instanceof Map || this.collection instanceof Set) {
records = getChangeRecords(oldCollection);
} else {
//we might need to combine this with existing change records....
records = calcSplices(this.collection, 0, this.collection.length, oldCollection, 0, oldCollection.length);
}
} else {
if (this.collection instanceof Map || this.collection instanceof Set) {
records = changeRecords;
} else {
records = projectArraySplices(this.collection, changeRecords);
}
}
this.callSubscribers(records);
}
if (this.lengthObserver) {
this.lengthObserver.call(this.collection[this.lengthPropertyName]);
}
}
}
@subscriberCollection()
export class CollectionLengthObserver {
constructor(collection) {
this.collection = collection;
this.lengthPropertyName = collection instanceof Map || collection instanceof Set ? 'size' : 'length';
this.currentValue = collection[this.lengthPropertyName];
}
getValue() {
return this.collection[this.lengthPropertyName];
}
setValue(newValue) {
this.collection[this.lengthPropertyName] = newValue;
}
subscribe(context, callable) {
this.addSubscriber(context, callable);
}
unsubscribe(context, callable) {
this.removeSubscriber(context, callable);
}
call(newValue) {
let oldValue = this.currentValue;
this.callSubscribers(newValue, oldValue);
this.currentValue = newValue;
}
}
/* eslint-disable no-extend-native */
let pop = Array.prototype.pop;
let push = Array.prototype.push;
let reverse = Array.prototype.reverse;
let shift = Array.prototype.shift;
let sort = Array.prototype.sort;
let splice = Array.prototype.splice;
let unshift = Array.prototype.unshift;
Array.prototype.pop = function() {
let notEmpty = this.length > 0;
let methodCallResult = pop.apply(this, arguments);
if (notEmpty && this.__array_observer__ !== undefined) {
this.__array_observer__.addChangeRecord({
type: 'delete',
object: this,
name: this.length,
oldValue: methodCallResult
});
}
return methodCallResult;
};
Array.prototype.push = function() {
let methodCallResult = push.apply(this, arguments);
if (this.__array_observer__ !== undefined) {
this.__array_observer__.addChangeRecord({
type: 'splice',
object: this,
index: this.length - arguments.length,
removed: [],
addedCount: arguments.length
});
}
return methodCallResult;
};
Array.prototype.reverse = function() {
let oldArray;
if (this.__array_observer__ !== undefined) {
this.__array_observer__.flushChangeRecords();
oldArray = this.slice();
}
let methodCallResult = reverse.apply(this, arguments);
if (this.__array_observer__ !== undefined) {
this.__array_observer__.reset(oldArray);
}
return methodCallResult;
};
Array.prototype.shift = function() {
let notEmpty = this.length > 0;
let methodCallResult = shift.apply(this, arguments);
if (notEmpty && this.__array_observer__ !== undefined) {
this.__array_observer__.addChangeRecord({
type: 'delete',
object: this,
name: 0,
oldValue: methodCallResult
});
}
return methodCallResult;
};
Array.prototype.sort = function() {
let oldArray;
if (this.__array_observer__ !== undefined) {
this.__array_observer__.flushChangeRecords();
oldArray = this.slice();
}
let methodCallResult = sort.apply(this, arguments);
if (this.__array_observer__ !== undefined) {
this.__array_observer__.reset(oldArray);
}
return methodCallResult;
};
Array.prototype.splice = function() {
let methodCallResult = splice.apply(this, arguments);
if (this.__array_observer__ !== undefined) {
this.__array_observer__.addChangeRecord({
type: 'splice',
object: this,
index: arguments[0],
removed: methodCallResult,
addedCount: arguments.length > 2 ? arguments.length - 2 : 0
});
}
return methodCallResult;
};
Array.prototype.unshift = function() {
let methodCallResult = unshift.apply(this, arguments);
if (this.__array_observer__ !== undefined) {
this.__array_observer__.addChangeRecord({
type: 'splice',
object: this,
index: 0,
removed: [],
addedCount: arguments.length
});
}
return methodCallResult;
};
export function getArrayObserver(taskQueue, array) {
return ModifyArrayObserver.for(taskQueue, array);
}
class ModifyArrayObserver extends ModifyCollectionObserver {
constructor(taskQueue, array) {
super(taskQueue, array);
}
/**
* Searches for observer or creates a new one associated with given array instance
* @param taskQueue
* @param array instance for which observer is searched
* @returns ModifyArrayObserver always the same instance for any given array instance
*/
static for(taskQueue, array) {
if (!('__array_observer__' in array)) {
Reflect.defineProperty(array, '__array_observer__', {
value: ModifyArrayObserver.create(taskQueue, array),
enumerable: false, configurable: false
});
}
return array.__array_observer__;
}
static create(taskQueue, array) {
return new ModifyArrayObserver(taskQueue, array);
}
}
export class Expression {
constructor() {
this.isChain = false;
this.isAssignable = false;
}
evaluate(scope: Scope, lookupFunctions: any, args?: any): any {
throw new Error(`Binding expression "${this}" cannot be evaluated.`);
}
assign(scope: Scope, value: any, lookupFunctions: any): any {
throw new Error(`Binding expression "${this}" cannot be assigned to.`);
}
toString() {
return Unparser.unparse(this);
}
}
export class Chain extends Expression {
constructor(expressions) {
super();
this.expressions = expressions;
this.isChain = true;
}
evaluate(scope, lookupFunctions) {
let result;
let expressions = this.expressions;
let last;
for (let i = 0, length = expressions.length; i < length; ++i) {
last = expressions[i].evaluate(scope, lookupFunctions);
if (last !== null) {
result = last;
}
}
return result;
}
accept(visitor) {
return visitor.visitChain(this);
}
}
export class BindingBehavior extends Expression {
constructor(expression, name, args) {
super();
this.expression = expression;
this.name = name;
this.args = args;
}
evaluate(scope, lookupFunctions) {
return this.expression.evaluate(scope, lookupFunctions);
}
assign(scope, value, lookupFunctions) {
return this.expression.assign(scope, value, lookupFunctions);
}
accept(visitor) {
return visitor.visitBindingBehavior(this);
}
connect(binding, scope) {
this.expression.connect(binding, scope);
}
bind(binding, scope, lookupFunctions) {
if (this.expression.expression && this.expression.bind) {
this.expression.bind(binding, scope, lookupFunctions);
}
let behavior = lookupFunctions.bindingBehaviors(this.name);
if (!behavior) {
throw new Error(`No BindingBehavior named "${this.name}" was found!`);
}
let behaviorKey = `behavior-${this.name}`;
if (binding[behaviorKey]) {
throw new Error(`A binding behavior named "${this.name}" has already been applied to "${this.expression}"`);
}
binding[behaviorKey] = behavior;
behavior.bind.apply(behavior, [binding, scope].concat(evalList(scope, this.args, binding.lookupFunctions)));
}
unbind(binding, scope) {
let behaviorKey = `behavior-${this.name}`;
binding[behaviorKey].unbind(binding, scope);
binding[behaviorKey] = null;
if (this.expression.expression && this.expression.unbind) {
this.expression.unbind(binding, scope);
}
}
}
export class ValueConverter extends Expression {
constructor(expression, name, args, allArgs) {
super();
this.expression = expression;
this.name = name;
this.args = args;
this.allArgs = allArgs;
}
evaluate(scope, lookupFunctions) {
let converter = lookupFunctions.valueConverters(this.name);
if (!converter) {
throw new Error(`No ValueConverter named "${this.name}" was found!`);
}
if ('toView' in converter) {
return converter.toView.apply(converter, evalList(scope, this.allArgs, lookupFunctions));
}
return this.allArgs[0].evaluate(scope, lookupFunctions);
}
assign(scope, value, lookupFunctions) {
let converter = lookupFunctions.valueConverters(this.name);
if (!converter) {
throw new Error(`No ValueConverter named "${this.name}" was found!`);
}
if ('fromView' in converter) {
value = converter.fromView.apply(converter, [value].concat(evalList(scope, this.args, lookupFunctions)));
}
return this.allArgs[0].assign(scope, value, lookupFunctions);
}
accept(visitor) {
return visitor.visitValueConverter(this);
}
connect(binding, scope) {
let expressions = this.allArgs;
let i = expressions.length;
while (i--) {
expressions[i].connect(binding, scope);
}
}
}
export class Assign extends Expression {
constructor(target, value) {
super();
this.target = target;
this.value = value;
}
evaluate(scope, lookupFunctions) {
return this.target.assign(scope, this.value.evaluate(scope, lookupFunctions));
}
accept(vistor) {
vistor.visitAssign(this);
}
connect(binding, scope) {
}
}
export class Conditional extends Expression {
constructor(condition, yes, no) {
super();
this.condition = condition;
this.yes = yes;
this.no = no;
}
evaluate(scope, lookupFunctions) {
return (!!this.condition.evaluate(scope)) ? this.yes.evaluate(scope) : this.no.evaluate(scope);
}
accept(visitor) {
return visitor.visitConditional(this);
}
connect(binding, scope) {
this.condition.connect(binding, scope);
if (this.condition.evaluate(scope)) {
this.yes.connect(binding, scope);
} else {
this.no.connect(binding, scope);
}
}
}
export class AccessThis extends Expression {
constructor(ancestor) {
super();
this.ancestor = ancestor;
}
evaluate(scope, lookupFunctions) {
let oc = scope.overrideContext;
let i = this.ancestor;
while (i-- && oc) {
oc = oc.parentOverrideContext;
}
return i < 1 && oc ? oc.bindingContext : undefined;
}
accept(visitor) {
return visitor.visitAccessThis(this);
}
connect(binding, scope) {
}
}
export class AccessScope extends Expression {
constructor(name, ancestor) {
super();
this.name = name;
this.ancestor = ancestor;
this.isAssignable = true;
}
evaluate(scope, lookupFunctions) {
let context = getContextFor(this.name, scope, this.ancestor);
return context[this.name];
}
assign(scope, value) {
let context = getContextFor(this.name, scope, this.ancestor);
return context ? (context[this.name] = value) : undefined;
}
accept(visitor) {
return visitor.visitAccessScope(this);
}
connect(binding, scope) {
let context = getContextFor(this.name, scope, this.ancestor);
binding.observeProperty(context, this.name);
}
}
export class AccessMember extends Expression {
constructor(object, name) {
super();
this.object = object;
this.name = name;
this.isAssignable = true;
}
evaluate(scope, lookupFunctions) {
let instance = this.object.evaluate(scope, lookupFunctions);
return instance === null || instance === undefined ? instance : instance[this.name];
}
assign(scope, value) {
let instance = this.object.evaluate(scope);
if (instance === null || instance === undefined) {
instance = {};
this.object.assign(scope, instance);
}
instance[this.name] = value;
return value;
}
accept(visitor) {
return visitor.visitAccessMember(this);
}
connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (obj) {
binding.observeProperty(obj, this.name);
}
}
}
export class AccessKeyed extends Expression {
constructor(object, key) {
super();
this.object = object;
this.key = key;
this.isAssignable = true;
}
evaluate(scope, lookupFunctions) {
let instance = this.object.evaluate(scope, lookupFunctions);
let lookup = this.key.evaluate(scope, lookupFunctions);
return getKeyed(instance, lookup);
}
assign(scope, value) {
let instance = this.object.evaluate(scope);
let lookup = this.key.evaluate(scope);
return setKeyed(instance, lookup, value);
}
accept(visitor) {
return visitor.visitAccessKeyed(this);
}
connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (obj instanceof Object) {
this.key.connect(binding, scope);
let key = this.key.evaluate(scope);
// observe the property represented by the key as long as it's not an array
// being accessed by an integer key which would require dirty-checking.
if (key !== null && key !== undefined
&& !(Array.isArray(obj) && typeof(key) === 'number')) {
binding.observeProperty(obj, key);
}
}
}
}
export class CallScope extends Expression {
constructor(name, args, ancestor) {
super();
this.name = name;
this.args = args;
this.ancestor = ancestor;
}
evaluate(scope, lookupFunctions, mustEvaluate) {
let args = evalList(scope, this.args, lookupFunctions);
let context = getContextFor(this.name, scope, this.ancestor);
let func = getFunction(context, this.name, mustEvaluate);
if (func) {
return func.apply(context, args);
}
return undefined;
}
accept(visitor) {
return visitor.visitCallScope(this);
}
connect(binding, scope) {
let args = this.args;
let i = args.length;
while (i--) {
args[i].connect(binding, scope);
}
// todo: consider adding `binding.observeProperty(scope, this.name);`
}
}
export class CallMember extends Expression {
constructor(object, name, args) {
super();
this.object = object;
this.name = name;
this.args = args;
}
evaluate(scope, lookupFunctions, mustEvaluate) {
let instance = this.object.evaluate(scope, lookupFunctions);
let args = evalList(scope, this.args, lookupFunctions);
let func = getFunction(instance, this.name, mustEvaluate);
if (func) {
return func.apply(instance, args);
}
return undefined;
}
accept(visitor) {
return visitor.visitCallMember(this);
}
connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (getFunction(obj, this.name, false)) {
let args = this.args;
let i = args.length;
while (i--) {
args[i].connect(binding, scope);
}
}
}
}
export class CallFunction extends Expression {
constructor(func, args) {
super();
this.func = func;
this.args = args;
}
evaluate(scope, lookupFunctions, mustEvaluate) {
let func = this.func.evaluate(scope, lookupFunctions);
if (typeof func === 'function') {
return func.apply(null, evalList(scope, this.args, lookupFunctions));
}
if (!mustEvaluate && (func === null || func === undefined)) {
return undefined;
}
throw new Error(`${this.func} is not a function`);
}
accept(visitor) {
return visitor.visitCallFunction(this);
}
connect(binding, scope) {
this.func.connect(binding, scope);
let func = this.func.evaluate(scope);
if (typeof func === 'function') {
let args = this.args;
let i = args.length;
while (i--) {
args[i].connect(binding, scope);
}
}
}
}
export class Binary extends Expression {
constructor(operation, left, right) {
super();
this.operation = operation;
this.left = left;
this.right = right;
}
evaluate(scope, lookupFunctions) {
let left = this.left.evaluate(scope);
switch (this.operation) {
case '&&': return left && this.right.evaluate(scope);
case '||': return left || this.right.evaluate(scope);
// no default
}
let right = this.right.evaluate(scope);
switch (this.operation) {
case '==' : return left == right; // eslint-disable-line eqeqeq
case '===': return left === right;
case '!=' : return left != right; // eslint-disable-line eqeqeq
case '!==': return left !== right;
// no default
}
// Null check for the operations.
if (left === null || right === null || left === undefined || right === undefined) {
switch (this.operation) {
case '+':
if (left !== null && left !== undefined) return left;
if (right !== null && right !== undefined) return right;
return 0;
case '-':
if (left !== null && left !== undefined) return left;
if (right !== null && right !== undefined) return 0 - right;
return 0;
// no default
}
return null;
}
switch (this.operation) {
case '+' : return autoConvertAdd(left, right);
case '-' : return left - right;
case '*' : return left * right;
case '/' : return left / right;
case '%' : return left % right;
case '<' : return left < right;
case '>' : return left > right;
case '<=' : return left <= right;
case '>=' : return left >= right;
case '^' : return left ^ right;
// no default
}
throw new Error(`Internal error [${this.operation}] not handled`);
}
accept(visitor) {
return visitor.visitBinary(this);
}
connect(binding, scope) {
this.left.connect(binding, scope);
let left = this.left.evaluate(scope);
if (this.operation === '&&' && !left || this.operation === '||' && left) {
return;
}
this.right.connect(binding, scope);
}
}
export class PrefixNot extends Expression {
constructor(operation, expression) {
super();
this.operation = operation;
this.expression = expression;
}
evaluate(scope, lookupFunctions) {
return !this.expression.evaluate(scope);
}
accept(visitor) {
return visitor.visitPrefix(this);
}
connect(binding, scope) {
this.expression.connect(binding, scope);
}
}
export class LiteralPrimitive extends Expression {
constructor(value) {
super();
this.value = value;
}
evaluate(scope, lookupFunctions) {
return this.value;
}
accept(visitor) {
return visitor.visitLiteralPrimitive(this);
}
connect(binding, scope) {
}
}
export class LiteralString extends Expression {
constructor(value) {
super();
this.value = value;
}
evaluate(scope, lookupFunctions) {
return this.value;
}
accept(visitor) {
return visitor.visitLiteralString(this);
}
connect(binding, scope) {
}
}
export class LiteralArray extends Expression {
constructor(elements) {
super();
this.elements = elements;
}
evaluate(scope, lookupFunctions) {
let elements = this.elements;
let result = [];
for (let i = 0, length = elements.length; i < length; ++i) {
result[i] = elements[i].evaluate(scope, lookupFunctions);
}
return result;
}
accept(visitor) {
return visitor.visitLiteralArray(this);
}
connect(binding, scope) {
let length = this.elements.length;
for (let i = 0; i < length; i++) {
this.elements[i].connect(binding, scope);
}
}
}
export class LiteralObject extends Expression {
constructor(keys, values) {
super();
this.keys = keys;
this.values = values;
}
evaluate(scope, lookupFunctions) {
let instance = {};
let keys = this.keys;
let values = this.values;
for (let i = 0, length = keys.length; i < length; ++i) {
instance[keys[i]] = values[i].evaluate(scope, lookupFunctions);
}
return instance;
}
accept(visitor) {
return visitor.visitLiteralObject(this);
}
connect(binding, scope) {
let length = this.keys.length;
for (let i = 0; i < length; i++) {
this.values[i].connect(binding, scope);
}
}
}
let evalListCache = [[], [0], [0, 0], [0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0, 0]];
/// Evaluate the [list] in context of the [scope].
function evalList(scope, list, lookupFunctions) {
let length = list.length;
for (let cacheLength = evalListCache.length; cacheLength <= length; ++cacheLength) {
evalListCache.push([]);
}
let result = evalListCache[length];
for (let i = 0; i < length; ++i) {
result[i] = list[i].evaluate(scope, lookupFunctions);
}
return result;
}
/// Add the two arguments with automatic type conversion.
function autoConvertAdd(a, b) {
if (a !== null && b !== null) {
// TODO(deboer): Support others.
if (typeof a === 'string' && typeof b !== 'string') {
return a + b.toString();
}
if (typeof a !== 'string' && typeof b === 'string') {
return a.toString() + b;
}
return a + b;
}
if (a !== null) {
return a;
}
if (b !== null) {
return b;
}
return 0;
}
function getFunction(obj, name, mustExist) {
let func = obj === null || obj === undefined ? null : obj[name];
if (typeof func === 'function') {
return func;
}
if (!mustExist && (func === null || func === undefined)) {
return null;
}
throw new Error(`${name} is not a function`);
}
function getKeyed(obj, key) {
if (Array.isArray(obj)) {
return obj[parseInt(key, 10)];
} else if (obj) {
return obj[key];
} else if (obj === null || obj === undefined) {
return undefined;
}
return obj[key];
}
function setKeyed(obj, key, value) {
if (Array.isArray(obj)) {
let index = parseInt(key, 10);
if (obj.length <= index) {
obj.length = index + 1;
}
obj[index] = value;
} else {
obj[key] = value;
}
return value;
}
export class Unparser {
constructor(buffer) {
this.buffer = buffer;
}
static unparse(expression) {
let buffer = [];
let visitor = new Unparser(buffer);
expression.accept(visitor);
return buffer.join('');
}
write(text) {
this.buffer.push(text);
}
writeArgs(args) {
this.write('(');
for (let i = 0, length = args.length; i < length; ++i) {
if (i !== 0) {
this.write(',');
}
args[i].accept(this);
}
this.write(')');
}
visitChain(chain) {
let expressions = chain.expressions;
for (let i = 0, length = expression.length; i < length; ++i) {
if (i !== 0) {
this.write(';');
}
expressions[i].accept(this);
}
}
visitBindingBehavior(behavior) {
let args = behavior.args;
behavior.expression.accept(this);
this.write(`&${behavior.name}`);
for (let i = 0, length = args.length; i < length; ++i) {
this.write(':');
args[i].accept(this);
}
}
visitValueConverter(converter) {
let args = converter.args;
converter.expression.accept(this);
this.write(`|${converter.name}`);
for (let i = 0, length = args.length; i < length; ++i) {
this.write(':');
args[i].accept(this);
}
}
visitAssign(assign) {
assign.target.accept(this);
this.write('=');
assign.value.accept(this);
}
visitConditional(conditional) {
conditional.condition.accept(this);
this.write('?');
conditional.yes.accept(this);
this.write(':');
conditional.no.accept(this);
}
visitAccessThis(access) {
if (access.ancestor === 0) {
this.write('$this');
return;
}
this.write('$parent');
let i = access.ancestor - 1;
while (i--) {
this.write('.$parent');
}
}
visitAccessScope(access) {
let i = access.ancestor;
while (i--) {
this.write('$parent.');
}
this.write(access.name);
}
visitAccessMember(access) {
access.object.accept(this);
this.write(`.${access.name}`);
}
visitAccessKeyed(access) {
access.object.accept(this);
this.write('[');
access.key.accept(this);
this.write(']');
}
visitCallScope(call) {
let i = call.ancestor;
while (i--) {
this.write('$parent.');
}
this.write(call.name);
this.writeArgs(call.args);
}
visitCallFunction(call) {
call.func.accept(this);
this.writeArgs(call.args);
}
visitCallMember(call) {
call.object.accept(this);
this.write(`.${call.name}`);
this.writeArgs(call.args);
}
visitPrefix(prefix) {
this.writ