From b89a7ee8b956c96a1dcee995ea840feddc5d4b27 Mon Sep 17 00:00:00 2001 From: Pierre Frisch Date: Thu, 22 Dec 2011 07:25:50 -0800 Subject: First commit of Ninja to ninja-internal Signed-off-by: Valerio Virgillito --- .../montage/ui/controller/array-controller.js | 796 +++++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100755 node_modules/montage/ui/controller/array-controller.js (limited to 'node_modules/montage/ui/controller/array-controller.js') diff --git a/node_modules/montage/ui/controller/array-controller.js b/node_modules/montage/ui/controller/array-controller.js new file mode 100755 index 00000000..98b0c66f --- /dev/null +++ b/node_modules/montage/ui/controller/array-controller.js @@ -0,0 +1,796 @@ +/* + This file contains proprietary software owned by Motorola Mobility, Inc.
+ No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
+ (c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. +
*/ +/** + @module montage/ui/controller/array-controller + @requires montage/core/core + @requires montage/ui/controller/object-controller + @requires montage/core/event/mutable-event + */ +var Montage = require("montage").Montage, + ObjectController = require("ui/controller/object-controller").ObjectController, + MutableEvent = require("core/event/mutable-event").MutableEvent; +/** + The ArrayController helps with organizing a collection of objects, and managing user selection within that collection. + TYou can assign an ArrayController instance as the contentProvider property for a Montage List or Repetition object. + @class module:montage/ui/controller/array-controller.ArrayController + @classdesc + @extends module:montage/ui/controller/object-controller.ObjectController +*/ +var ArrayController = exports.ArrayController = Montage.create(ObjectController, /** @lends module:montage/ui/controller/array-controller.ArrayController# */ { + + /** + @private + */ + _content: { + enumerable: false, + value: null + }, + /** + The content managed by the ArrayController. + @type {Function} + @default {String} null + */ + content: { + get: function() { + return this._content; + }, + set: function(value) { + + if (this._content === value) { + return; + } + + this._content = value; + + //TODO for right now assume that any content change invalidates the selection completely; we'll need to address this of course + this.selectedObjects = null; + + this.organizeObjects(); + } + }, + + /** + The user-defined delegate object for the ArrayController.
+ If a delegate object exists, the ArrayController will notify the delegate of changes to the collection, or the selection.
+ Currently, the only supported delegate method is shouldChangeSelection, which passes the new and old selected objects.
+ If the function returns false the selection action is canceled. + @type {Object} + @default null + @example + var ArrayController = require("ui/controller/array-controller").ArrayController; + var controller = ArrayController.create(); + controller.delegate = Montage.create(Object.prototype, { + shouldChangeSelection: function(newObj, oldObj) { + console.log("New object is", newObj, "Old object is", oldObj); + } + }) + */ + delegate: { + value: null + }, + + /** + @private + */ + _organizedObjects: { + enumerable: false, + value: null + }, + + /** + The filtered and sorted content of the ArrayCollection. + @type {Function} + @default null + */ + organizedObjects: { + enumerable: false, + get: function() { + + if (this._organizedObjects) { + return this._organizedObjects; + } + + this.organizeObjects(); + + return this._organizedObjects; + } + }, + + /** + Specifies whether the ArrayCollection's content is automatically organized (false, by default). + @type {Property} + @default {Boolean} false + */ + automaticallyOrganizeObjects: { + value: false + }, + + /** + @private + */ + _sortFunction: { + enumerable: false, + value: null + }, + + /** + The sort function used to organize the array collection. + @type {Function} + @default null + @version 1.0 + */ + sortFunction: { + get: function() { + return this._sortFunction; + }, + set: function(value) { + + if (this._sortFunction === value) { + return; + } + + this._sortFunction = value; + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /** + @private + */ + _filterFunction: { + enumerable: false, + value: null + }, + + /** + The filter function used to organize the array collection. + @type {Function} + @default null + @version 1.0 + */ + filterFunction: { + get: function() { + return this._filterFunction; + }, + set: function(value) { + + if (this._filterFunction === value) { + return; + } + + this._filterFunction = value; + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /** + @private + */ + _startIndex: { + enumerable: false, + value: null + }, + + /** + The start index of the organized objects. + @type {Function} + @default null + @version 1.0 + */ + startIndex: { + enumerable: false, + get: function() { + return this._startIndex; + }, + set: function(value) { + + if (this._startIndex === value) { + return; + } + + this._startIndex = value; + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /** + @private + */ + _endIndex: { + enumerable: false, + value: null + }, + + /** + The start index of the organized objects. + @type {Function} + @default null + @version 1.0 + */ + endIndex: { + enumerable: false, + get: function() { + return this._endIndex; + }, + set: function(value) { + + if (this._endIndex === value) { + return; + } + + this._endIndex = value; + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /* + @private + */ + _organizedObjectsIndexes: { + value: null, + enumerable: false + }, + + /* + @private + */ + _rangedOrganizedObjectsIndexes: { + value: null, + enumerable: false + }, + + /** + @private + */ + _selectedIndexes: { + value: null, + enumerable: false + }, + + /** + Description TODO + @type {Function} + @default null + */ + selectedIndexes: { + get: function() { + + if (this._selectedIndexes) { + return this._selectedIndexes; + } + + if (!this._selectedContentIndexes) { + return null; + } + return this._selectedIndexes = this._convertIndexesFromContentToOrganized(this.selectedContentIndexes); + }, + set: function(value) { + if (this._selectedIndexes !== value) { + var newIndexes = value ? this._convertIndexesFromOrganizedToContent(value) : null, + newSelection = null; + if (this.delegate && typeof this.delegate.shouldChangeSelection === "function") { + if (newIndexes) { + newSelection = this.content.filter(function(value, i) { + return newIndexes.indexOf(i) >= 0; + }, this); + } + + if (this.delegate.shouldChangeSelection(this, newSelection, this._selectedObjects) === false) { + return; + } + } + + this._selectedIndexes = value; + var selectedContentIndexesChangeEvent,selectedObjectsChangeEvent; + selectedContentIndexesChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedContentIndexes" , this.selectedContentIndexes); + selectedObjectsChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedObjects" , this._selectedObjects); + + this._selectedContentIndexes = newIndexes; + this._selectedObjects = null; + + this.dispatchEvent(selectedContentIndexesChangeEvent.withPlusValue(this.selectedContentIndexes)); + this.dispatchEvent(selectedObjectsChangeEvent.withPlusValue(this.selectedObjects)); + } + } + }, + + /** + @private + */ + _convertIndexesFromOrganizedToContent: { + value: function(indexes) { + var index, selectedContentIndexes = [], lookup, valueLength = indexes.length, selectedIndex, lookupLength; + // if _rangedOrganizedObjectsIndexes != null we have applied a range + lookup = this._rangedOrganizedObjectsIndexes ? this._rangedOrganizedObjectsIndexes : this._organizedObjectsIndexes; + if (lookup) { + lookupLength = lookup.length; + for (index = 0; index < valueLength ; index++) { + selectedIndex = indexes[index]; + if(selectedIndex < lookupLength && selectedIndex >= 0) { + selectedContentIndexes[selectedContentIndexes.length] = lookup[selectedIndex]; + } + } + } else { + // then it's just a range with not filter or sort + for (index = 0; index < valueLength ; index++) { + selectedContentIndexes[selectedContentIndexes.length] = indexes[index] + this.startIndex; + } + } + return selectedContentIndexes.sort(); + } + }, + + /** + @private + */ + _convertIndexesFromContentToOrganized: { + value: function(indexes) { + var index, selectedOrganizedIndexes = [], lookup, valueLength = indexes.length, selectedIndex; + // if _rangedOrganizedObjectsIndexes != null we have applied a range + lookup = this._rangedOrganizedObjectsIndexes ? this._rangedOrganizedObjectsIndexes : this._organizedObjectsIndexes; + if (lookup) { + for (index = 0; index < valueLength ; index++) { + selectedIndex = indexes[index]; + if (selectedIndex >= 0) { + selectedIndex = lookup.indexOf(selectedIndex); + if (selectedIndex !== -1) { + selectedOrganizedIndexes[selectedOrganizedIndexes.length] = selectedIndex; + } + } + } + } else { + // then it's just a range with not filter or sort + for (index = 0; index < valueLength ; index++) { + selectedIndex = indexes[index] - this.startIndex; + // Check if we are within the range of the current organizedObjects + if(selectedIndex > -1 && (this.endIndex == null || selectedIndex < this.endIndex)) { + selectedOrganizedIndexes[selectedOrganizedIndexes.length] = selectedIndex; + } + } + } + return selectedOrganizedIndexes.sort(); + } + }, + + /** + Organizes the array collection using the filter and sort functions, if defined.
+ Dispatches a change event. + @function + @fires change@organizeObjects + */ + organizeObjects: { + value: function() { + + var organizedObjects = this.content, + funktion = this.filterFunction, + index = 0, + newIndex = 0, + filteredIndexes, + sortedIndexes, + startIndex = this.startIndex, + endIndex = this.endIndex, + tmpArray, item; + + if (organizedObjects && typeof funktion === "function") { + filteredIndexes = []; + organizedObjects = organizedObjects.filter(function filterFunctionWrapper(item) { + var filterValue = funktion.call(this, item); + if (filterValue) { + // we are going to keep the item in the new array + filteredIndexes[newIndex] = index; + newIndex++; + } + index++; + return filterValue; + }, this); + } + + if (typeof (funktion = this._sortFunction) === "function") { + + // need to attach index information so that we can pick it up after the sort + // this has the added side effect of creating a clone of the array so that if it wasn't filtered, + // we are not editing the content directly + sortedIndexes = []; + tmpArray = []; + index = 0; + + for (index = 0; (item = organizedObjects[index]); index++) { + if (item !== null && typeof item === "object") { + // attach the index + item._montage_array_controller_index = index; + tmpArray[index] = item; + } else { + // wrap in a host object + tmpArray[index] = { + _montage_array_controller_index : index, + _montage_array_controller_value : item + }; + } + } + + // Do the sort + tmpArray = tmpArray.sort(function sortFunctionWrapper(a, b) { + if (a._montage_array_controller_value) { + a = a._montage_array_controller_value; + } + if (b._montage_array_controller_value) { + b = b._montage_array_controller_value; + } + return funktion.call(this, a, b); + }); + + // get all the new indexes + organizedObjects = []; + for (index = 0; (item = tmpArray[index]); index++) { + newIndex = item._montage_array_controller_index; + sortedIndexes[index] = filteredIndexes ? filteredIndexes[newIndex] : newIndex; + if (item._montage_array_controller_value) { + organizedObjects[index] = item._montage_array_controller_value; + } else { + organizedObjects[index] = item; + delete item._montage_array_controller_index; + } + } + // store it for later use + this._organizedObjectsIndexes = sortedIndexes; + } else { + // store it for later use + this._organizedObjectsIndexes = filteredIndexes; + } + + this._applyRangeIfNeeded(organizedObjects); + } + }, + + /** + @private + */ + _applyRangeIfNeeded: { + value: function(organizedObjects) { + + var startIndex = this.startIndex, + endIndex = this.endIndex, + changeEvent; + + + // We apply the range after the content is filtered and sorted + if (organizedObjects && (typeof startIndex === "number" || typeof endIndex === "number")) { + startIndex = typeof startIndex === "number" && startIndex >= 0 ? startIndex : 0; + endIndex = typeof endIndex === "number" && endIndex < organizedObjects.length ? endIndex : organizedObjects.length; + // apply the range + organizedObjects = organizedObjects.slice(startIndex, endIndex); + // store the index lookup change + if(this._organizedObjectsIndexes) { + this._rangedOrganizedObjectsIndexes = this._organizedObjectsIndexes.slice(startIndex, endIndex); + } else { + this._rangedOrganizedObjectsIndexes = null; + } + } + + // I don't want to provide a setter for organizedObjects, so update the cached value and notify observers that + // the organizedObjects property changed + changeEvent = MutableEvent.changeEventForKeyAndValue("organizedObjects" , this._organizedObjects ? this._organizedObjects.slice(0) : this._organizedObjects); + + this._organizedObjects = organizedObjects ? organizedObjects : []; + + this.dispatchEvent(changeEvent.withPlusValue(this._organizedObjects)); + } + }, + + /** + @private + */ + _selectedObjects: { + enumerable: false, + value: null + }, + + /** + Gets or sets the selected objects in the collection.
+ Setting the selected objects to a new value fires an event of type change@selectedObjects. + @type {Function} + @default null + @fires change@selectedContentIndexes + */ + selectedObjects: { + enumerable: false, + get: function() { + + if (this._selectedObjects) { + return this._selectedObjects; + } + + if (!this._selectedContentIndexes) { + return null; + } + + this._selectedObjects = this.content.filter(function(value, i) { + return this._selectedContentIndexes.indexOf(i) >= 0; + }, this); + + return this._selectedObjects; + }, + set: function(value) { + + // Normalizing the value before the difference check prevents false-positive "changes" for things like [x]=>x + if (value === null || typeof value === "undefined") { + // undefined => null + value = null; + } else if (!Array.isArray(value)) { + // any single object, including false and zero + value = [value]; + } + + // TODO validate the array content maybe? + + if (this._selectedObjects === value) { + return; + } + + if (this.delegate && typeof this.delegate.shouldChangeSelection === "function") { + if (this.delegate.shouldChangeSelection(this, value, this._selectedObjects) === false) { + return; + } + } + this._selectedObjects = value; + + var selectedContentIndexesChangeEvent, + selectedIndexesChangeEvent; + + selectedContentIndexesChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedContentIndexes" , this._selectedContentIndexes); + selectedIndexesChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedIndexes" , this._selectedIndexes); + + this._selectedContentIndexes = null; + this._selectedIndexes = null; + + this.dispatchEvent(selectedContentIndexesChangeEvent.withPlusValue(this.selectedContentIndexes)); + this.dispatchEvent(selectedIndexesChangeEvent.withPlusValue(this.selectedIndexes)); + } + }, + + /** + @private + */ + _selectedContentIndexes: { + enumerable: false, + value: null + }, + + /** + Gets or sets the indexes of the currently selected items in the collection.
+ When set to a new set of indexes, generates a event of type "change@selectedObjects". + @type {Function} + @default null + */ + selectedContentIndexes: { + enumerable: false, + get: function() { + + if (this._selectedContentIndexes) { + return this._selectedContentIndexes; + } + + if (!this._selectedObjects) { + return null; + } + + this._selectedContentIndexes = []; + var selectedIndex; + this._selectedObjects.forEach(function(value) { + if((selectedIndex = this.content.indexOf(value)) !== -1) { + this._selectedContentIndexes.push(selectedIndex); + } + }, this); + + return this._selectedContentIndexes; + }, + set: function(value) { + var selectedIndexesChangeEvent, + selectedObjectsChangeEvent; + + // Normalizing the value before the difference check prevents false-positive "changes" for things like [x]=>x + if (value === null || value === false || typeof value === "undefined") { + // undefined, false => null + value = null; + } else if (!Array.isArray(value)) { + // any single index, including zero + value = [value]; + } + + // TODO validate the array content maybe? + + if (this._selectedContentIndexes === value) { + return; + } + + if (this.delegate && typeof this.delegate.shouldChangeSelection === "function") { + var newIndexes = value, newSelection = null; + + if (newIndexes) { + newSelection = this.content.filter(function(value, i) { + return newIndexes.indexOf(i) >= 0; + }, this); + } + + if (this.delegate.shouldChangeSelection(this, newSelection, this._selectedObjects) === false) { + return; + } + } + + this._selectedContentIndexes = value; + + selectedIndexesChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedIndexes" , this._selectedIndexes); + selectedObjectsChangeEvent = MutableEvent.changeEventForKeyAndValue("selectedObjects" , this._selectedObjects); + + this._selectedIndexes = null; + this._selectedObjects = null; + + this.dispatchEvent(selectedIndexesChangeEvent.withPlusValue(this.selectedIndexes)); + this.dispatchEvent(selectedObjectsChangeEvent.withPlusValue(this.selectedObjects)); + } + }, + + /** + Initalizes the ArrayController with the specified content. + @function + @param {Object} content The collection of objects for the ArrayController to manage. + @returns {ArrayController} + */ + + initWithContent: { + value: function(content) { + this.content = content; + return this; + } + }, + + /** + A Boolean that specifies whether new objects that are added to the array collection are automatically selected. + @type {Property} + @default {Boolean} false + */ + selectObjectsOnAddition: { + enumerable: false, + value: false + }, + + /** + A Boolean that specifies whether the filter function is set to null when new objects that are added to the content. + @type {Property} + @default {Boolean} true + */ + clearFilterFunctionOnAddition: { + enumerable: false, + value: true + }, + + /** + Adds an item to the array collection's content property. + @function + @example + var ArrayCollection = require("ui/controller/array-controller").ArrayCollection; + var ac = ArrayCollection.create(); + ac.add({name: "John"}); + */ + add: { + value: function() { + + var newObject = this.newObject(); + this.content.push(newObject); + + //TODO fdf what do we do with the filterFunction if the newObject wouldn't show? + if (this.selectObjectsOnAddition) { + this.selectedContentIndexes = [this.content.length-1]; + } + if (this.clearFilterFunctionOnAddition) { + this.filterFunction = null; + } else { + + } + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /** + Adds one or more items to the array controller's content property. + @function + @example + var ArrayCollection = require("ui/controller/array-controller").ArrayCollection; + var ac = ArrayCollection.create(); + ac.addObjects( {label: "News"}, {label: "Sports"}, {label: "Weather"} ); + */ + addObjects: { + value: function() { + + var objects = Array.prototype.slice.call(arguments), + i, + objectCount = objects.length, + selectedContentIndexes, firstIndex; + + for (i = 0; i < objectCount; i++) { + this.content.push(objects[i]); + } + + if (this.selectObjectsOnAddition) { + selectedContentIndexes = []; + firstIndex = this.content.length-objectCount; + for (i = 0; i < objectCount; i++) { + selectedContentIndexes[i] = firstIndex++; + } + this.selectedContentIndexes = selectedContentIndexes; + this.selectedObjects = objects; + } + + if (this.clearFilterFunctionOnAddition) { + this.filterFunction = null; + } + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + }, + + /** + Removes the currently selected item or items in the collection. + @function + */ + remove: { + value: function() { + + if (this.selectedObjects && this.selectedObjects.length > 0) { + this.removeObjects.apply(this, this.selectedObjects); + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + } + // TODO what do we want to do otherwise? + } + }, + + /** + Removes the specified object or objects from the collection. + @function + */ + removeObjects: { + value: function() { + var objectsToRemove = Array.prototype.slice.call(arguments), + remainingObjects; + + // TODO what do we do if there are no arguments? (should this be where we solve the problem of calling remove with no selection?) + + remainingObjects = this.content.filter(function(value) { + return objectsToRemove.indexOf(value) < 0; + }); + + this.content = remainingObjects; + + //TODO abandon selection? preserve what we can of the selection? + + if (this.automaticallyOrganizeObjects) { + this.organizeObjects(); + } + + } + } + +}); -- cgit v1.2.3