UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,135 lines (975 loc) 33.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (martinwittemann) ************************************************************************ */ /** * The data array is a special array used in the data binding context of * qooxdoo. It does not extend the native array of JavaScript but its a wrapper * for it. All the native methods are included in the implementation and it * also fires events if the content or the length of the array changes in * any way. Also the <code>.length</code> property is available on the array. * * This class does not need to be disposed, unless you set the autoDisposeItems * property to true and want the items to be disposed. */ qx.Class.define("qx.data.Array", { extend : qx.core.Object, include : qx.data.marshal.MEventBubbling, implement : [qx.data.IListData], /** * Creates a new instance of an array. * * @param param {var} The parameter can be some types.<br/> * Without a parameter a new blank array will be created.<br/> * If there is more than one parameter is given, the parameter will be * added directly to the new array.<br/> * If the parameter is a number, a new Array with the given length will be * created.<br/> * If the parameter is a JavaScript array, a new array containing the given * elements will be created. */ construct : function(param) { this.base(arguments); // if no argument is given if (param == undefined) { this.__array = []; // check for elements (create the array) } else if (arguments.length > 1) { // create an empty array and go through every argument and push it this.__array = []; for (var i = 0; i < arguments.length; i++) { this.__array.push(arguments[i]); } // check for a number (length) } else if (typeof param == "number") { this.__array = new Array(param); // check for an array itself } else if (param instanceof Array) { this.__array = qx.lang.Array.clone(param); // error case } else { this.__array = []; this.dispose(); throw new Error("Type of the parameter not supported!"); } // propagate changes for (var i=0; i<this.__array.length; i++) { this._applyEventPropagation(this.__array[i], null, i); } // update the length at startup this.__updateLength(); // work against the console printout of the array if (qx.core.Environment.get("qx.debug")) { this[0] = "Please use 'toArray()' to see the content."; } }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Flag to set the dispose behavior of the array. If the property is set to * <code>true</code>, the array will dispose its content on dispose, too. */ autoDisposeItems : { check : "Boolean", init : false } }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events : { /** * The change event which will be fired if there is a change in the array. * The data contains a map with five key value pairs: * <li>start: The start index of the change.</li> * <li>end: The end index of the change.</li> * <li>type: The type of the change as a String. This can be 'add', * 'remove', 'order' or 'add/remove'</li> * <li>added: The items which has been added (as a JavaScript array)</li> * <li>removed: The items which has been removed (as a JavaScript array)</li> */ "change" : "qx.event.type.Data", /** * The changeLength event will be fired every time the length of the * array changes. */ "changeLength": "qx.event.type.Data" }, members : { // private members __array : null, /** * Concatenates the current and the given array into a new one. * * @param array {qx.data.Array|Array} The javaScript array which should be concatenated * to the current array. * * @return {qx.data.Array} A new array containing the values of both former * arrays. */ concat: function(array) { array = qx.lang.Array.toNativeArray(array); if (array) { var newArray = this.__array.concat(array); } else { var newArray = this.__array.concat(); } return new qx.data.Array(newArray); }, /** * Returns the array as a string using the given connector string to * connect the values. * * @param connector {String} the string which should be used to past in * between of the array values. * * @return {String} The array as a string. */ join: function(connector) { return this.__array.join(connector); }, /** * Removes and returns the last element of the array. * An change event will be fired. * * @return {var} The last element of the array. */ pop: function() { var item = this.__array.pop(); this.__updateLength(); // remove the possible added event listener this._registerEventChaining(null, item, this.length - 1); // fire change bubble event this.fireDataEvent("changeBubble", { value: [], name: this.length + "", old: [item], item: this }); this.fireDataEvent("change", { start: this.length - 1, end: this.length - 1, type: "remove", removed : [item], added : [] }, null ); return item; }, /** * Adds an element at the end of the array. * * @param varargs {var} Multiple elements. Every element will be added to * the end of the array. An change event will be fired. * * @return {Number} The new length of the array. */ push: function(varargs) { for (var i = 0; i < arguments.length; i++) { this.__array.push(arguments[i]); this.__updateLength(); // apply to every pushed item an event listener for the bubbling this._registerEventChaining(arguments[i], null, this.length - 1); // fire change bubbles event this.fireDataEvent("changeBubble", { value: [arguments[i]], name: (this.length - 1) + "", old: [], item: this }); // fire change event this.fireDataEvent("change", { start: this.length - 1, end: this.length - 1, type: "add", added: [arguments[i]], removed : [] }, null ); } return this.length; }, /** * Reverses the order of the array. An change event will be fired. */ reverse: function() { // ignore on empty arrays if (this.length == 0) { return; } var oldArray = this.__array.concat(); this.__array.reverse(); this.__updateEventPropagation(0, this.length); this.fireDataEvent("change", {start: 0, end: this.length - 1, type: "order", added: [], removed: []}, null ); // fire change bubbles event this.fireDataEvent("changeBubble", { value: this.__array, name: "0-" + (this.__array.length - 1), old: oldArray, item: this }); }, /** * Removes the first element of the array and returns it. An change event * will be fired. * * @return {var} the former first element. */ shift: function() { // ignore on empty arrays if (this.length == 0) { return; } var item = this.__array.shift(); this.__updateLength(); // remove the possible added event listener this._registerEventChaining(null, item, this.length -1); // as every item has changed its position, we need to update the event bubbling this.__updateEventPropagation(0, this.length); // fire change bubbles event this.fireDataEvent("changeBubble", { value: [], name: "0", old: [item], item: this }); // fire change event this.fireDataEvent("change", { start: 0, end: this.length -1, type: "remove", removed : [item], added : [] }, null ); return item; }, /** * Returns a new array based on the range specified by the parameters. * * @param from {Number} The start index. * @param to {Number?null} The zero-based end index. <code>slice</code> extracts * up to but not including <code>to</code>. If omitted, slice extracts to the * end of the array. * * @return {qx.data.Array} A new array containing the given range of values. */ slice: function(from, to) { return new qx.data.Array(this.__array.slice(from, to)); }, /** * Method to remove and add new elements to the array. A change event * will be fired for every removal or addition unless the array is * identical before and after splicing. * * @param startIndex {Integer} The index where the splice should start * @param amount {Integer} Defines number of elements which will be removed * at the given position. * @param varargs {var} All following parameters will be added at the given * position to the array. * @return {qx.data.Array} An data array containing the removed elements. * Keep in to dispose this one, even if you don't use it! */ splice: function(startIndex, amount, varargs) { // store the old length var oldLength = this.__array.length; // invoke the slice on the array var returnArray = this.__array.splice.apply(this.__array, arguments); // fire a change event for the length if (this.__array.length != oldLength) { this.__updateLength(); } else if (amount == arguments.length - 2) { // if we added as much items as we removed var addedItems = qx.lang.Array.fromArguments(arguments, 2); // check if the array content equals the content before the operation for (var i = 0; i < addedItems.length; i++) { if (addedItems[i] !== returnArray[i]) { break; } // if all added and removed items are equal if (i == addedItems.length -1) { // prevent all events and return a new array return new qx.data.Array(); } } } // fire an event for the change var removed = amount > 0; var added = arguments.length > 2; if (removed || added) { var addedItems = qx.lang.Array.fromArguments(arguments, 2); if (returnArray.length == 0) { var type = "add"; var end = startIndex + addedItems.length; } else if (addedItems.length == 0) { var type = "remove"; var end = this.length - 1; } else { var type = "add/remove"; var end = startIndex + Math.max(addedItems.length, returnArray.length) - 1; } this.fireDataEvent("change", { start: startIndex, end: end, type: type, added : addedItems, removed : returnArray }, null ); } // remove the listeners first [BUG #7132] for (var i = 0; i < returnArray.length; i++) { this._registerEventChaining(null, returnArray[i], i); } // add listeners for (var i = 2; i < arguments.length; i++) { this._registerEventChaining(arguments[i], null, startIndex + (i - 2)); } // apply event chaining for every item moved this.__updateEventPropagation(startIndex + (arguments.length - 2) - amount, this.length); // fire the changeBubble event if (removed || added) { var value = []; for (var i = 2; i < arguments.length; i++) { value[i-2] = arguments[i]; } var endIndex = (startIndex + Math.max(arguments.length - 3 , amount - 1)); var name = startIndex == endIndex ? endIndex : startIndex + "-" + endIndex; var eventData = { value: value, name: name + "", old: returnArray, item: this }; this.fireDataEvent("changeBubble", eventData); } return (new qx.data.Array(returnArray)); }, /** * Efficiently replaces the array with the contents of src; this will suppress the * change event if the array contents are the same, and will make sure that only * one change event is fired * * @param src {qx.data.Array|Array} the new value to set the array to */ replace: function(src) { src = qx.lang.Array.toNativeArray(src); if (this.equals(src)) { return; } var args = [ 0, this.getLength() ]; src.forEach(function(item) { args.push(item); }); this.splice.apply(this, args); }, /** * Sorts the array. If a function is given, this will be used to * compare the items. <code>changeBubble</code> event will only be fired, * if sorting result differs from original array. * * @param func {Function} A compare function comparing two parameters and * should return a number. */ sort: function(func) { // ignore if the array is empty if (this.length == 0) { return; } var oldArray = this.__array.concat(); this.__array.sort.apply(this.__array, arguments); // prevent changeBubble event if nothing has been changed if (qx.lang.Array.equals(this.__array, oldArray) === true){ return; } this.__updateEventPropagation(0, this.length); this.fireDataEvent("change", {start: 0, end: this.length - 1, type: "order", added: [], removed: []}, null ); // fire change bubbles event this.fireDataEvent("changeBubble", { value: this.__array, name: "0-" + (this.length - 1), old: oldArray, item: this }); }, /** * Adds the given items to the beginning of the array. For every element, * a change event will be fired. * * @param varargs {var} As many elements as you want to add to the beginning. * @return {Integer} The new length of the array */ unshift: function(varargs) { for (var i = arguments.length - 1; i >= 0; i--) { this.__array.unshift(arguments[i]); this.__updateLength(); // apply to every item an event listener for the bubbling this.__updateEventPropagation(0, this.length); // fire change bubbles event this.fireDataEvent("changeBubble", { value: [this.__array[0]], name: "0", old: [this.__array[1]], item: this }); // fire change event this.fireDataEvent("change", { start: 0, end: this.length - 1, type: "add", added : [arguments[i]], removed : [] }, null ); } return this.length; }, /** * Returns the list data as native array. Beware of the fact that the * internal representation will be returned and any manipulation of that * can cause a misbehavior of the array. This method should only be used for * debugging purposes. * * @return {Array} The native array. */ toArray: function() { return this.__array; }, /** * Replacement function for the getting of the array value. * array[0] should be array.getItem(0). * * @param index {Number} The index requested of the array element. * * @return {var} The element at the given index. */ getItem: function(index) { return this.__array[index]; }, /** * Replacement function for the setting of an array value. * array[0] = "a" should be array.setItem(0, "a"). * A change event will be fired if the value changes. Setting the same * value again will not lead to a change event. * * @param index {Number} The index of the array element. * @param item {var} The new item to set. */ setItem: function(index, item) { var oldItem = this.__array[index]; // ignore settings of already set items [BUG #4106] if (oldItem === item) { return; } this.__array[index] = item; // set an event listener for the bubbling this._registerEventChaining(item, oldItem, index); // only update the length if its changed if (this.length != this.__array.length) { this.__updateLength(); } // fire change bubbles event this.fireDataEvent("changeBubble", { value: [item], name: index + "", old: [oldItem], item: this }); // fire change event this.fireDataEvent("change", { start: index, end: index, type: "add/remove", added: [item], removed: [oldItem] }, null ); }, /** * This method returns the current length stored under .length on each * array. * * @return {Number} The current length of the array. */ getLength: function() { return this.length; }, /** * Returns the index of the item in the array. If the item is not in the * array, -1 will be returned. * * @param item {var} The item of which the index should be returned. * @return {Number} The Index of the given item. */ indexOf: function(item) { return this.__array.indexOf(item); }, /** * Returns the last index of the item in the array. If the item is not in the * array, -1 will be returned. * * @param item {var} The item of which the index should be returned. * @return {Number} The Index of the given item. */ lastIndexOf: function(item) { return this.__array.lastIndexOf(item); }, /** * Returns the toString of the original Array * @return {String} The array as a string. */ toString: function() { if (this.__array != null) { return this.__array.toString(); } return ""; }, /* --------------------------------------------------------------------------- IMPLEMENTATION OF THE QX.LANG.ARRAY METHODS --------------------------------------------------------------------------- */ /** * Check if the given item is in the current array. * * @deprecated {6.0} Please use the include method instead * * @param item {var} The item which is possibly in the array. * @return {Boolean} true, if the array contains the given item. */ contains: function(item) { return this.includes(item); }, /** * Check if the given item is in the current array. * * @param item {var} The item which is possibly in the array. * @return {Boolean} true, if the array contains the given item. */ includes: function(item) { return this.__array.indexOf(item) !== -1; }, /** * Return a copy of the given arr * * @return {qx.data.Array} copy of this */ copy : function() { return this.concat(); }, /** * Insert an element at a given position. * * @param index {Integer} Position where to insert the item. * @param item {var} The element to insert. */ insertAt : function(index, item) { this.splice(index, 0, item).dispose(); }, /** * Insert an item into the array before a given item. * * @param before {var} Insert item before this object. * @param item {var} The item to be inserted. */ insertBefore : function(before, item) { var index = this.indexOf(before); if (index == -1) { this.push(item); } else { this.splice(index, 0, item).dispose(); } }, /** * Insert an element into the array after a given item. * * @param after {var} Insert item after this object. * @param item {var} Object to be inserted. */ insertAfter : function(after, item) { var index = this.indexOf(after); if (index == -1 || index == (this.length - 1)) { this.push(item); } else { this.splice(index + 1, 0, item).dispose(); } }, /** * Remove an element from the array at the given index. * * @param index {Integer} Index of the item to be removed. * @return {var} The removed item. */ removeAt : function(index) { var returnArray = this.splice(index, 1); var item = returnArray.getItem(0); returnArray.dispose(); return item; }, /** * Remove all elements from the array. * * @return {Array} A native array containing the removed elements. */ removeAll : function() { // remove all possible added event listeners for (var i = 0; i < this.__array.length; i++) { this._registerEventChaining(null, this.__array[i], i); } // ignore if array is empty if (this.getLength() == 0) { return []; } // store the old data var oldLength = this.getLength(); var items = this.__array.concat(); // change the length this.__array.length = 0; this.__updateLength(); // fire change bubbles event this.fireDataEvent("changeBubble", { value: [], name: "0-" + (oldLength - 1), old: items, item: this }); // fire the change event this.fireDataEvent("change", { start: 0, end: oldLength - 1, type: "remove", removed : items, added : [] }, null ); return items; }, /** * Append the items of the given array. * * @param array {Array|qx.data.IListData} The items of this array will * be appended. * @throws {Error} if the argument is not an array. */ append : function(array) { // qooxdoo array support array = qx.lang.Array.toNativeArray(array); // this check is important because opera throws an uncatchable error if // apply is called without an array as argument. if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertArray(array, "The parameter must be an array."); } var oldLength = this.__array.length; Array.prototype.push.apply(this.__array, array); // add a listener to the new items for (var i = 0; i < array.length; i++) { this._registerEventChaining(array[i], null, oldLength + i); } var oldLength = this.length; this.__updateLength(); // fire change bubbles var name = oldLength == (this.length-1) ? oldLength : oldLength + "-" + (this.length-1); this.fireDataEvent("changeBubble", { value: array, name: name + "", old: [], item: this }); // fire the change event this.fireDataEvent("change", { start: oldLength, end: this.length - 1, type: "add", added : array, removed : [] }, null ); }, /** * Removes all elements which are listed in the array. * * @param array {Array} the elements of this array will be excluded from this one */ exclude : function(array) { array = qx.lang.Array.toNativeArray(array); array.forEach(function(item) { this.remove(item); }, this); }, /** * Remove the given item. * * @param item {var} Item to be removed from the array. * @return {var} The removed item. */ remove : function(item) { var index = this.indexOf(item); if (index != -1) { this.splice(index, 1).dispose(); return item; } }, /** * Check whether the given array has the same content as this. * Checks only the equality of the arrays' content. * * @param array {qx.data.Array} The array to check. * @return {Boolean} Whether the two arrays are equal. */ equals : function(array) { if (this.length !== array.length) { return false; } array = qx.lang.Array.toNativeArray(array); for (var i = 0; i < this.length; i++) { if (this.getItem(i) !== array[i]) { return false; } } return true; }, /** * Returns the sum of all values in the array. Supports * numeric values only. * * @return {Number} The sum of all values. */ sum : function() { var result = 0; for (var i = 0; i < this.length; i++) { result += this.getItem(i); } return result; }, /** * Returns the highest value in the given array. * Supports numeric values only. * * @return {Number | null} The highest of all values or undefined if the * array is empty. */ max : function() { var result = this.getItem(0); for (var i = 1; i < this.length; i++) { if (this.getItem(i) > result) { result = this.getItem(i); } } return result === undefined ? null : result; }, /** * Returns the lowest value in the array. Supports * numeric values only. * * @return {Number | null} The lowest of all values or undefined * if the array is empty. */ min : function() { var result = this.getItem(0); for (var i = 1; i < this.length; i++) { if (this.getItem(i) < result) { result = this.getItem(i); } } return result === undefined ? null : result; }, /** * Invokes the given function for every item in the array. * * @param callback {Function} The function which will be call for every * item in the array. It will be invoked with three parameters: * the item, the index and the array itself. * @param context {var?} The context in which the callback will be invoked. */ forEach : function(callback, context) { for (var i = 0; i < this.__array.length; i++) { callback.call(context, this.__array[i], i, this); } }, /* --------------------------------------------------------------------------- Additional JS1.6 methods --------------------------------------------------------------------------- */ /** * Creates a new array with all elements that pass the test implemented by * the provided function. It returns a new data array instance so make sure * to think about disposing it. * @param callback {Function} The test function, which will be executed for every * item in the array. The function will have three arguments. * <li><code>item</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param self {var?undefined} The context of the callback. * @return {qx.data.Array} A new array instance containing only the items * which passed the test. */ filter : function(callback, self) { return new qx.data.Array(this.__array.filter(callback, self)); }, /** * Creates a new array with the results of calling a provided function on every * element in this array. It returns a new data array instance so make sure * to think about disposing it. * @param callback {Function} The mapping function, which will be executed for every * item in the array. The function will have three arguments. * <li><code>item</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param self {var?undefined} The context of the callback. * @return {qx.data.Array} A new array instance containing the new created items. */ map : function(callback, self) { return new qx.data.Array(this.__array.map(callback, self)); }, /** * Tests whether any element in the array passes the test implemented by the * provided function. * @param callback {Function} The test function, which will be executed for every * item in the array. The function will have three arguments. * <li><code>item</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param self {var?undefined} The context of the callback. * @return {Boolean} <code>true</code>, if any element passed the test function. */ some : function(callback, self) { return this.__array.some(callback, self); }, /** * Tests whether every element in the array passes the test implemented by the * provided function. * @param callback {Function} The test function, which will be executed for every * item in the array. The function will have three arguments. * <li><code>item</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param self {var?undefined} The context of the callback. * @return {Boolean} <code>true</code>, if every element passed the test function. */ every : function(callback, self) { return this.__array.every(callback, self); }, /** * Apply a function against an accumulator and each value of the array * (from left-to-right) as to reduce it to a single value. * @param callback {Function} The accumulator function, which will be * executed for every item in the array. The function will have four arguments. * <li><code>previousItem</code>: the previous item</li> * <li><code>currentItem</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param initValue {var?undefined} Object to use as the first argument to the first * call of the callback. * @return {var} The returned value of the last accumulator call. */ reduce : function(callback, initValue) { return this.__array.reduce(callback, initValue); }, /** * Apply a function against an accumulator and each value of the array * (from right-to-left) as to reduce it to a single value. * @param callback {Function} The accumulator function, which will be * executed for every item in the array. The function will have four arguments. * <li><code>previousItem</code>: the previous item</li> * <li><code>currentItem</code>: the current item in the array</li> * <li><code>index</code>: the index of the current item</li> * <li><code>array</code>: The native array instance, NOT the data array instance.</li> * @param initValue {var?undefined} Object to use as the first argument to the first * call of the callback. * @return {var} The returned value of the last accumulator call. */ reduceRight : function(callback, initValue) { return this.__array.reduceRight(callback, initValue); }, /* --------------------------------------------------------------------------- INTERNAL HELPERS --------------------------------------------------------------------------- */ /** * Internal function which updates the length property of the array. * Every time the length will be updated, a {@link #changeLength} data * event will be fired. */ __updateLength: function() { var oldLength = this.length; this.length = this.__array.length; this.fireDataEvent("changeLength", this.length, oldLength); }, /** * Helper to update the event propagation for a range of items. * @param from {Number} Start index. * @param to {Number} End index. */ __updateEventPropagation : function(from, to) { for (var i=from; i < to; i++) { this._registerEventChaining(this.__array[i], this.__array[i], i); }; } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { for (var i = 0; i < this.__array.length; i++) { var item = this.__array[i]; this._applyEventPropagation(null, item, i); // dispose the items on auto dispose if (this.isAutoDisposeItems() && item && item instanceof qx.core.Object) { item.dispose(); } } this.__array = null; } });