/**
* The `proact-arrays` module provides reactive arrays.
* All the modification operations over arrays, like `push` for example could be listened to.
*
*
* @module proact-arrays
* @main proact-arrays
*/
/**
* <p>
* Constructs a `ProAct.ArrayCore`. `ProAct.ArrayCore` is a {{#crossLink "ProAct.Core"}}{{/crossLink}} that manages all the updates/listeners for an `ProAct.Array`.
* </p>
* <p>
* It is responsible for updating length or index listeners and adding the right ones on read.
* </p>
* <p>
* `ProAct.ArrayCore` is part of the `proact-arrays` module of ProAct.js.
* </p>
*
* @class ProAct.ArrayCore
* @constructor
* @extends ProAct.Core
* @param {Object} array
* The shell {{#crossLink "ProAct.Array"}}{{/crossLink}} arround this core.
* @param {Object} meta
* Optional meta data to be used to define the observer-observable behavior of the <i>array</i>.
*/
ProAct.ArrayCore = P.AC = function (array, meta) {
P.C.call(this, array, meta); // Super!
this.lastIndexCaller = null;
this.lastLengthCaller = null;
};
ProAct.ArrayCore.prototype = P.U.ex(Object.create(P.C.prototype), {
/**
* Reference to the constructor of this object.
*
* @property constructor
* @type ProAct.ArrayCore
* @final
* @for ProAct.ArrayCore
*/
constructor: ProAct.ArrayCore,
/**
* Generates function wrapper around a normal function which sets
* the {{#crossLink "ProAct.ArrayCore/indexListener:method"}}{{/crossLink}} of the index calling the function.
* <p>
* This is used if the array is complex - contains other ProAct.js objects, and there should be special
* updates for their elements/properties.
* </p>
*
* @for ProAct.ArrayCore
* @instance
* @method actionFunction
* @param {Function} fun
* The source function.
* @return {Function}
* The action function wrapper.
*/
actionFunction: function (fun) {
var core = this;
return function () {
var oldCaller = P.currentCaller,
i = arguments[1], res;
P.currentCaller = core.indexListener(i);
res = fun.apply(this, slice.call(arguments, 0));
P.currentCaller = oldCaller;
return res;
};
},
/**
* Generates listener for given index or reuses already generated one.
* <p>
* This listener mimics a property listener, the idea is - if anything is listening to
* index changes in this' shell (array) and the shell is complex - has elements that are ProAct.js objects,
* if some of this element has property change, its notification should be dispatched to all the objects,
* listening to index changes in the array.
* </p>
* <p>
* So this way we can listen for stuff like array.[].foo - the foo property change for every element in the array.
* </p>
*
* @for ProAct.ArrayCore
* @instance
* @protected
* @method indexListener
* @param {Number} i
* The index.
* @return {Object}
* A listener mimicing a property one.
*/
indexListener: function (i) {
if (!this.indexListeners) {
this.indexListeners = {};
}
var core = this,
shell = core.shell;
if (!this.indexListeners[i]) {
this.indexListeners[i] = {
call: function (source) {
core.makeListener(new P.E(source, shell, P.E.Types.array, [
P.A.Operations.set, i, shell._array[i], shell._array[i]
]));
},
property: core
};
}
return this.indexListeners[i];
},
/**
* Creates the <i>listener</i> of this `ProAct.ArrayCore`.
* <p>
* The right array typed events can change this' shell (array).
* </p>
* <p>
* If a non-event element is passed to the listener, the element is pushed
* to the shell.
* </p>
* <p>
* If a value event is passed to the listener, the new value is pushed
* to the shell.
* </p>
*
* @for ProAct.Actor
* @instance
* @protected
* @method makeListener
* @return {Object}
* The <i>listener of this ArrayCore</i>.
*/
makeListener: function () {
if (!this.listener) {
var self = this.shell;
this.listener = {
queueName: this.queueName,
call: function (event) {
if (!event || !(event instanceof P.E)) {
self.push(event);
return;
}
if (event.type === P.E.Types.value) {
self.push(event.args[2]);
return;
}
var op = event.args[0],
ind = event.args[1],
ov = event.args[2],
nv = event.args[3],
nvs,
operations = P.Array.Operations;
if (op === operations.set) {
self[ind] = nv;
} else if (op === operations.add) {
nvs = slice.call(nv, 0);
if (ind === 0) {
pArrayProto.unshift.apply(self, nvs);
} else {
pArrayProto.push.apply(self, nvs);
}
} else if (op === operations.remove) {
if (ind === 0) {
self.shift();
} else {
self.pop();
}
} else if (op === operations.setLength) {
self.length = nv;
} else if (op === operations.reverse) {
self.reverse();
} else if (op === operations.sort) {
if (P.U.isFunction(nv)) {
self.sort(nv);
} else {
self.sort();
}
} else if (op === operations.splice) {
if (nv) {
nvs = slice.call(nv, 0);
} else {
nvs = [];
}
if (ind === null || ind === undefined) {
ind = self.indexOf(ov[0]);
if (ind === -1) {
return;
}
}
pArrayProto.splice.apply(self, [ind, ov.length].concat(nvs));
}
}
};
}
return this.listener;
},
/**
* Generates the initial listeners object.
* It is used for resetting all the listeners too.
* <p>
* For `ProAct.ArrayCore` the default listeners object is
* <pre>
* {
* index: [],
* length: []
* }
* </pre>
* </p>
*
* @for ProAct.ArrayCore
* @protected
* @instance
* @method defaultListeners
* @return {Object}
* A map containing the default listeners collections (index and length type of listeners).
*/
defaultListeners: function () {
return {
index: [],
length: []
};
},
/**
* A list of actions or action to be used when no action is passed for the methods working with actions.
* <p>
* For `ProAct.ArrayCore` these are both 'length' and 'index' actions.
* </p>
*
* @for ProAct.ArrayCore
* @protected
* @instance
* @method defaultActions
* @default ['length', 'index']
* @return {Array}
* The actions to be used if no actions are provided to action related methods,
* like {{#crossLink "ProAct.Actor/on:method"}}{{/crossLink}}, {{#crossLink "ProAct.Actor/off:method"}}{{/crossLink}}, {{#crossLink "ProAct.Actor/update:method"}}{{/crossLink}}, {{#crossLink "ProAct.Actor/willUpdate:method"}}{{/crossLink}}.
*/
defaultActions: function () {
return ['length', 'index'];
},
/**
* Creates the <i>event</i> to be send to the listeners on update.
* <p>
* By default this method returns {{#crossLink "ProAct.Event.Types/array:property"}}{{/crossLink}} event.
* </p>
*
* @for ProAct.ArrayCore
* @instance
* @protected
* @method makeEvent
* @default {ProAct.Event} with type {{#crossLink "ProAct.Event.Types/array:property"}}{{/crossLink}}
* @param {ProAct.Event} source
* The source event of the event. It can be null
* @param {Array} eventData
* An array of four elements describing the changes:
* <ol>
* <li>{{#crossLink "ProAct.Array.Operations"}}{{/crossLink}} member defining the changing operation - for example {{#crossLink "ProAct.Array.Operations/add:property"}}{{/crossLink}}</li>
* <li>The index on which the chage occures.</li>
* <li>The old values beginning from the index.</li>
* <li>The new values beginning from the index.</li>
* </ol>
* Can be null. If null an empty (unchanging) event is created.
* @return {ProAct.Event}
* The event.
*/
makeEvent: function (source, eventData) {
if (!eventData) {
return new P.E(source, this.shell, P.E.Types.array, pArrayOps.setLength, -1, this.shell.length, this.shell.length);
}
var op = eventData[0],
ind = eventData[1],
oldVal = eventData[2],
newVal = eventData[3];
return new P.E(source, this.shell, P.E.Types.array, op, ind, oldVal, newVal);
},
/**
* Uses {{#crossLink "ProAct/currentCaller:property"}}{{/crossLink}} to automatically add a new listener to this property if the caller is set.
* <p>
* This method is used by the index getters or the length getter to make every reader of the length/index a listener to it.
* </p>
*
* @for ProAct.ArrayCore
* @protected
* @instance
* @method addCaller
* @param {String} type
* If the caller should be added as an 'index' listener or a 'length' listener. If skipped or null it is added as both.
*/
addCaller: function (type) {
if (!type) {
this.addCaller('index');
this.addCaller('length');
return;
}
var caller = P.currentCaller,
capType = type.charAt(0).toUpperCase() + type.slice(1),
lastCallerField = 'last' + capType + 'Caller',
lastCaller = this[lastCallerField];
if (caller && lastCaller !== caller) {
this.on(type, caller);
this[lastCallerField] = caller;
}
},
/**
* Special update method for updating listeners after a {{#crossLink "ProAct.Array/splice:method"}}{{/crossLink}} call.
* <p>
* Depending on the changes the index listeners, the length listeners or both can be notified.
* </p>
*
* @for ProAct.ArrayCore
* @instance
* @method updateSplice
* @param {Number} index
* The index of the splice operation.
* @param {Array} spliced
* A list of the deleted items. Can be empty.
* @param {Array} newItems
* A list of the newly added items. Can be empty.
* @return {ProAct.ArrayCore}
* <i>this</i>
*/
updateSplice: function (index, spliced, newItems) {
var actions, op = pArrayOps.splice;
if (!spliced || !newItems || (spliced.length === 0 && newItems.length === 0)) {
return;
}
if (spliced.length === newItems.length) {
actions = 'index';
} else if (!newItems.length || !spliced.length) {
actions = 'length';
}
return ActorUtil.update.call(this, null, actions, [op, index, spliced, newItems]);
},
/**
* Special update method for updating listeners by comparrison to another array.
* <p>
* For every difference between <i>this shell</i>'s array and the passed one, there will be listeners notification.
* </p>
*
* @for ProAct.ArrayCore
* @instance
* @method updateByDiff
* @param {Array} array
* The array to compare to.
* @return {ProAct.ArrayCore}
* <i>this</i>
*/
updateByDiff: function (array) {
var j, diff = P.U.diff(array, this.shell._array), cdiff;
for (j in diff) {
cdiff = diff[j];
if (cdiff) {
this.updateSplice(j, cdiff.o, cdiff.n);
}
}
return this;
},
/**
* Initializes all the index accessors and the length accessor for <i>this's shell array</i>.
* <p>
* For the length on every read, the {{#crossLink "ProAct/currentCaller:property"}}{{/crossLink}} is added as a 'length' listener.
* </p>
* <p>
* For every index on every read, the {{#crossLink "ProAct/currentCaller:property"}}{{/crossLink}} is added as an 'index' listener.
* Listener accessors are defined using {{#crossLink "ProAct.ArrayCore/defineIndexProp:method"}}{{/crossLink}}.
* </p>
* <p>
* {{#crossLink "ProAct.ArrayCore/addCaller:method"}}{{/crossLink}} is used to retrieve the current caller and add it as the right listener.
* </p>
* <p>
* Setting values for an index or the length updates the right listeners.
* </p>
*
* @for ProAct.ArrayCore
* @protected
* @instance
* @method setup
*/
setup: function () {
var self = this,
array = this.shell,
ln = array._array.length,
getLength, setLength, oldLength, i;
for (i = 0; i < ln; i++) {
this.defineIndexProp(i);
}
getLength = function () {
self.addCaller('length');
return array._array.length;
};
setLength = function (newLength) {
if (array._array.length === newLength) {
return;
}
oldLength = array._array.length;
array._array.length = newLength;
ActorUtil.update.call(self, null, 'length', [pArrayOps.setLength, -1, oldLength, newLength]);
return newLength;
};
Object.defineProperty(array, 'length', {
configurable: false,
enumerable: true,
get: getLength,
set: setLength
});
},
/**
* Defines accessors for index of <i>this' shell array</i>.
* <p>
* For an index on every read, the {{#crossLink "ProAct/currentCaller:property"}}{{/crossLink}} is added as an 'index' listener.
* </p>
* <p>
* {{#crossLink "ProAct.ArrayCore/addCaller:method"}}{{/crossLink}} is used to retrieve the current caller and add it as the right listener.
* </p>
* <p>
* Setting values for an index updates the 'index' listeners.
* </p>
* <p>
* If on the index is reciding an array or an object, it is turned to reactive object/array.
* </p>
*
* @for ProAct.ArrayCore
* @protected
* @instance
* @method defineIndexProp
* @param {Number} i
* The index to define accessor for.
*/
defineIndexProp: function (i) {
var self = this,
proArray = this.shell,
array = proArray._array,
oldVal,
isA = P.U.isArray,
isO = P.U.isObject,
isF = P.U.isFunction;
if (isA(array[i])) {
new P.ArrayProperty(array, i);
} else if (isF(array[i])) {
} else if (array[i] === null) {
} else if (isO(array[i])) {
this.isComplex = true;
new P.ObjectProperty(array, i);
}
Object.defineProperty(proArray, i, {
enumerable: true,
configurable: true,
get: function () {
self.addCaller('index');
return array[i];
},
set: function (newVal) {
if (array[i] === newVal) {
return;
}
oldVal = array[i];
array[i] = newVal;
ActorUtil.update.call(self, null, 'index', [pArrayOps.set, i, oldVal, newVal]);
}
});
}
});