From 13ae16997d4bbca14e255d5989d1c44a76eac72c Mon Sep 17 00:00:00 2001 From: Valerio Virgillito Date: Wed, 16 May 2012 15:23:48 -0700 Subject: montage v.0.10 integration Signed-off-by: Valerio Virgillito --- js/io/system/ninjalibrary.json | 2 +- node_modules/descriptor.json | 42 +- node_modules/montage/core/change-notification.js | 1418 ++++++++++++++++++++ .../montage/core/converter/date-converter.js | 4 +- node_modules/montage/core/core.js | 600 +-------- node_modules/montage/core/deserializer.js | 4 +- node_modules/montage/core/event/binding.js | 953 +------------ node_modules/montage/core/event/event-manager.js | 56 +- node_modules/montage/core/event/key-manager.js | 978 ++++++++++++++ node_modules/montage/core/exception.js | 2 +- node_modules/montage/core/extras/array.js | 292 ++++ node_modules/montage/core/extras/object.js | 256 ++++ node_modules/montage/core/extras/string.js | 79 ++ node_modules/montage/core/promise-queue.js | 2 +- node_modules/montage/core/selector.js | 9 + .../montage/core/selector/abstract-language.js | 303 +++++ .../montage/core/selector/abstract-selector.js | 189 +++ .../montage/core/selector/abstract-semantics.js | 100 ++ node_modules/montage/core/selector/language.js | 381 ++++++ node_modules/montage/core/selector/parser.js | 62 + .../montage/core/selector/property-language.js | 346 +++++ node_modules/montage/core/selector/semantics.js | 392 ++++++ node_modules/montage/core/serializer.js | 18 +- node_modules/montage/core/shim.js | 12 - node_modules/montage/core/shim/array.js | 51 +- node_modules/montage/core/shim/immediate.js | 57 - node_modules/montage/core/shim/object.js | 36 + node_modules/montage/core/shim/string.js | 81 -- node_modules/montage/package.json | 1 + node_modules/montage/require/browser.js | 2 +- node_modules/montage/require/require.js | 20 +- .../autocomplete/autocomplete.reel/autocomplete.js | 126 +- .../autocomplete/result-item.reel/result-item.js | 33 + .../results-list.reel/results-list.html | 5 +- .../autocomplete/results-list.reel/results-list.js | 7 +- node_modules/montage/ui/button.reel/button.js | 6 + .../component-placeholder.js | 146 -- node_modules/montage/ui/component.js | 163 ++- node_modules/montage/ui/composer/key-composer.js | 385 ++++++ .../montage/ui/composer/translate-composer.js | 230 ++-- .../montage/ui/condition.reel/condition.js | 79 +- .../montage/ui/controller/array-controller.js | 53 +- node_modules/montage/ui/flow-bezier-spline.js | 163 ++- node_modules/montage/ui/flow-path-cubic.js | 115 -- node_modules/montage/ui/flow-path-lerp.js | 112 -- node_modules/montage/ui/flow-path-linear.js | 83 -- node_modules/montage/ui/flow-path-sigmoid.js | 148 -- node_modules/montage/ui/flow-path.js | 215 --- node_modules/montage/ui/flow.reel/flow.html | 10 +- node_modules/montage/ui/flow.reel/flow.js | 758 +++++++---- node_modules/montage/ui/list.reel/list.html | 2 +- node_modules/montage/ui/loader.reel/loader.js | 15 +- .../ui/nearest-neighbor-component-search.js | 122 +- .../montage/ui/repetition.reel/repetition.js | 711 +++++++--- .../rich-text-editor.reel/rich-text-editor-base.js | 4 +- .../rich-text-editor.reel/rich-text-editor.js | 4 +- .../montage/ui/scroller.reel/scroller.html | 9 + node_modules/montage/ui/scroller.reel/scroller.js | 20 - .../ui/skeleton/range-input.reel/range-input.html | 18 +- .../ui/skeleton/range-input.reel/range-input.js | 159 ++- node_modules/montage/ui/slot.reel/slot.js | 262 +--- .../montage/ui/substitution.reel/substitution.js | 14 - node_modules/montage/ui/tabs.reel/tabs.html | 3 +- node_modules/montage/ui/template.js | 9 +- .../token-field/token-field.reel/token-field.css | 40 + .../token-field/token-field.reel/token-field.html | 87 ++ .../ui/token-field/token-field.reel/token-field.js | 221 +++ .../montage/ui/token-field/token.reel/token.css | 54 + .../montage/ui/token-field/token.reel/token.html | 45 + .../montage/ui/token-field/token.reel/token.js | 91 ++ 70 files changed, 7692 insertions(+), 3783 deletions(-) create mode 100644 node_modules/montage/core/change-notification.js create mode 100644 node_modules/montage/core/event/key-manager.js create mode 100755 node_modules/montage/core/extras/array.js create mode 100644 node_modules/montage/core/extras/object.js create mode 100755 node_modules/montage/core/extras/string.js create mode 100755 node_modules/montage/core/selector.js create mode 100644 node_modules/montage/core/selector/abstract-language.js create mode 100644 node_modules/montage/core/selector/abstract-selector.js create mode 100644 node_modules/montage/core/selector/abstract-semantics.js create mode 100644 node_modules/montage/core/selector/language.js create mode 100644 node_modules/montage/core/selector/parser.js create mode 100644 node_modules/montage/core/selector/property-language.js create mode 100644 node_modules/montage/core/selector/semantics.js delete mode 100755 node_modules/montage/core/shim.js mode change 100755 => 100644 node_modules/montage/core/shim/array.js delete mode 100644 node_modules/montage/core/shim/immediate.js create mode 100644 node_modules/montage/core/shim/object.js delete mode 100755 node_modules/montage/core/shim/string.js create mode 100644 node_modules/montage/ui/autocomplete/result-item.reel/result-item.js delete mode 100755 node_modules/montage/ui/component-placeholder.reel/component-placeholder.js create mode 100644 node_modules/montage/ui/composer/key-composer.js delete mode 100644 node_modules/montage/ui/flow-path-cubic.js delete mode 100644 node_modules/montage/ui/flow-path-lerp.js delete mode 100644 node_modules/montage/ui/flow-path-linear.js delete mode 100644 node_modules/montage/ui/flow-path-sigmoid.js delete mode 100644 node_modules/montage/ui/flow-path.js create mode 100644 node_modules/montage/ui/token-field/token-field.reel/token-field.css create mode 100644 node_modules/montage/ui/token-field/token-field.reel/token-field.html create mode 100644 node_modules/montage/ui/token-field/token-field.reel/token-field.js create mode 100644 node_modules/montage/ui/token-field/token.reel/token.css create mode 100644 node_modules/montage/ui/token-field/token.reel/token.html create mode 100644 node_modules/montage/ui/token-field/token.reel/token.js diff --git a/js/io/system/ninjalibrary.json b/js/io/system/ninjalibrary.json index feced079..041e7ed7 100644 --- a/js/io/system/ninjalibrary.json +++ b/js/io/system/ninjalibrary.json @@ -1,6 +1,6 @@ { "libraries": [ - {"name": "Montage", "path": "/node_modules/descriptor.json", "version": "0.8.0.0"}, + {"name": "Montage", "path": "/node_modules/descriptor.json", "version": "0.10.0.0"}, {"name": "RDGE", "path": "/assets/descriptor.json", "version": "0.5.6.0"} ] } \ No newline at end of file diff --git a/node_modules/descriptor.json b/node_modules/descriptor.json index a2e96aa3..5617234f 100644 --- a/node_modules/descriptor.json +++ b/node_modules/descriptor.json @@ -7,7 +7,7 @@ "children": [ {"name": "bin"}, {"name": "core", - "children": [{"name": "converter"}, {"name": "event"}, {"name": "geometry"}, {"name": "shim"}] + "children": [{"name": "converter"}, {"name": "event"}, {"name": "extras"},{"name": "geometry"}, {"name": "selector"},{"name": "shim"}] }, {"name": "data", "children": [{"name": "ldapaccess"}, {"name": "nosqlaccess"}, {"name": "restaccess"}, {"name": "sqlaccess"}] @@ -18,13 +18,12 @@ "children": [ {"name": "anchor.reel"}, {"name": "autocomplete", - "children": [{"name": "autocomplete.reel"}, {"name": "results-list.reel"}]}, + "children": [{"name": "autocomplete.reel"}, {"name": "result-item.reel"}, {"name": "results-list.reel"}]}, {"name": "bluemoon", "children": [{"name": "button-group.reel"}, {"name": "button.reel"}, {"name": "checkbox.reel"}, {"name": "progress.reel"}, {"name": "slider.reel"}, {"name": "textarea.reel"}, {"name": "textfield.reel"}, {"name": "toggle.reel"}]}, {"name": "button.reel"}, {"name": "checkbox.reel"}, {"name": "component-group.reel"}, - {"name": "component-placeholder.reel"}, {"name": "composer"}, {"name": "condition.reel"}, {"name": "controller"}, @@ -56,6 +55,8 @@ {"name": "textfield.reel"}, {"name": "toggle-button.reel"}, {"name": "toggle-switch.reel"}, + {"name": "token-field", + "children": [{"name": "token-field.reel"}, {"name": "token.reel"}]}, {"name": "video-player.reel", "children": [{"name": "images"}]} ] @@ -83,7 +84,8 @@ ], "files": [ "montage/core/bitfield.js", - "montage/core/converter/bytes-converter.js", + "montage/core/change-notification.js", + "montage/core/converter/bytes-converter.js", "montage/core/converter/converter.js", "montage/core/converter/currency-converter.js", "montage/core/converter/date-converter.js", @@ -98,7 +100,11 @@ "montage/core/event/action-event-listener.js", "montage/core/event/binding.js", "montage/core/event/event-manager.js", + "montage/core/event/key-manager.js", "montage/core/event/mutable-event.js", + "montage/core/extras/array.js", + "montage/core/extras/object.js", + "montage/core/extras/string.js", "montage/core/exception.js", "montage/core/gate.js", "montage/core/geometry/cubic-bezier.js", @@ -106,14 +112,22 @@ "montage/core/jshint.js", "montage/core/logger.js", "montage/core/next-tick.js", + "montage/core/promise-connection.js", + "montage/core/promise-queue.js", "montage/core/promise.js", + "montage/core/selector/abstract-language.js", + "montage/core/selector/abstract-selector.js", + "montage/core/selector/abstract-semantics.js", + "montage/core/selector/language.js", + "montage/core/selector/parser.js", + "montage/core/selector/property-language.js", + "montage/core/selector/semantics.js", + "montage/core/selector.js", "montage/core/serializer.js", "montage/core/shim/array.js", - "montage/core/shim/immediate.js", - "montage/core/shim/string.js", + "montage/core/shim/object.js", "montage/core/shim/structures.js", "montage/core/shim/weak-map.js", - "montage/core/shim.js", "montage/core/state-chart.js", "montage/core/undo-manager.js", "montage/core/url.js", @@ -158,6 +172,7 @@ "montage/ui/autocomplete/autocomplete.reel/autocomplete.html", "montage/ui/autocomplete/autocomplete.reel/autocomplete.js", "montage/ui/autocomplete/autocomplete.reel/loading.gif", + "montage/ui/autocomplete/result-item.reel/result-item.js", "montage/ui/autocomplete/results-list.reel/results-list.css", "montage/ui/autocomplete/results-list.reel/results-list.html", "montage/ui/autocomplete/results-list.reel/results-list.js", @@ -195,9 +210,9 @@ "montage/ui/check-input.js", "montage/ui/checkbox.reel/checkbox.js", "montage/ui/component-group.reel/component-group.js", - "montage/ui/component-placeholder.reel/component-placeholder.js", "montage/ui/component.js", "montage/ui/composer/composer.js", + "montage/ui/composer/key-composer.js", "montage/ui/composer/press-composer.js", "montage/ui/composer/swipe-composer.js", "montage/ui/composer/translate-composer.js", @@ -212,11 +227,6 @@ "montage/ui/dynamic-element.reel/dynamic-element.js", "montage/ui/editable-text.js", "montage/ui/flow-bezier-spline.js", - "montage/ui/flow-path-cubic.js", - "montage/ui/flow-path-lerp.js", - "montage/ui/flow-path-linear.js", - "montage/ui/flow-path-sigmoid.js", - "montage/ui/flow-path.js", "montage/ui/flow.reel/flow.html", "montage/ui/flow.reel/flow.js", "montage/ui/image.reel/image.js", @@ -277,6 +287,12 @@ "montage/ui/toggle-switch.reel/toggle-switch.css", "montage/ui/toggle-switch.reel/toggle-switch.html", "montage/ui/toggle-switch.reel/toggle-switch.js", + "montage/ui/token-field/token-field.reel/token-field.css", + "montage/ui/token-field/token-field.reel/token-field.html", + "montage/ui/token-field/token-field.reel/token-field.js", + "montage/ui/token-field/token.reel/token.css", + "montage/ui/token-field/token.reel/token.html", + "montage/ui/token-field/token.reel/token.js", "montage/ui/video-player.reel/images/fullscreen-enter.png", "montage/ui/video-player.reel/images/fullscreen-exit.png", "montage/ui/video-player.reel/images/pause.png", diff --git a/node_modules/montage/core/change-notification.js b/node_modules/montage/core/change-notification.js new file mode 100644 index 00000000..bfe5ff2d --- /dev/null +++ b/node_modules/montage/core/change-notification.js @@ -0,0 +1,1418 @@ +/* + 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/core/event/change-notification +*/ + +var Montage = require("montage").Montage, + logger = require("core/logger").logger("change-notification"), + UNDERSCORE = "_"; + +// key: , , +var _descriptorsDirectory = Object.create(null); + +// key: , +var _willChangeDescriptorsDirectory = Object.create(null); +var _willChangeDescriptorsIndexesDirectory = Object.create(null); +var _changeDescriptorsDirectory = Object.create(null); +var _changeDescriptorsIndexesDirectory = Object.create(null); + +exports.__reset__ = function() { + _descriptorsDirectory = Object.create(null); + _willChangeDescriptorsDirectory = Object.create(null); + _willChangeDescriptorsIndexesDirectory = Object.create(null); + _changeDescriptorsDirectory = Object.create(null); + _changeDescriptorsIndexesDirectory = Object.create(null); + // also need to remove all installed setters +}; + +exports.__debug__ = function() { + console.log("_descriptorsDirectory", _descriptorsDirectory); + console.log("_willChangeDescriptorsDirectory", _willChangeDescriptorsDirectory, _willChangeDescriptorsIndexesDirectory); + console.log("_changeDescriptorsDirectory", _changeDescriptorsDirectory, _changeDescriptorsIndexesDirectory); +}; + +var ChangeNotification = exports.ChangeNotification = Object.create(Montage, { + // (object) => .uuid + // + // (target_n): { + // : { + // target, + // propertyPath, + // willChangeListeners: { + // (listener_n): { + // listenerTarget, + // listenerFunction, + // listensToMutation + // } + // }, + // changeListeners: same as willChangeListeners, + // willChangeListenersCount: Object.keys(willChangeListeners).length, + // changeListenersCount: Object.keys(changeListeners).length, + // handleWillChange: function() + // handleChange: function() + // } + // } + _descriptorsRegistry: { + writable: true, + value: Object.create(null) + }, + + _createFunctionDescriptor: { + value: function(target, listener, beforeChange, mutation) { + var identifier, + functionName, + functionDescriptor = Object.create(ChangeNotificationFunctionDescriptor); + + if (typeof listener === "function") { + functionDescriptor.listenerFunction = listener; + functionDescriptor.listenerTarget = target; + } else { + identifier = target.identifier; + + if (identifier) { + identifier = identifier.toCapitalized(); + + functionName = "handle" + identifier + (beforeChange ? "WillChange" : "Change"); + if (typeof listener[functionName] === "function") { + functionDescriptor.listenerFunctionName = functionName; + functionDescriptor.listenerFunction = listener[functionName]; + functionDescriptor.listenerTarget = listener; + } + } + + if (!functionDescriptor.listenerFunction) { + functionName = "handle" + (beforeChange ? "WillChange" : "Change"); + if (typeof listener[functionName] === "function") { + functionDescriptor.listenerFunctionName = functionName; + functionDescriptor.listenerFunction = listener[functionName]; + functionDescriptor.listenerTarget = listener; + } + } + } + + if (!functionDescriptor.listenerFunction) { + console.log("Could not find valid listener when installing", target, listener); + throw "Could not find valid listener when installing"; + } + + functionDescriptor.listensToMutation = mutation; + return functionDescriptor; + } + }, + + registerPropertyChangeListener: { + value: function(target, path, listener, beforeChange, mutation) { + var targetKey = target.uuid, + registry = this._descriptorsRegistry, + targetEntry = registry[targetKey], + descriptor; + + if (path == null) { + path = "*"; + mutation = true; + } + + if (!targetEntry) { + targetEntry = registry[targetKey] = Object.create(null); + targetEntry.propertyPathCount = 0; + } + + descriptor = targetEntry[path]; + if (!descriptor) { + descriptor = targetEntry[path] = Object.create(ChangeNotificationDescriptor).initWithTargetPath(target, path); + targetEntry.propertyPathCount++; + } + descriptor.registerListener(listener, beforeChange, mutation); + + return descriptor; + } + }, + + unregisterPropertyChangeListener: { + value: function(target, path, listener, beforeChange) { + var targetKey = target.uuid, + registry = this._descriptorsRegistry, + targetEntry = registry[targetKey], + descriptor; + + if (path == null) { + path = "*"; + } + + if (targetEntry) { + descriptor = targetEntry[path]; + if (descriptor) { + // TODO: should this function return the number of listeners? + descriptor.unregisterListener(listener, beforeChange); + if (descriptor.willChangeListenersCount === 0 && + descriptor.changeListenersCount === 0) { + delete targetEntry[path]; + if (--targetEntry.propertyPathCount === 0) { + delete registry[targetKey]; + } + } + } + } + } + }, + + getPropertyChangeDescriptor: { + value: function(target, path) { + var targetEntry = this._descriptorsRegistry[target.uuid]; + + if (targetEntry) { + if (path == null) { + path = "*"; + } + return targetEntry[path]; + } + } + }, + + __debug__: { + value: function() { + console.log("_descriptorsRegistry: ", this._descriptorsRegistry); + } + }, + + __reset__: { + value: function() { + this._descriptorsRegistry = Object.create(null); + } + } +}); + +var ChangeNotificationDescriptor = Montage.create(Montage, { + target: {value: null}, + propertyPath: {value: null}, + willChangeListeners: {value: null}, + changeListeners: {value: null}, + willChangeListenersCount: {value: 0}, + changeListenersCount: {value: 0}, + isActive: {value: false}, + // list of all objects that this listener needed to start listening to. + // these are the objects in the target.getProperty(path). + // format [(target, propertyName, remainingPath)*] + dependencies: {value: null}, + hasWillChangeDependencies: {value: false}, + hasChangeDependencies: {value: false}, + // index of where this listener is expected to be in its dependent listeners + dependentDescriptorsIndex: {value: null}, + mutationDependencyIndex: {value: null}, + mutationListenersCount: {value: 0}, + + initWithTargetPath: { + value: function(target, path) { + this.target = target; + this.propertyPath = path; + + return this; + } + }, + registerListener: { + value: function(listener, beforeChange, mutation) { + var listenerKey = listener.uuid; + + if (beforeChange) { + listeners = this.willChangeListeners; + if (!listeners) { + listeners = this.willChangeListeners = Object.create(null); + } + if (!(listenerKey in listeners)) { + listeners[listenerKey] = ChangeNotification._createFunctionDescriptor(this.target, listener, beforeChange, mutation); + this.willChangeListenersCount++; + if (mutation) { + this.mutationListenersCount++; + } + } + } else { + listeners = this.changeListeners; + if (!listeners) { + listeners = this.changeListeners = Object.create(null); + } + if (!(listenerKey in listeners)) { + listeners[listenerKey] = ChangeNotification._createFunctionDescriptor(this.target, listener, beforeChange, mutation); + this.changeListenersCount++; + if (mutation) { + this.mutationListenersCount++; + } + } + } + } + }, + unregisterListener: { + value: function(listener, beforeChange) { + var listenerKey = listener.uuid; + + if (beforeChange) { + listeners = this.willChangeListeners; + if (listeners && listenerKey in listeners) { + if (listeners[listenerKey].listensToMutation) { + this.mutationListenersCount--; + } + delete listeners[listenerKey]; + this.willChangeListenersCount--; + } + } else { + listeners = this.changeListeners; + if (listeners && listenerKey in listeners) { + if (listeners[listenerKey].listensToMutation) { + this.mutationListenersCount--; + } + delete listeners[listenerKey]; + this.changeListenersCount--; + } + } + + if (this.willChangeListenersCount === 0 && + this.changeListenersCount === 0) { + // no need to listen to any dependencies now. + this.removeDependencies(); + } + } + }, + hasListeners: { + value: function() { + return this.willChangeListenersCount > 0 || + this.changeListenersCount > 0; + } + }, + + setupDependencies: { + value: function(target, path, beforeChange, mutation) { + var dependencies = this.dependencies; + + if (this.hasChangeDependencies) { + // if we're at this point it means that the only dependencies to install is + // beforeChange dependencies, give up if they're already installed. + if (this.hasWillChangeDependencies || !beforeChange) { + return; + } + // since the dependencies array is already setup, might as well use + // it instead of going through getProperty again. + for (var i = 0, l = dependencies.length; i < l; i+=3) { + dependencies[i].addPropertyChangeListener(dependencies[i+1], this, true, dependencies[i+2] != null); + } + } else { + this.addDependency(target, path, beforeChange, mutation); + } + + if (!this.hasChangeDependencies) { + // At this point change dependencies were definitely installed + // because we always need them to get the "plus" value. + if (beforeChange) { + this.hasWillChangeDependencies = true; + } + this.hasChangeDependencies = true; + } else { + // If change dependencies were already installed then the only + // option left is that will change dependencies were now installed. + this.hasWillChangeDependencies = true; + } + } + }, + + addDependency: { + value: function (target, path, beforeChange, mutation) { + var self = this, + ignoreMutation; + + target.getProperty(path, null, null, function (target, propertyName, result, index, remainingPath) { + + ignoreMutation = mutation ? remainingPath != null : true; + if (beforeChange) { + target.addPropertyChangeListener(propertyName, self, true, ignoreMutation); + } + // we always need to listen to the "afterChange" notification because + // we only have access to the plus object at that time. + // we need that object in order to install the new listeners + // on the remainingPath. + + target.addPropertyChangeListener(propertyName, self, false, ignoreMutation); + self.registerDependency(target, propertyName, remainingPath); + }); + } + }, + + removeDependencies: { + value: function() { + var dependencies = this.dependencies, + target, + propertyName, + descriptor; + + if (dependencies) { + for (var i = 0, l = dependencies.length; i < l; i+=3) { + target = dependencies[i]; + propertyName = dependencies[i+1]; + descriptor = ChangeNotification.getPropertyChangeDescriptor(target, propertyName); + + if (this.hasWillChangeDependencies) { + target.removePropertyChangeListener(propertyName, this, true); + } + if (this.hasChangeDependencies) { + target.removePropertyChangeListener(propertyName, this); + } + if (descriptor) { + delete descriptor.dependentDescriptorsIndex[this.uuid]; + } + } + dependencies.length = 0; + } + } + }, + updateDependenciesAtIndex: { + value: function(index, oldValue, newValue) { + var self = this, + dependencies = this.dependencies, + remainingPath = dependencies[index+2]; + + // remove listeners from the old value + if (oldValue != null) { + oldValue.getProperty(remainingPath, null, null, function(target, propertyName, result, index, remainingPath) { + + self.unregisterDependency(target, propertyName, remainingPath); + + if (self.hasWillChangeDependencies) { + target.removePropertyChangeListener(propertyName, self, true); + } + if (self.hasChangeDependencies) { + target.removePropertyChangeListener(propertyName, self); + } + }); + } + + // add listeners to the new value + if (newValue != null) { + newValue.getProperty(remainingPath, null, null, function(target, propertyName, result, index, remainingPath) { + if (self.hasWillChangeDependencies) { + target.addPropertyChangeListener(propertyName, self, true, remainingPath != null); + } + if (self.hasChangeDependencies) { + target.addPropertyChangeListener(propertyName, self, false, remainingPath != null); + } + self.registerDependency(target, propertyName, remainingPath); + }); + } + } + }, + updateDependencies: { + value: function(notification) { + var dependenciesIndex = notification._dependenciesIndex; + + if (dependenciesIndex != null) { + // This property change was triggered by a change in one of the + // dependencies, therefore we need to remove all the listeners + // from the old values and add listeners to the new ones. + if (notification.isMutation) { + // If this listener is being triggered by a mutation change then + // we need to go through the old values and remove the listeners + // and go through the new values and add listeners. + for (var i = 0, l = notification.minus.length; i < l; i++) { + this.updateDependenciesAtIndex(dependenciesIndex, notification.minus[i], null); + } + for (var i = 0, l = notification.plus.length; i < l; i++) { + this.updateDependenciesAtIndex(dependenciesIndex, null, notification.plus[i]); + } + } else { + this.updateDependenciesAtIndex(dependenciesIndex, notification.minus, notification.plus); + } + } else if (this.mutationListenersCount > 0 && !notification.isMutation) { + // We're listening to mutation events on the property so we need + // to remove the mutation listener on the old value and add it + // to the new one. + // However, we should restrict ourselves to notifications that + // actually change the value at a property path, mutation doesn't + // change the value at the property path, the value itself is + // still the same. + this.updateMutationDependency(notification.plus); + } + } + }, + updateMutationDependency: { + value: function(newTarget) { + var target, + installMutationDependency; + + if (this.mutationDependencyIndex != null) { + var target = this.dependencies[this.mutationDependencyIndex]; + } + + if (target === newTarget) { + return; + } + + installMutationDependency = this.mutationListenersCount > 0 && + newTarget != null && + typeof newTarget === "object"; + + if (target) { + this.unregisterDependency(target, null, null); + target.removePropertyChangeListener(null, this, true); + target.removePropertyChangeListener(null, this, false); + this.mutationDependencyIndex = null; + } + if (installMutationDependency) { + if (this.willChangeListenersCount > 0) { + newTarget.addPropertyChangeListener(null, this, true); + } + if (this.changeListenersCount > 0) { + newTarget.addPropertyChangeListener(null, this, false); + } + this.mutationDependencyIndex = this.registerDependency(newTarget, null, null); + } + } + }, + registerDependency: { + value: function(target, propertyName, remainingPath) { + var dependencyDescriptor = ChangeNotification.getPropertyChangeDescriptor(target, propertyName), + dependentDescriptorsIndex, + dependencies, + dependentKey, + ix; + + if (dependencyDescriptor) { + dependentDescriptorsIndex = dependencyDescriptor.dependentDescriptorsIndex; + dependencies = this.dependencies; + dependentKey = this.uuid; + + if (!dependencies) { + dependencies = this.dependencies = []; + } + // TODO: should use descriptor after all? + ix = dependencies.push(target, propertyName, remainingPath) - 3; + if (!dependentDescriptorsIndex) { + dependentDescriptorsIndex = dependencyDescriptor.dependentDescriptorsIndex = Object.create(null); + } + if (!(dependentKey in dependentDescriptorsIndex)) { + dependentDescriptorsIndex[dependentKey] = ix; + } + + return ix; + } + } + }, + unregisterDependency: { + value: function(target, propertyName, remainingPath) { + var dependencyDescriptor = ChangeNotification.getPropertyChangeDescriptor(target, propertyName), + dependencies = this.dependencies, + targetIx; + + if (dependencyDescriptor) { + do { + targetIx = dependencies.indexOf(target); + if (dependencies[targetIx+1] === propertyName && + dependencies[targetIx+2] === remainingPath) { + dependencies.splice(targetIx, 3); + break; + } else { + targetIx = dependencies.indexOf(target, targetIx+1); + } + } while (targetIx != -1); + if (targetIx == -1) { + throw "getProperty target (" + this.target.uuid + ":" + propertyName + ") not found in dependencies for " + this.propertyPath; + } + + delete dependencyDescriptor.dependentDescriptorsIndex[this.uuid]; + } + } + }, + handleWillChange: { + value: function(notification) { + notification.phase = "before"; + this.handleChange(notification, this.willChangeListeners); + } + }, + handleChange: { + value: function(notification, listeners) { + var listener, + dependentDescriptorsIndex = this.dependentDescriptorsIndex, + dependenciesIndex = notification._dependenciesIndex, + isMutationNotification; + + // TODO: maybe I should replicate this + if (arguments.length < 2) { + listeners = this.changeListeners; + notification.phase = "after"; + this.updateDependencies(notification); + } + + // TODO: I need to know the index of dependency, should this be in the notification? + if (listeners) { + notification._dependenciesIndex = null; + notification.currentTarget = this.target; + notification.currentPropertyPath = this.propertyPath; + isMutationNotification = notification.isMutation; + for (var key in listeners) { + listener = listeners[key]; + + // NOTE it's possible to have a changeListener not listen for mutations in cases + // where it will still need to react to them. This is the case when the propertyPath depends + // upon other propertyPaths. The installed changeListener will not necessarily listen for mutations + // but the other changeListeners installed to observe the dependent properties were listening to + // mutation, as is the original function. So call the listenerFunction. + if ((isMutationNotification && this.target._dependenciesForProperty && + this.target._dependenciesForProperty[this.propertyPath]) + || !isMutationNotification || listener.listensToMutation) { + + if (dependentDescriptorsIndex) { + notification._dependenciesIndex = dependentDescriptorsIndex[key]; + } + listener.listenerFunction.call(listener.listenerTarget, notification); + } + } + notification._dependenciesIndex = dependenciesIndex; + } + } + } +}); + +var ChangeNotificationFunctionDescriptor = Object.create(null, { + listenerTarget: {writable: true, value: null}, + listenerFunction: {writable: true, value: null}, + listenerFunctionName: {writable: true, value: null}, + listensToMutation: {writable: true, value: false} +}); + +var ObjectPropertyChangeDispatcherManager = Object.create(null, { + installDispatcherOnTargetProperty: { + value: function(target, propertyName) { + var prototypeAndDescriptor, + currentPropertyDescriptor, + currentSetter, + prototypeDefiningProperty; + + prototypeAndDescriptor = Object.getPrototypeAndDescriptorDefiningProperty(target, propertyName); + currentPropertyDescriptor = prototypeAndDescriptor.propertyDescriptor; + + if (currentPropertyDescriptor) { + currentSetter = currentPropertyDescriptor.set; + prototypeDefiningProperty = prototypeAndDescriptor.prototype; + + if ("value" in currentPropertyDescriptor) { + this.addDispatcherToTargetProperty(target, propertyName, currentPropertyDescriptor.enumerable); + } else if (currentSetter && !currentSetter.isDispatchingSetter) { + this.addDispatcherToTargetPropertyWithDescriptor(target, propertyName, currentPropertyDescriptor); + } + } else { + this.addDispatcherToTargetProperty(target, propertyName, true); + } + } + }, + + uninstallDispatcherOnTargetProperty: { + value: function(target, propertyName) { + + } + }, + + dispatcherPropertyNamePrefix: { + value: "_" + }, + + addDispatcherToTargetProperty: { + value: function(target, propertyName, enumerable) { + var prefixedPropertyName = this.dispatcherPropertyNamePrefix + propertyName; + + DispatcherPropertyDescriptor.enumerable = enumerable; + PrefixedPropertyDescriptor.value = target[propertyName]; + DispatcherPropertyDescriptor.get = function() { + return this[prefixedPropertyName]; + }; + DispatcherPropertyDescriptor.set = function changeNotificationSetter(value) { + var descriptor = ChangeNotification.getPropertyChangeDescriptor(target, propertyName), + previousValue, + notification; + + if (!descriptor) { + this[prefixedPropertyName] = value; + return; + } + + previousValue = this[prefixedPropertyName]; + if (previousValue === value) { + // Nothing to do here + return; + } + + if (descriptor.isActive && + target === descriptor.target && + propertyName === descriptor.propertyPath) { + //console.log("Cycle detected at ", target, " ", propertyName); + return; + } + + // TODO: recycle these notification objects + notification = Object.create(PropertyChangeNotification); + notification.target = this; + notification.propertyPath = propertyName; + notification.minus = previousValue; + + descriptor.isActive = true; + descriptor.handleWillChange(notification); + this[prefixedPropertyName] = value; + notification.plus = this[prefixedPropertyName]; + descriptor.handleChange(notification); + descriptor.isActive = false; + }; + DispatcherPropertyDescriptor.set.isDispatchingSetter = true; + + delete target[propertyName]; + Object.defineProperty(target, prefixedPropertyName, PrefixedPropertyDescriptor); + Object.defineProperty(target, propertyName, DispatcherPropertyDescriptor); + } + }, + + addDispatcherToTargetPropertyWithDescriptor: { + value: function(target, propertyName, propertyDescriptor) { + var originalSetter = propertyDescriptor.set; + + DispatcherPropertyDescriptor.enumerable = propertyDescriptor.enumerable; + PrefixedPropertyDescriptor.value = target[propertyName]; + DispatcherPropertyDescriptor.get = propertyDescriptor.get; + DispatcherPropertyDescriptor.set = function changeNotificationSetter(value) { + var descriptor = ChangeNotification.getPropertyChangeDescriptor(target, propertyName), + previousValue, + notification; + + if (!descriptor) { + return; + } + + previousValue = this[propertyName]; + if (previousValue === value) { + // Nothing to do here + return; + } + + if (descriptor.isActive && + target === descriptor.target && + propertyName === descriptor.propertyPath && + changeNotificationSetter.caller !== originalSetter) { + //console.log("Cycle detected at ", target, " ", propertyName); + return; + } + + // TODO: recycle these notification objects + notification = Object.create(PropertyChangeNotification); + notification.target = this; + notification.propertyPath = propertyName; + notification.minus = previousValue; + notification.plus = value; + + descriptor.isActive = true; + descriptor.handleWillChange(notification); + originalSetter.apply(this, arguments); + notification.plus = this[propertyName]; + // this is a setter so we have no idea what it does to the value given + // that's why we need to retrieve the value again + descriptor.handleChange(notification); + descriptor.isActive = false; + }; + DispatcherPropertyDescriptor.set.isDispatchingSetter = true; + DispatcherPropertyDescriptor.set.originalSetter = originalSetter; + Object.defineProperty(target, propertyName, DispatcherPropertyDescriptor); + } + }, + + removeDispatcherOnTargetProperty: { + value: function(target, propertyName) { + + } + } +}); + +Object.defineProperty(Object.prototype, "dispatchPropertyChange", { + value: function(/*affected paths,..., callback*/) { + + var argumentCount = arguments.length, + callbackArgumentIndex = argumentCount - 1, + callback, + i, + iProperty, + descriptor, + observedProperties, + observedPropertyCount, + notification; + + if (argumentCount < 2) { + throw "Affected property (or properties) and callback to effect change are required to dispatchPropertyChange"; + } + + callback = arguments[callbackArgumentIndex]; + + if (!(callback && typeof callback == "function")) { + throw "Callback to effect actual change is required to dispatchPropertyChange"; + } + + // Storing observedProperties as [propertyA, descriptorA, notificationA, propertyB, descriptorB, notificationB...] + observedProperties = []; + + for (i = 0; i < callbackArgumentIndex; i++) { + iProperty = arguments[i]; + descriptor = ChangeNotification.getPropertyChangeDescriptor(this, iProperty); + if (descriptor) { + notification = Object.create(PropertyChangeNotification); + observedProperties.push(iProperty, descriptor, notification); + + notification.target = this; + notification.minus = this.getProperty(iProperty); + descriptor.handleWillChange(notification); + } + } + + callback.call(this); + + for (i = 0, observedPropertyCount = observedProperties.length; i < observedPropertyCount; i+=3) { + iProperty = observedProperties[i]; + descriptor = observedProperties[i+1]; + notification = observedProperties[i+2]; + + notification.plus = this.getProperty(iProperty); + descriptor.handleChange(notification); + } + + } +}); + +Object.defineProperty(Object.prototype, "addPropertyChangeListener", { + value: function(path, listener, beforeChange, ignoreMutation) { + var descriptor, + dependentPropertyPaths, + i, + iPath; + + // If the uuid isn't consistent, the target isn't observable without leaking memory + // as we'll never be able to unregister it + if (!listener || !path || this.uuid !== this.uuid) { + return; + } + + descriptor = ChangeNotification.registerPropertyChangeListener(this, path, listener, beforeChange, !ignoreMutation); + // if it's a multiple property path then setup the dependencies, otherwise + // install a dispatcher on the property unless the target explicitly + // asks not to with automaticallyDispatchPropertyChangeListener. + if (path.indexOf(".") !== -1) { + descriptor.setupDependencies(this, path, beforeChange, !ignoreMutation); + } else { + if (typeof this.automaticallyDispatchPropertyChangeListener !== "function" || + this.automaticallyDispatchPropertyChangeListener(path)) { + ObjectPropertyChangeDispatcherManager.installDispatcherOnTargetProperty(this, path); + // give an opportunity for the actual value of the path to have something + // to say when it comes to property change listeners, this is useful, + // for instance, for arrays, that can start listen on mutation. + if (!ignoreMutation && descriptor.mutationListenersCount == 1) { + descriptor.updateMutationDependency(this[path]); + } + } + + // Observe any paths this property is dependent upon, as found in the dependencies attribute of + // this property's descriptor + dependentPropertyPaths = this._dependenciesForProperty ? this._dependenciesForProperty[path] : null; + + // TODO should adding a dispatcher on a dependent property also be subjected to checking for + // automaticDispatchPropertyChangeListener, probably + if (dependentPropertyPaths) { + for (i = 0; (iPath = dependentPropertyPaths[i]); i++) { + this.addPropertyChangeListener(iPath, descriptor, beforeChange, false); + descriptor.registerDependency(this, iPath, null); + } + } + } + } +}); +Object.defineProperty(Object.prototype, "removePropertyChangeListener", { + value: function removePropertyChangeListener(path, listener, beforeChange) { + var descriptor = ChangeNotification.getPropertyChangeDescriptor(this, path); + + if (!descriptor) { + return; + } + + ChangeNotification.unregisterPropertyChangeListener(this, path, listener, beforeChange); + descriptor.updateMutationDependency(); + } +}); + +var _unobservable_string_property_regexp = /^length$/; +Object.defineProperty(String.prototype, "addPropertyChangeListener", { + value: function(path, listener, beforeChange, ignoreMutation) { + + if (path != null && _unobservable_string_property_regexp.test(path)) { + return; + } + + Object.prototype.addPropertyChangeListener.call(this, path, listener, beforeChange, ignoreMutation); + } +}); + +var DispatcherPropertyDescriptor = { + configurable: true +}; + +var PrefixedPropertyDescriptor = { + enumerable: false, + writable: true, + configurable: true +}; + +var PropertyChangeNotification = exports.PropertyChangeNotification = Object.create(null, { + phase: {writable: true, value: null}, + target: {writable: true, value: null}, + propertyPath: {writable: true, value: null}, + minus: {writable: true, value: null}, + plus: {writable: true, value: null}, + currentTarget: {writable: true, value: null}, + currentPropertyPath: {writable: true, value: null}, + isMutation: {writable: true, value: false} +}); + +var ChangeNotificationDispatchingArray = exports.ChangeNotificationDispatchingArray = []; +var _index_array_regexp = /^[0-9]+$/; +var _unobservable_array_property_regexp = /^length$/; +Object.defineProperty(Array.prototype, "addPropertyChangeListener", { + value: function(path, listener, beforeChange, ignoreMutation) { + var listenChange, listenIndexChange, listenFunctionChange, + descriptor; + + if (!listener) { + return; + } + + if (path == null || path.indexOf(".") == -1) { + + if (_unobservable_array_property_regexp.test(path)) { + return; + } + + listenFunctionChange = path ? /\(.*\)/.test(path) : false; //TODO extract this regex + listenChange = (path == null); + listenIndexChange = _index_array_regexp.test(path); + } + + if (listenChange || listenIndexChange || listenFunctionChange) { + if (!this.isDispatchingArray) { + this.__proto__ = ChangeNotificationDispatchingArray; + } + descriptor = ChangeNotification.registerPropertyChangeListener(this, (listenFunctionChange ? null : path), listener, beforeChange, !ignoreMutation); + + // give an opportunity for the actual value of the path to have something + // to say when it comes to property change listeners, this is useful, + // for instance, for arrays, that can start listen on mutation. + if (listenIndexChange && !ignoreMutation && descriptor.mutationListenersCount == 1) { + descriptor.updateMutationDependency(this[path]); + } + } else { + Object.prototype.addPropertyChangeListener.apply(this, arguments); + } + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_dispatchArrayChangeNotification", { + enumerable: false, + configurable: false, + value: function(methodName, methodArguments, index, howManyToRemove, newValues) { + var descriptor = ChangeNotification.getPropertyChangeDescriptor(this, null), + notification, + indexNotification = Object.create(PropertyChangeNotification), + delta, + currentLength = this.length, + howManyToAdd = newValues.length, + maxLength, + oldValues = this.slice(index, index+howManyToRemove); + + indexNotification.target = this; + + // can't remove more than the available elements. + if (index + howManyToRemove > currentLength) { + howManyToRemove = currentLength - index; + } + delta = howManyToAdd - howManyToRemove; + maxLength = currentLength + (delta > 0 ? delta : 0); + + if (descriptor) { + notification = Object.create(PropertyChangeNotification); + notification.target = this; + notification.minus = oldValues; + notification.plus = newValues; + notification.index = index; + notification.isMutation = true; + // dispatch mutation notification + descriptor.handleWillChange(notification); + } + this._dispatchArrayBulkWillChangeNotification(indexNotification, index, newValues, delta, maxLength); + + result = this[methodName].apply(this, methodArguments); + + if (descriptor) { + notification.plus = newValues; + // dispatch mutation notification + descriptor.handleChange(notification); + } + this._dispatchArrayBulkChangeNotification(indexNotification, index, oldValues, delta, maxLength); + + return result; + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_dispatchArrayBulkWillChangeNotification", { + enumerable: false, + configurable: false, + value: function(notification, index, plus, delta, maxLength) { + var descriptor, + oldValue, + newValue; + + for (var i = 0, l = plus.length; i < l; i++, index++) { + descriptor = ChangeNotification.getPropertyChangeDescriptor(this, index); + if (descriptor) { + oldValue = this[index]; + newValue = plus[i]; + if (oldValue !== newValue) { + notification.index = index; + notification.propertyPath = String(index); + notification.minus = oldValue; + descriptor.handleWillChange(notification); + } + } + } + + if (delta != 0) { + for (; index < maxLength; index++) { + descriptor = ChangeNotification.getPropertyChangeDescriptor(this, index); + if (descriptor) { + oldValue = this[index]; + newValue = this[index-delta]; + if (oldValue !== newValue) { + notification.index = index; + notification.propertyPath = String(index); + notification.minus = oldValue; + descriptor.handleWillChange(notification); + } + } + } + } + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_dispatchArrayBulkChangeNotification", { + enumerable: false, + configurable: false, + value: function(notification, index, minus, delta, maxLength) { + var descriptor, + oldValue, + newValue; + + for (var i = 0, l = minus.length; i < l; i++, index++) { + descriptor = ChangeNotification.getPropertyChangeDescriptor(this, index); + if (descriptor) { + oldValue = minus[i]; + newValue = this[index]; + if (oldValue !== newValue) { + notification.index = index; + notification.propertyPath = String(index); + notification.minus = oldValue; + notification.plus = newValue; + descriptor.handleChange(notification); + } + } + } + + if (delta != 0) { + for (; index < maxLength; index++) { + descriptor = ChangeNotification.getPropertyChangeDescriptor(this, index); + if (descriptor) { + oldValue = this[index+delta]; + newValue = this[index]; + if (oldValue !== newValue) { + notification.index = index; + notification.propertyPath = String(index); + notification.minus = oldValue; + notification.plus = newValue; + descriptor.handleChange(notification); + } + } + } + } + } +}); + +Object.defineProperty(Array.prototype, "_setProperty", { + enumerable: false, + configurable: true, + value: function(index, value) { + return this[index] = value; + } +}); +Object.defineProperty(Array.prototype, "setProperty", { + enumerable: false, + configurable: true, + value: function(path, value) { + if (String(path).indexOf(".") == -1) { + if (this.__proto__ === ChangeNotificationDispatchingArray && !isNaN(path)) { + return this._dispatchArrayChangeNotification("_setProperty", arguments, Number(path), 1, Array.prototype.slice.call(arguments, 1, 2)); + } else { + return this[path] = value; + } + } else { + return Object.prototype.setProperty.apply(this, arguments); + } + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "isDispatchingArray", { + enumerable: false, + configurable: false, + value: true +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_splice", { + enumerable: false, + configurable: true, + value: Array.prototype.splice +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "splice", { + enumerable: false, + configurable: true, + value: function(index, howMany/*[, element1[, ...[, elementN]]]*/) { + return this._dispatchArrayChangeNotification("_splice", arguments, index, howMany, Array.prototype.slice.call(arguments, 2)); + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_shift", { + enumerable: false, + configurable: true, + value: Array.prototype.shift +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "shift", { + enumerable: false, + configurable: true, + value: function() { + return this._dispatchArrayChangeNotification("_shift", arguments, 0, 1, []); + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_unshift", { + enumerable: false, + configurable: true, + value: Array.prototype.unshift +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "unshift", { + enumerable: false, + configurable: true, + value: function() { + return this._dispatchArrayChangeNotification("_unshift", arguments, 0, 0, Array.prototype.slice.call(arguments, 0)); + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_push", { + enumerable: false, + configurable: true, + value: Array.prototype.push +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "push", { + enumerable: false, + configurable: true, + value: function() { + return this._dispatchArrayChangeNotification("_push", arguments, this.length, 0, Array.prototype.slice.call(arguments, 0)); + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_pop", { + enumerable: false, + configurable: true, + value: Array.prototype.pop +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "pop", { + enumerable: false, + configurable: true, + value: function() { + if (this.length > 0) { + return this._dispatchArrayChangeNotification("_pop", arguments, this.length-1, 1, []); + } + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_reverse", { + enumerable: false, + configurable: true, + value: Array.prototype.reverse +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "reverse", { + enumerable: false, + configurable: true, + value: function() { + var length = this.length; + if (length === 0) { + return; + } + + var descriptor = ChangeNotification.getPropertyChangeDescriptor(this, null), + indexDescriptor, + notification, + indexNotification = Object.create(PropertyChangeNotification), + oldValue, + newValue; + + indexNotification.target = this; + + if (descriptor) { + notification = Object.create(PropertyChangeNotification); + notification.target = this; + notification.isMutation = true; + descriptor.handleWillChange(notification); + } + + for (var i = 0; i < length; i++) { + indexDescriptor = ChangeNotification.getPropertyChangeDescriptor(this, i); + if (indexDescriptor) { + oldValue = this[i]; + if (oldValue !== this[length-i-1]) { + indexNotification.index = i; + indexNotification.propertyPath = String(i); + indexNotification.minus = oldValue; + indexDescriptor.handleWillChange(indexNotification); + } + } + } + + this._reverse(); + + if (descriptor) { + // nothing was really added or removed... + notification.minus = notification.plus = []; + descriptor.handleChange(notification); + } + + for (var i = 0; i < length; i++) { + indexDescriptor = ChangeNotification.getPropertyChangeDescriptor(this, i); + if (indexDescriptor) { + oldValue = this[length-i-1]; + newValue = this[i]; + if (oldValue !== newValue) { + indexNotification.index = i; + indexNotification.propertyPath = String(i); + indexNotification.minus = oldValue; + indexNotification.plus = newValue; + indexDescriptor.handleChange(indexNotification); + } + } + } + } +}); + +Object.defineProperty(ChangeNotificationDispatchingArray, "_sortIndexArray", { + enumerable: false, + configurable: true, + value: [] +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "_sortDefaultCompareFunction", { + enumerable: false, + configurable: true, + value: function(a, b) { + return String(a).localeCompare(String(b)); + } +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "_sort", { + enumerable: false, + configurable: true, + value: Array.prototype.sort +}); +Object.defineProperty(ChangeNotificationDispatchingArray, "sort", { + enumerable: false, + configurable: true, + value: function(compareFunction) { + var self, + length = this.length, + descriptor, + indexDescriptor, + notification, + indexNotification, + oldValue, + newValue, + _sortIndexArray, + _sortIndexArrayLength; + + if (length === 0) { + return; + } + + if (!compareFunction) { + compareFunction = this._sortDefaultCompareFunction; + } + + self = this; + _sortIndexArray = ChangeNotificationDispatchingArray._sortIndexArray; + _sortIndexArrayLength = _sortIndexArray.length; + + if (_sortIndexArrayLength < length) { + _sortIndexArray[length] = length-1; + for (var i = _sortIndexArrayLength; i < length; i++) { + _sortIndexArray[i] = i; + } + } + // http://jsperf.com/array-of-indexes/4 + indexArray = _sortIndexArray.slice(0, length); + // sort the indexes only + this._sort.call(indexArray, function(e1, e2) { + ret