/* 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}, observedDependentProperties: {value: null}, initWithTargetPath: { value: function(target, path) { this.target = target; this.propertyPath = path; return this; } }, registerListener: { value: function(listener, beforeChange, mutation) { var listenerKey = listener.uuid, listeners; 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, listeners; 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, uuid = this.uuid; // we need to stop circular property dependencies. // e.g.: object.foo depends on object.bar and object.bar depends on object.foo. if (notification[uuid]) { return; } // 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]; } notification[uuid] = true; listener.listenerFunction.call(listener.listenerTarget, notification); notification[uuid] = false; } } 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) { originalSetter.apply(this, arguments); return; } previousValue = this[propertyName]; if (previousValue === value) { originalSetter.apply(this, arguments); // Nothing more 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 && !descriptor.isActive) { notification = Object.create(PropertyChangeNotification); observedProperties.push(iProperty, descriptor, notification); notification.target = this; notification.minus = this.getProperty(iProperty); descriptor.isActive = true; 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); descriptor.isActive = false; } } }); 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) { if (!descriptor.observedDependentProperties) { descriptor.observedDependentProperties = {}; } for (i = 0; (iPath = dependentPropertyPaths[i]); i++) { if (!descriptor.observedDependentProperties[iPath]) { descriptor.observedDependentProperties[iPath] = true; 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 = { phase: null, target: null, propertyPath: null, minus: null, plus: null, currentTarget: null, currentPropertyPath: null, isMutation: 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, dotIndex; if (!listener) { return; } if (path == null || (dotIndex = 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); // We need to do this because the Object.prototype.addPropertyChangeListener doesn't create dependencies // for no-dot paths, but in array array.path will have dependencies when path is not an index or null. if (dotIndex == -1) { descriptor = ChangeNotification.getPropertyChangeDescriptor(this, path); descriptor.setupDependencies(this, path, beforeChange, !ignoreMutation); } } } }); Object.defineProperty(ChangeNotificationDispatchingArray, "_dispatchArrayChangeNotification", { enumerable: false, configurable: false, value: function(methodName, methodArguments, index, howManyToRemove, newValues) { var descriptor = ChangeNotification.getPropertyChangeDescriptor(this, null), result, 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, indexArray, _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) { return compareFunction(self[e1], self[e2]); }); descriptor = ChangeNotification.getPropertyChangeDescriptor(this, null); indexNotification = Object.create(PropertyChangeNotification); 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++) { indexArray[i] = this[indexArray[i]]; indexDescriptor = ChangeNotification.getPropertyChangeDescriptor(this, i); if (indexDescriptor) { oldValue = this[i]; if (oldValue !== indexArray[i]) { indexNotification.index = i; indexNotification.propertyPath = String(i); indexNotification.minus = oldValue; indexDescriptor.handleWillChange(indexNotification); } } } for (var i = 0; i < length; i++) { indexDescriptor = ChangeNotification.getPropertyChangeDescriptor(this, i); if (indexDescriptor) { oldValue = this[i]; newValue = indexArray[i]; this[i] = indexArray[i]; if (oldValue !== newValue) { indexNotification.index = i; indexNotification.propertyPath = String(i); indexNotification.minus = oldValue; indexNotification.plus = newValue; indexDescriptor.handleChange(indexNotification); } } else { this[i] = indexArray[i]; } } if (descriptor) { // nothing was really added or removed... notification.minus = notification.plus = []; descriptor.handleChange(notification); } } }); Object.defineProperty(ChangeNotificationDispatchingArray, "_wipe", { enumerable: false, configurable: true, value: Array.prototype.wipe }); /** Removes all members of this array making the object suitable for reuse @function module:montage/core/core.Array.wipe */ Object.defineProperty(ChangeNotificationDispatchingArray, "wipe", { enumerable: false, configurable: true, value: function() { this._dispatchArrayChangeNotification("_wipe", arguments, this.length, 0, Array.prototype.slice.call(arguments, 0)); return this; } }); if (typeof define === "function") { // ugly code is ugly Object.defineProperty(Object.prototype, "__debugChangeNotifications__", { enumerable: false, configurable: false, value: function() { var registry = ChangeNotification._descriptorsRegistry[this.uuid], path, log = []; if (registry) { for (path in registry) { log.push('"'+path+'"', registry[path]); var dependencies = registry[path].dependencies; if (dependencies) { log.push("\n\tlistens to "); for (var i = 0; i < dependencies.length; i += 3) { if (dependencies[i+1] == null) { log.push("mutation @", dependencies[i]); } else { log.push("\"" + dependencies[i+1] + "\" @", dependencies[i]); } log.push("\n\t "); } log.pop(); } var changeListeners = registry[path].changeListeners; var bindings = []; for (var key in changeListeners) { var listenerTarget = changeListeners[key].listenerTarget; var listenerFunctionName = changeListeners[key].listenerFunctionName; var info = Montage.getInfoForObject(listenerTarget); if (info.objectName === "PropertyChangeBindingListener") { if (listenerTarget.bindingOrigin === this && listenerTarget.bindingPropertyPath === path) { bindings.push("\"" + listenerTarget.targetPropertyPath + "\" @ " + (Montage.getInfoForObject(listenerTarget.target).objectName || "") + "(", listenerTarget.target, ")"); } else { bindings.push("\"" + listenerTarget.bindingPropertyPath + "\" @ " + (Montage.getInfoForObject(listenerTarget.bindingOrigin).objectName || "") + "(", listenerTarget.bindingOrigin, ")"); } bindings.push("\n\t "); } } var listeners = []; (function gatherListeners(descriptor, withBindings) { var changeListeners = descriptor.changeListeners; for (var key in changeListeners) { var listenerTarget = changeListeners[key].listenerTarget, listenerFunctionName = changeListeners[key].listenerFunctionName, info = Montage.getInfoForObject(listenerTarget); if (info.objectName !== "PropertyChangeBindingListener") { if (descriptor.dependentDescriptorsIndex && key in descriptor.dependentDescriptorsIndex) { listeners.push('"'+listenerTarget.propertyPath + "\" (", listenerTarget ,")", "-> "); } else { listeners.push(listenerFunctionName ? listenerFunctionName : "", "@ " + info.objectName + " (", listenerTarget, ")"); } gatherListeners(listenerTarget, true); listeners.push("\n\t "); } else if (withBindings) { listeners.push("\"" + listenerTarget.bindingPropertyPath + "\" @ " + Montage.getInfoForObject(listenerTarget.bindingOrigin).objectName + "(", listenerTarget.bindingOrigin, ")"); } } })(registry[path]); if (listeners.length > 0) { listeners.pop(); log.push("\n\tis listened by "); log.push.apply(log, listeners); } if (bindings.length > 0) { bindings.pop(); log.push("\n\tis bound to "); log.push.apply(log, bindings); } log.push("\n\n"); } console.log.apply(console, log); } else { console.log("No change listeners installed."); } } }); }