123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- // Copyright 2006 The Closure Library Authors. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS-IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- /**
- * @fileoverview
- * Central class for registering and accessing data sources
- * Also handles processing of data events.
- *
- * There is a shared global instance that most client code should access via
- * goog.ds.DataManager.getInstance(). However you can also create your own
- * DataManager using new
- *
- * Implements DataNode to provide the top element in a data registry
- * Prepends '$' to top level data names in path to denote they are root object
- *
- */
- goog.provide('goog.ds.DataManager');
- goog.require('goog.ds.BasicNodeList');
- goog.require('goog.ds.DataNode');
- goog.require('goog.ds.Expr');
- goog.require('goog.object');
- goog.require('goog.string');
- goog.require('goog.structs');
- goog.require('goog.structs.Map');
- /**
- * Create a DataManger
- * @extends {goog.ds.DataNode}
- * @constructor
- * @final
- */
- goog.ds.DataManager = function() {
- this.dataSources_ = new goog.ds.BasicNodeList();
- this.autoloads_ = new goog.structs.Map();
- this.listenerMap_ = {};
- this.listenersByFunction_ = {};
- this.aliases_ = {};
- this.eventCount_ = 0;
- this.indexedListenersByFunction_ = {};
- };
- /**
- * Global instance
- * @private
- */
- goog.ds.DataManager.instance_ = null;
- goog.inherits(goog.ds.DataManager, goog.ds.DataNode);
- /**
- * Get the global instance
- * @return {!goog.ds.DataManager} The data manager singleton.
- */
- goog.ds.DataManager.getInstance = function() {
- if (!goog.ds.DataManager.instance_) {
- goog.ds.DataManager.instance_ = new goog.ds.DataManager();
- }
- return goog.ds.DataManager.instance_;
- };
- /**
- * Clears the global instance (for unit tests to reset state).
- */
- goog.ds.DataManager.clearInstance = function() {
- goog.ds.DataManager.instance_ = null;
- };
- /**
- * Add a data source
- * @param {goog.ds.DataNode} ds The data source.
- * @param {boolean=} opt_autoload Whether to automatically load the data,
- * defaults to false.
- * @param {string=} opt_name Optional name, can also get name
- * from the datasource.
- */
- goog.ds.DataManager.prototype.addDataSource = function(
- ds, opt_autoload, opt_name) {
- var autoload = !!opt_autoload;
- var name = opt_name || ds.getDataName();
- if (!goog.string.startsWith(name, '$')) {
- name = '$' + name;
- }
- ds.setDataName(name);
- this.dataSources_.add(ds);
- this.autoloads_.set(name, autoload);
- };
- /**
- * Create an alias for a data path, very similar to assigning a variable.
- * For example, you can set $CurrentContact -> $Request/Contacts[5], and all
- * references to $CurrentContact will be procesed on $Request/Contacts[5].
- *
- * Aliases will hide datasources of the same name.
- *
- * @param {string} name Alias name, must be a top level path ($Foo).
- * @param {string} dataPath Data path being aliased.
- */
- goog.ds.DataManager.prototype.aliasDataSource = function(name, dataPath) {
- if (!this.aliasListener_) {
- this.aliasListener_ = goog.bind(this.listenForAlias_, this);
- }
- if (this.aliases_[name]) {
- var oldPath = this.aliases_[name].getSource();
- this.removeListeners(this.aliasListener_, oldPath + '/...', name);
- }
- this.aliases_[name] = goog.ds.Expr.create(dataPath);
- this.addListener(this.aliasListener_, dataPath + '/...', name);
- this.fireDataChange(name);
- };
- /**
- * Listener function for matches of paths that have been aliased.
- * Fires a data change on the alias as well.
- *
- * @param {string} dataPath Path of data event fired.
- * @param {string} name Name of the alias.
- * @private
- */
- goog.ds.DataManager.prototype.listenForAlias_ = function(dataPath, name) {
- var aliasedExpr = this.aliases_[name];
- if (aliasedExpr) {
- // If it's a subpath, appends the subpath to the alias name
- // otherwise just fires on the top level alias
- var aliasedPath = aliasedExpr.getSource();
- if (dataPath.indexOf(aliasedPath) == 0) {
- this.fireDataChange(name + dataPath.substring(aliasedPath.length));
- } else {
- this.fireDataChange(name);
- }
- }
- };
- /**
- * Gets a named child node of the current node.
- *
- * @param {string} name The node name.
- * @return {goog.ds.DataNode} The child node,
- * or null if no node of this name exists.
- */
- goog.ds.DataManager.prototype.getDataSource = function(name) {
- if (this.aliases_[name]) {
- return this.aliases_[name].getNode();
- } else {
- return this.dataSources_.get(name);
- }
- };
- /**
- * Get the value of the node
- * @return {!Object} The value of the node.
- * @override
- */
- goog.ds.DataManager.prototype.get = function() {
- return this.dataSources_;
- };
- /** @override */
- goog.ds.DataManager.prototype.set = function(value) {
- throw Error('Can\'t set on DataManager');
- };
- /** @override */
- goog.ds.DataManager.prototype.getChildNodes = function(opt_selector) {
- if (opt_selector) {
- return new goog.ds.BasicNodeList(
- [this.getChildNode(/** @type {string} */ (opt_selector))]);
- } else {
- return this.dataSources_;
- }
- };
- /**
- * Gets a named child node of the current node
- * @param {string} name The node name.
- * @return {goog.ds.DataNode} The child node,
- * or null if no node of this name exists.
- * @override
- */
- goog.ds.DataManager.prototype.getChildNode = function(name) {
- return this.getDataSource(name);
- };
- /** @override */
- goog.ds.DataManager.prototype.getChildNodeValue = function(name) {
- var ds = this.getDataSource(name);
- return ds ? ds.get() : null;
- };
- /**
- * Get the name of the node relative to the parent node
- * @return {string} The name of the node.
- * @override
- */
- goog.ds.DataManager.prototype.getDataName = function() {
- return '';
- };
- /**
- * Gets the a qualified data path to this node
- * @return {string} The data path.
- * @override
- */
- goog.ds.DataManager.prototype.getDataPath = function() {
- return '';
- };
- /**
- * Load or reload the backing data for this node
- * only loads datasources flagged with autoload
- * @override
- */
- goog.ds.DataManager.prototype.load = function() {
- var len = this.dataSources_.getCount();
- for (var i = 0; i < len; i++) {
- var ds = this.dataSources_.getByIndex(i);
- var autoload = this.autoloads_.get(ds.getDataName());
- if (autoload) {
- ds.load();
- }
- }
- };
- /**
- * Gets the state of the backing data for this node
- * @return {goog.ds.LoadState} The state.
- * @override
- */
- goog.ds.DataManager.prototype.getLoadState = goog.abstractMethod;
- /**
- * Whether the value of this node is a homogeneous list of data
- * @return {boolean} True if a list.
- * @override
- */
- goog.ds.DataManager.prototype.isList = function() {
- return false;
- };
- /**
- * Get the total count of events fired (mostly for debugging)
- * @return {number} Count of events.
- */
- goog.ds.DataManager.prototype.getEventCount = function() {
- return this.eventCount_;
- };
- /**
- * Adds a listener
- * Listeners should fire when any data with path that has dataPath as substring
- * is changed.
- * TODO(user) Look into better listener handling
- *
- * @param {Function} fn Callback function, signature function(dataPath, id).
- * @param {string} dataPath Fully qualified data path.
- * @param {string=} opt_id A value passed back to the listener when the dataPath
- * is matched.
- */
- goog.ds.DataManager.prototype.addListener = function(fn, dataPath, opt_id) {
- // maxAncestor sets how distant an ancestor you can be of the fired event
- // and still fire (you always fire if you are a descendant).
- // 0 means you don't fire if you are an ancestor
- // 1 means you only fire if you are parent
- // 1000 means you will fire if you are ancestor (effectively infinite)
- var maxAncestors = 0;
- if (goog.string.endsWith(dataPath, '/...')) {
- maxAncestors = 1000;
- dataPath = dataPath.substring(0, dataPath.length - 4);
- } else if (goog.string.endsWith(dataPath, '/*')) {
- maxAncestors = 1;
- dataPath = dataPath.substring(0, dataPath.length - 2);
- }
- opt_id = opt_id || '';
- var key = dataPath + ':' + opt_id + ':' + goog.getUid(fn);
- var listener = {dataPath: dataPath, id: opt_id, fn: fn};
- var expr = goog.ds.Expr.create(dataPath);
- var fnUid = goog.getUid(fn);
- if (!this.listenersByFunction_[fnUid]) {
- this.listenersByFunction_[fnUid] = {};
- }
- this.listenersByFunction_[fnUid][key] = {listener: listener, items: []};
- while (expr) {
- var listenerSpec = {listener: listener, maxAncestors: maxAncestors};
- var matchingListeners = this.listenerMap_[expr.getSource()];
- if (matchingListeners == null) {
- matchingListeners = {};
- this.listenerMap_[expr.getSource()] = matchingListeners;
- }
- matchingListeners[key] = listenerSpec;
- maxAncestors = 0;
- expr = expr.getParent();
- this.listenersByFunction_[fnUid][key].items.push(
- {key: key, obj: matchingListeners});
- }
- };
- /**
- * Adds an indexed listener.
- *
- * Indexed listeners allow for '*' in data paths. If a * exists, will match
- * all values and return the matched values in an array to the callback.
- *
- * Currently uses a promiscuous match algorithm: Matches everything before the
- * first '*', and then does a regex match for all of the returned events.
- * Although this isn't optimized, it is still an improvement as you can collapse
- * 100's of listeners into a single regex match
- *
- * @param {Function} fn Callback function, signature (dataPath, id, indexes).
- * @param {string} dataPath Fully qualified data path.
- * @param {string=} opt_id A value passed back to the listener when the dataPath
- * is matched.
- */
- goog.ds.DataManager.prototype.addIndexedListener = function(
- fn, dataPath, opt_id) {
- var firstStarPos = dataPath.indexOf('*');
- // Just need a regular listener
- if (firstStarPos == -1) {
- this.addListener(fn, dataPath, opt_id);
- return;
- }
- var listenPath = dataPath.substring(0, firstStarPos) + '...';
- // Create regex that matches * to any non '\' character
- var ext = '$';
- if (goog.string.endsWith(dataPath, '/...')) {
- dataPath = dataPath.substring(0, dataPath.length - 4);
- ext = '';
- }
- var regExpPath = goog.string.regExpEscape(dataPath);
- var matchRegExp = regExpPath.replace(/\\\*/g, '([^\\\/]+)') + ext;
- // Matcher function applies the regex and calls back the original function
- // if the regex matches, passing in an array of the matched values
- var matchRegExpRe = new RegExp(matchRegExp);
- var matcher = function(path, id) {
- var match = matchRegExpRe.exec(path);
- if (match) {
- match.shift();
- fn(path, opt_id, match);
- }
- };
- this.addListener(matcher, listenPath, opt_id);
- // Add the indexed listener to the map so that we can remove it later.
- var fnUid = goog.getUid(fn);
- if (!this.indexedListenersByFunction_[fnUid]) {
- this.indexedListenersByFunction_[fnUid] = {};
- }
- var key = dataPath + ':' + opt_id;
- this.indexedListenersByFunction_[fnUid][key] = {
- listener: {dataPath: listenPath, fn: matcher, id: opt_id}
- };
- };
- /**
- * Removes indexed listeners with a given callback function, and optional
- * matching datapath and matching id.
- *
- * @param {Function} fn Callback function, signature function(dataPath, id).
- * @param {string=} opt_dataPath Fully qualified data path.
- * @param {string=} opt_id A value passed back to the listener when the dataPath
- * is matched.
- */
- goog.ds.DataManager.prototype.removeIndexedListeners = function(
- fn, opt_dataPath, opt_id) {
- this.removeListenersByFunction_(
- this.indexedListenersByFunction_, true, fn, opt_dataPath, opt_id);
- };
- /**
- * Removes listeners with a given callback function, and optional
- * matching dataPath and matching id
- *
- * @param {Function} fn Callback function, signature function(dataPath, id).
- * @param {string=} opt_dataPath Fully qualified data path.
- * @param {string=} opt_id A value passed back to the listener when the dataPath
- * is matched.
- */
- goog.ds.DataManager.prototype.removeListeners = function(
- fn, opt_dataPath, opt_id) {
- // Normalize data path root
- if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/...')) {
- opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 4);
- } else if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/*')) {
- opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 2);
- }
- this.removeListenersByFunction_(
- this.listenersByFunction_, false, fn, opt_dataPath, opt_id);
- };
- /**
- * Removes listeners with a given callback function, and optional
- * matching dataPath and matching id from the given listenersByFunction
- * data structure.
- *
- * @param {Object} listenersByFunction The listeners by function.
- * @param {boolean} indexed Indicates whether the listenersByFunction are
- * indexed or not.
- * @param {Function} fn Callback function, signature function(dataPath, id).
- * @param {string=} opt_dataPath Fully qualified data path.
- * @param {string=} opt_id A value passed back to the listener when the dataPath
- * is matched.
- * @private
- */
- goog.ds.DataManager.prototype.removeListenersByFunction_ = function(
- listenersByFunction, indexed, fn, opt_dataPath, opt_id) {
- var fnUid = goog.getUid(fn);
- var functionMatches = listenersByFunction[fnUid];
- if (functionMatches != null) {
- for (var key in functionMatches) {
- var functionMatch = functionMatches[key];
- var listener = functionMatch.listener;
- if ((!opt_dataPath || opt_dataPath == listener.dataPath) &&
- (!opt_id || opt_id == listener.id)) {
- if (indexed) {
- this.removeListeners(listener.fn, listener.dataPath, listener.id);
- }
- if (functionMatch.items) {
- for (var i = 0; i < functionMatch.items.length; i++) {
- var item = functionMatch.items[i];
- delete item.obj[item.key];
- }
- }
- delete functionMatches[key];
- }
- }
- }
- };
- /**
- * Get the total number of listeners (per expression listened to, so may be
- * more than number of times addListener() has been called
- * @return {number} Number of listeners.
- */
- goog.ds.DataManager.prototype.getListenerCount = function() {
- var /** number */ count = 0;
- goog.object.forEach(this.listenerMap_, function(matchingListeners) {
- count += goog.structs.getCount(matchingListeners);
- });
- return count;
- };
- /**
- * Disables the sending of all data events during the execution of the given
- * callback. This provides a way to avoid useless notifications of small changes
- * when you will eventually send a data event manually that encompasses them
- * all.
- *
- * Note that this function can not be called reentrantly.
- *
- * @param {Function} callback Zero-arg function to execute.
- */
- goog.ds.DataManager.prototype.runWithoutFiringDataChanges = function(callback) {
- if (this.disableFiring_) {
- throw Error('Can not nest calls to runWithoutFiringDataChanges');
- }
- this.disableFiring_ = true;
- try {
- callback();
- } finally {
- this.disableFiring_ = false;
- }
- };
- /**
- * Fire a data change event to all listeners
- *
- * If the path matches the path of a listener, the listener will fire
- *
- * If your path is the parent of a listener, the listener will fire. I.e.
- * if $Contacts/bob@bob.com changes, then we will fire listener for
- * $Contacts/bob@bob.com/Name as well, as the assumption is that when
- * a parent changes, all children are invalidated.
- *
- * If your path is the child of a listener, the listener may fire, depending
- * on the ancestor depth.
- *
- * A listener for $Contacts might only be interested if the contact name changes
- * (i.e. $Contacts doesn't fire on $Contacts/bob@bob.com/Name),
- * while a listener for a specific contact might
- * (i.e. $Contacts/bob@bob.com would fire on $Contacts/bob@bob.com/Name).
- * Adding "/..." to a lisetener path listens to all children, and adding "/*" to
- * a listener path listens only to direct children
- *
- * @param {string} dataPath Fully qualified data path.
- */
- goog.ds.DataManager.prototype.fireDataChange = function(dataPath) {
- if (this.disableFiring_) {
- return;
- }
- var expr = goog.ds.Expr.create(dataPath);
- var ancestorDepth = 0;
- // Look for listeners for expression and all its parents.
- // Parents of listener expressions are all added to the listenerMap as well,
- // so this will evaluate inner loop every time the dataPath is a child or
- // an ancestor of the original listener path
- while (expr) {
- var matchingListeners = this.listenerMap_[expr.getSource()];
- if (matchingListeners) {
- for (var id in matchingListeners) {
- var match = matchingListeners[id];
- var listener = match.listener;
- if (ancestorDepth <= match.maxAncestors) {
- listener.fn(dataPath, listener.id);
- }
- }
- }
- ancestorDepth++;
- expr = expr.getParent();
- }
- this.eventCount_++;
- };
|