/**
* ObservableArray is able to notify its change through events of four basic
* operations including 'insert', 'remove', 'replace', 'reset'.
* ObservableArray implements most used methods including 'push', 'pop', and
* 'splice'. Due to the syntax limitation it provides 'get' and 'set' for
* manuplication values stored in the array. 'reset' is also provided for
* replacing te entire array with another one. In addition to these methods,
* methods including 'forEach', 'map', 'every', 'some', 'indexOf',
* 'lastIndexOf', 'reduce', and 'reduceRight'.
*
* ObservableArray creation:
* It can be initialized by an ordinary array. If the array is not given, an
* empty is used by default.
*
* @example
* var observableArray = ObservableArray([1, 2, 3]);
*
* Events:
* Information regarding the operation is provided in the event detail. Please
* check the following example:
*
* @example
* var observableArray = ObservableArray([1, 2, 3]);
* var handler = {
* handleEvent: function(event) { console.log(JSON.stringify(event)); }
* }
* observableArray.addEventListener('insert', handler);
* observableArray.addEventListener('remove', handler);
* observableArray.addEventListener('replace', handler);
* observableArray.addEventListener('reset', handler);
*
* observableArray.push(4);
* // observableArray is [1, 2, 3, 4]
* // event.type is 'insert'
* // event.detail is {index: 3, count: 1, items: [4]}
*
* observableArray.pop();
* // observableArray is [1, 2, 3]
* // event.type is 'remove'
* // event.detail is {index: 3, count: 1, items: [4]}
*
* observableArray.set(2, 4);
* // observableArray is [1, 2, 4]
* // event.type is 'replace'
* // event.detail is {index: 2, oldValue: 3, newValue: 4}
*
* observableArray.reset([1, 2, 3]);
* // observableArray is [1, 2, 3]
* // event.type is 'reset'
* // event.detail is {items: [1, 2, 3]}
*
* observableArray.splice(2, 1, [4, 5, 6]);
* // observableArray is [1, 2, 4, 5, 6]
* // First event:
* // event.type is 'remove'
* // event.detail is {index: 2, count: 1, items: [3]}
* // Second event:
* // event.type is 'insert'
* // event.detail is {index: 2, count: 3, items: [4, 5, 6]}
*
* @module modules/mvvm/observable_array
*/
define(function(require) {
'use strict';
var Module = require('modules/base/module');
var EventEmitter = require('modules/base/event_emitter');
var Observable = require('modules/mvvm/observable');
var Events = ['insert', 'remove', 'replace', 'reset'];
var ReadOnlyMethods = ['forEach', 'map', 'every', 'some', 'indexOf',
'lastIndexOf', 'reduce', 'reduceRight'];
/**
* @class ObservableArray
* @requires module:modules/base/module
* @requires module:modules/base/event_emitter
* @requires module:modules/mvvm/observable
* @returns {ObservableArray}
*/
var ObservableArray = Module.create(function ObservableArray(array) {
this.super(Observable).call(this);
this.super(EventEmitter).call(this, Events);
this._array = array || [];
this._length = this._array.length;
}).extend(Observable, EventEmitter);
/**
* An observable property indicating the length of the array.
*
* @access public
* @type {Number}
*/
Observable.defineObservableProperty(ObservableArray.prototype, 'length', {
readonly: true
});
/**
* An observable property representing the inner array.
*
* @access public
* @type {Array}
*/
Observable.defineObservableProperty(ObservableArray.prototype, 'array', {
readonly: true
});
ReadOnlyMethods.forEach(function(op) {
ObservableArray.prototype[op] = function() {
return this._array[op].apply(this._array, arguments);
};
});
ObservableArray.prototype.push = function(item) {
this._array.push(item);
this._length = this._array.length;
this._emitEvent('insert', {
index: this._array.length - 1,
count: 1,
items: [item]
});
};
ObservableArray.prototype.pop = function() {
if (!this._array.length) {
return null;
}
var item = this._array.pop();
this._length = this._array.length;
this._emitEvent('remove', {
index: this._array.length,
count: 1,
items: [item]
});
return item;
};
ObservableArray.prototype.splice = function(index, count) {
if (arguments.length < 2) {
return [];
}
// Fix the arguments based on the behavior of the real spice function.
if (index >= this._length) {
index = this._length - 1;
} else if (index < 0) {
index = this._length + index;
}
if (count < 0) {
count = 0;
}
var addedItems = Array.prototype.slice.call(arguments, 2);
var removedItems = this._array.splice.apply(this._array, arguments);
this._length = this._array.length;
if (removedItems.length) {
this._emitEvent('remove', {
index: index,
count: count,
items: removedItems
});
}
if (addedItems.length) {
this._emitEvent('insert', {
index: index,
count: addedItems.length,
items: addedItems
});
}
return removedItems;
};
/**
* Set a value to the field of a specfied index.
*
* @access public
* @param {Number} index
* @param {Object} value
*/
ObservableArray.prototype.set = function(index, value) {
if (index < 0 || index >= this._array.length) {
this.throw('set: out of range');
}
var oldValue = this._array[index];
this._array[index] = value;
this._emitEvent('replace', {
index: index,
oldValue: oldValue,
newValue: value
});
};
/**
* Get the value from the field of a specfied index.
*
* @access public
* @param {Number} index
* @returns {Object}
*/
ObservableArray.prototype.get = function(index) {
return this._array[index];
};
/**
* Replace the entire array with another one.
*
* @access public
* @param {Array} array
*/
ObservableArray.prototype.reset = function(array) {
this._array = array || [];
this._length = this._array.length;
this._emitEvent('reset', {
items: this._array
});
};
return ObservableArray;
});