Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/datasource/datamanager.js
2868 views
1
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview
17
* Central class for registering and accessing data sources
18
* Also handles processing of data events.
19
*
20
* There is a shared global instance that most client code should access via
21
* goog.ds.DataManager.getInstance(). However you can also create your own
22
* DataManager using new
23
*
24
* Implements DataNode to provide the top element in a data registry
25
* Prepends '$' to top level data names in path to denote they are root object
26
*
27
*/
28
goog.provide('goog.ds.DataManager');
29
30
goog.require('goog.ds.BasicNodeList');
31
goog.require('goog.ds.DataNode');
32
goog.require('goog.ds.Expr');
33
goog.require('goog.object');
34
goog.require('goog.string');
35
goog.require('goog.structs');
36
goog.require('goog.structs.Map');
37
38
39
40
/**
41
* Create a DataManger
42
* @extends {goog.ds.DataNode}
43
* @constructor
44
* @final
45
*/
46
goog.ds.DataManager = function() {
47
this.dataSources_ = new goog.ds.BasicNodeList();
48
this.autoloads_ = new goog.structs.Map();
49
this.listenerMap_ = {};
50
this.listenersByFunction_ = {};
51
this.aliases_ = {};
52
this.eventCount_ = 0;
53
this.indexedListenersByFunction_ = {};
54
};
55
56
57
/**
58
* Global instance
59
* @private
60
*/
61
goog.ds.DataManager.instance_ = null;
62
goog.inherits(goog.ds.DataManager, goog.ds.DataNode);
63
64
65
/**
66
* Get the global instance
67
* @return {!goog.ds.DataManager} The data manager singleton.
68
*/
69
goog.ds.DataManager.getInstance = function() {
70
if (!goog.ds.DataManager.instance_) {
71
goog.ds.DataManager.instance_ = new goog.ds.DataManager();
72
}
73
return goog.ds.DataManager.instance_;
74
};
75
76
77
/**
78
* Clears the global instance (for unit tests to reset state).
79
*/
80
goog.ds.DataManager.clearInstance = function() {
81
goog.ds.DataManager.instance_ = null;
82
};
83
84
85
/**
86
* Add a data source
87
* @param {goog.ds.DataNode} ds The data source.
88
* @param {boolean=} opt_autoload Whether to automatically load the data,
89
* defaults to false.
90
* @param {string=} opt_name Optional name, can also get name
91
* from the datasource.
92
*/
93
goog.ds.DataManager.prototype.addDataSource = function(
94
ds, opt_autoload, opt_name) {
95
var autoload = !!opt_autoload;
96
var name = opt_name || ds.getDataName();
97
if (!goog.string.startsWith(name, '$')) {
98
name = '$' + name;
99
}
100
ds.setDataName(name);
101
this.dataSources_.add(ds);
102
this.autoloads_.set(name, autoload);
103
};
104
105
106
/**
107
* Create an alias for a data path, very similar to assigning a variable.
108
* For example, you can set $CurrentContact -> $Request/Contacts[5], and all
109
* references to $CurrentContact will be procesed on $Request/Contacts[5].
110
*
111
* Aliases will hide datasources of the same name.
112
*
113
* @param {string} name Alias name, must be a top level path ($Foo).
114
* @param {string} dataPath Data path being aliased.
115
*/
116
goog.ds.DataManager.prototype.aliasDataSource = function(name, dataPath) {
117
if (!this.aliasListener_) {
118
this.aliasListener_ = goog.bind(this.listenForAlias_, this);
119
}
120
if (this.aliases_[name]) {
121
var oldPath = this.aliases_[name].getSource();
122
this.removeListeners(this.aliasListener_, oldPath + '/...', name);
123
}
124
this.aliases_[name] = goog.ds.Expr.create(dataPath);
125
this.addListener(this.aliasListener_, dataPath + '/...', name);
126
this.fireDataChange(name);
127
};
128
129
130
/**
131
* Listener function for matches of paths that have been aliased.
132
* Fires a data change on the alias as well.
133
*
134
* @param {string} dataPath Path of data event fired.
135
* @param {string} name Name of the alias.
136
* @private
137
*/
138
goog.ds.DataManager.prototype.listenForAlias_ = function(dataPath, name) {
139
var aliasedExpr = this.aliases_[name];
140
141
if (aliasedExpr) {
142
// If it's a subpath, appends the subpath to the alias name
143
// otherwise just fires on the top level alias
144
var aliasedPath = aliasedExpr.getSource();
145
if (dataPath.indexOf(aliasedPath) == 0) {
146
this.fireDataChange(name + dataPath.substring(aliasedPath.length));
147
} else {
148
this.fireDataChange(name);
149
}
150
}
151
};
152
153
154
/**
155
* Gets a named child node of the current node.
156
*
157
* @param {string} name The node name.
158
* @return {goog.ds.DataNode} The child node,
159
* or null if no node of this name exists.
160
*/
161
goog.ds.DataManager.prototype.getDataSource = function(name) {
162
if (this.aliases_[name]) {
163
return this.aliases_[name].getNode();
164
} else {
165
return this.dataSources_.get(name);
166
}
167
};
168
169
170
/**
171
* Get the value of the node
172
* @return {!Object} The value of the node.
173
* @override
174
*/
175
goog.ds.DataManager.prototype.get = function() {
176
return this.dataSources_;
177
};
178
179
180
/** @override */
181
goog.ds.DataManager.prototype.set = function(value) {
182
throw Error('Can\'t set on DataManager');
183
};
184
185
186
/** @override */
187
goog.ds.DataManager.prototype.getChildNodes = function(opt_selector) {
188
if (opt_selector) {
189
return new goog.ds.BasicNodeList(
190
[this.getChildNode(/** @type {string} */ (opt_selector))]);
191
} else {
192
return this.dataSources_;
193
}
194
};
195
196
197
/**
198
* Gets a named child node of the current node
199
* @param {string} name The node name.
200
* @return {goog.ds.DataNode} The child node,
201
* or null if no node of this name exists.
202
* @override
203
*/
204
goog.ds.DataManager.prototype.getChildNode = function(name) {
205
return this.getDataSource(name);
206
};
207
208
209
/** @override */
210
goog.ds.DataManager.prototype.getChildNodeValue = function(name) {
211
var ds = this.getDataSource(name);
212
return ds ? ds.get() : null;
213
};
214
215
216
/**
217
* Get the name of the node relative to the parent node
218
* @return {string} The name of the node.
219
* @override
220
*/
221
goog.ds.DataManager.prototype.getDataName = function() {
222
return '';
223
};
224
225
226
/**
227
* Gets the a qualified data path to this node
228
* @return {string} The data path.
229
* @override
230
*/
231
goog.ds.DataManager.prototype.getDataPath = function() {
232
return '';
233
};
234
235
236
/**
237
* Load or reload the backing data for this node
238
* only loads datasources flagged with autoload
239
* @override
240
*/
241
goog.ds.DataManager.prototype.load = function() {
242
var len = this.dataSources_.getCount();
243
for (var i = 0; i < len; i++) {
244
var ds = this.dataSources_.getByIndex(i);
245
var autoload = this.autoloads_.get(ds.getDataName());
246
if (autoload) {
247
ds.load();
248
}
249
}
250
};
251
252
253
/**
254
* Gets the state of the backing data for this node
255
* @return {goog.ds.LoadState} The state.
256
* @override
257
*/
258
goog.ds.DataManager.prototype.getLoadState = goog.abstractMethod;
259
260
261
/**
262
* Whether the value of this node is a homogeneous list of data
263
* @return {boolean} True if a list.
264
* @override
265
*/
266
goog.ds.DataManager.prototype.isList = function() {
267
return false;
268
};
269
270
271
/**
272
* Get the total count of events fired (mostly for debugging)
273
* @return {number} Count of events.
274
*/
275
goog.ds.DataManager.prototype.getEventCount = function() {
276
return this.eventCount_;
277
};
278
279
280
/**
281
* Adds a listener
282
* Listeners should fire when any data with path that has dataPath as substring
283
* is changed.
284
* TODO(user) Look into better listener handling
285
*
286
* @param {Function} fn Callback function, signature function(dataPath, id).
287
* @param {string} dataPath Fully qualified data path.
288
* @param {string=} opt_id A value passed back to the listener when the dataPath
289
* is matched.
290
*/
291
goog.ds.DataManager.prototype.addListener = function(fn, dataPath, opt_id) {
292
// maxAncestor sets how distant an ancestor you can be of the fired event
293
// and still fire (you always fire if you are a descendant).
294
// 0 means you don't fire if you are an ancestor
295
// 1 means you only fire if you are parent
296
// 1000 means you will fire if you are ancestor (effectively infinite)
297
var maxAncestors = 0;
298
if (goog.string.endsWith(dataPath, '/...')) {
299
maxAncestors = 1000;
300
dataPath = dataPath.substring(0, dataPath.length - 4);
301
} else if (goog.string.endsWith(dataPath, '/*')) {
302
maxAncestors = 1;
303
dataPath = dataPath.substring(0, dataPath.length - 2);
304
}
305
306
opt_id = opt_id || '';
307
var key = dataPath + ':' + opt_id + ':' + goog.getUid(fn);
308
var listener = {dataPath: dataPath, id: opt_id, fn: fn};
309
var expr = goog.ds.Expr.create(dataPath);
310
311
var fnUid = goog.getUid(fn);
312
if (!this.listenersByFunction_[fnUid]) {
313
this.listenersByFunction_[fnUid] = {};
314
}
315
this.listenersByFunction_[fnUid][key] = {listener: listener, items: []};
316
317
while (expr) {
318
var listenerSpec = {listener: listener, maxAncestors: maxAncestors};
319
var matchingListeners = this.listenerMap_[expr.getSource()];
320
if (matchingListeners == null) {
321
matchingListeners = {};
322
this.listenerMap_[expr.getSource()] = matchingListeners;
323
}
324
matchingListeners[key] = listenerSpec;
325
maxAncestors = 0;
326
expr = expr.getParent();
327
this.listenersByFunction_[fnUid][key].items.push(
328
{key: key, obj: matchingListeners});
329
}
330
};
331
332
333
/**
334
* Adds an indexed listener.
335
*
336
* Indexed listeners allow for '*' in data paths. If a * exists, will match
337
* all values and return the matched values in an array to the callback.
338
*
339
* Currently uses a promiscuous match algorithm: Matches everything before the
340
* first '*', and then does a regex match for all of the returned events.
341
* Although this isn't optimized, it is still an improvement as you can collapse
342
* 100's of listeners into a single regex match
343
*
344
* @param {Function} fn Callback function, signature (dataPath, id, indexes).
345
* @param {string} dataPath Fully qualified data path.
346
* @param {string=} opt_id A value passed back to the listener when the dataPath
347
* is matched.
348
*/
349
goog.ds.DataManager.prototype.addIndexedListener = function(
350
fn, dataPath, opt_id) {
351
var firstStarPos = dataPath.indexOf('*');
352
// Just need a regular listener
353
if (firstStarPos == -1) {
354
this.addListener(fn, dataPath, opt_id);
355
return;
356
}
357
358
var listenPath = dataPath.substring(0, firstStarPos) + '...';
359
360
// Create regex that matches * to any non '\' character
361
var ext = '$';
362
if (goog.string.endsWith(dataPath, '/...')) {
363
dataPath = dataPath.substring(0, dataPath.length - 4);
364
ext = '';
365
}
366
var regExpPath = goog.string.regExpEscape(dataPath);
367
var matchRegExp = regExpPath.replace(/\\\*/g, '([^\\\/]+)') + ext;
368
369
// Matcher function applies the regex and calls back the original function
370
// if the regex matches, passing in an array of the matched values
371
var matchRegExpRe = new RegExp(matchRegExp);
372
var matcher = function(path, id) {
373
var match = matchRegExpRe.exec(path);
374
if (match) {
375
match.shift();
376
fn(path, opt_id, match);
377
}
378
};
379
this.addListener(matcher, listenPath, opt_id);
380
381
// Add the indexed listener to the map so that we can remove it later.
382
var fnUid = goog.getUid(fn);
383
if (!this.indexedListenersByFunction_[fnUid]) {
384
this.indexedListenersByFunction_[fnUid] = {};
385
}
386
var key = dataPath + ':' + opt_id;
387
this.indexedListenersByFunction_[fnUid][key] = {
388
listener: {dataPath: listenPath, fn: matcher, id: opt_id}
389
};
390
};
391
392
393
/**
394
* Removes indexed listeners with a given callback function, and optional
395
* matching datapath and matching id.
396
*
397
* @param {Function} fn Callback function, signature function(dataPath, id).
398
* @param {string=} opt_dataPath Fully qualified data path.
399
* @param {string=} opt_id A value passed back to the listener when the dataPath
400
* is matched.
401
*/
402
goog.ds.DataManager.prototype.removeIndexedListeners = function(
403
fn, opt_dataPath, opt_id) {
404
this.removeListenersByFunction_(
405
this.indexedListenersByFunction_, true, fn, opt_dataPath, opt_id);
406
};
407
408
409
/**
410
* Removes listeners with a given callback function, and optional
411
* matching dataPath and matching id
412
*
413
* @param {Function} fn Callback function, signature function(dataPath, id).
414
* @param {string=} opt_dataPath Fully qualified data path.
415
* @param {string=} opt_id A value passed back to the listener when the dataPath
416
* is matched.
417
*/
418
goog.ds.DataManager.prototype.removeListeners = function(
419
fn, opt_dataPath, opt_id) {
420
421
// Normalize data path root
422
if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/...')) {
423
opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 4);
424
} else if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/*')) {
425
opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 2);
426
}
427
428
this.removeListenersByFunction_(
429
this.listenersByFunction_, false, fn, opt_dataPath, opt_id);
430
};
431
432
433
/**
434
* Removes listeners with a given callback function, and optional
435
* matching dataPath and matching id from the given listenersByFunction
436
* data structure.
437
*
438
* @param {Object} listenersByFunction The listeners by function.
439
* @param {boolean} indexed Indicates whether the listenersByFunction are
440
* indexed or not.
441
* @param {Function} fn Callback function, signature function(dataPath, id).
442
* @param {string=} opt_dataPath Fully qualified data path.
443
* @param {string=} opt_id A value passed back to the listener when the dataPath
444
* is matched.
445
* @private
446
*/
447
goog.ds.DataManager.prototype.removeListenersByFunction_ = function(
448
listenersByFunction, indexed, fn, opt_dataPath, opt_id) {
449
var fnUid = goog.getUid(fn);
450
var functionMatches = listenersByFunction[fnUid];
451
if (functionMatches != null) {
452
for (var key in functionMatches) {
453
var functionMatch = functionMatches[key];
454
var listener = functionMatch.listener;
455
if ((!opt_dataPath || opt_dataPath == listener.dataPath) &&
456
(!opt_id || opt_id == listener.id)) {
457
if (indexed) {
458
this.removeListeners(listener.fn, listener.dataPath, listener.id);
459
}
460
if (functionMatch.items) {
461
for (var i = 0; i < functionMatch.items.length; i++) {
462
var item = functionMatch.items[i];
463
delete item.obj[item.key];
464
}
465
}
466
delete functionMatches[key];
467
}
468
}
469
}
470
};
471
472
473
/**
474
* Get the total number of listeners (per expression listened to, so may be
475
* more than number of times addListener() has been called
476
* @return {number} Number of listeners.
477
*/
478
goog.ds.DataManager.prototype.getListenerCount = function() {
479
var /** number */ count = 0;
480
goog.object.forEach(this.listenerMap_, function(matchingListeners) {
481
count += goog.structs.getCount(matchingListeners);
482
});
483
return count;
484
};
485
486
487
/**
488
* Disables the sending of all data events during the execution of the given
489
* callback. This provides a way to avoid useless notifications of small changes
490
* when you will eventually send a data event manually that encompasses them
491
* all.
492
*
493
* Note that this function can not be called reentrantly.
494
*
495
* @param {Function} callback Zero-arg function to execute.
496
*/
497
goog.ds.DataManager.prototype.runWithoutFiringDataChanges = function(callback) {
498
if (this.disableFiring_) {
499
throw Error('Can not nest calls to runWithoutFiringDataChanges');
500
}
501
502
this.disableFiring_ = true;
503
try {
504
callback();
505
} finally {
506
this.disableFiring_ = false;
507
}
508
};
509
510
511
/**
512
* Fire a data change event to all listeners
513
*
514
* If the path matches the path of a listener, the listener will fire
515
*
516
* If your path is the parent of a listener, the listener will fire. I.e.
517
* if $Contacts/[email protected] changes, then we will fire listener for
518
* $Contacts/[email protected]/Name as well, as the assumption is that when
519
* a parent changes, all children are invalidated.
520
*
521
* If your path is the child of a listener, the listener may fire, depending
522
* on the ancestor depth.
523
*
524
* A listener for $Contacts might only be interested if the contact name changes
525
* (i.e. $Contacts doesn't fire on $Contacts/[email protected]/Name),
526
* while a listener for a specific contact might
527
* (i.e. $Contacts/[email protected] would fire on $Contacts/[email protected]/Name).
528
* Adding "/..." to a lisetener path listens to all children, and adding "/*" to
529
* a listener path listens only to direct children
530
*
531
* @param {string} dataPath Fully qualified data path.
532
*/
533
goog.ds.DataManager.prototype.fireDataChange = function(dataPath) {
534
if (this.disableFiring_) {
535
return;
536
}
537
538
var expr = goog.ds.Expr.create(dataPath);
539
var ancestorDepth = 0;
540
541
// Look for listeners for expression and all its parents.
542
// Parents of listener expressions are all added to the listenerMap as well,
543
// so this will evaluate inner loop every time the dataPath is a child or
544
// an ancestor of the original listener path
545
while (expr) {
546
var matchingListeners = this.listenerMap_[expr.getSource()];
547
if (matchingListeners) {
548
for (var id in matchingListeners) {
549
var match = matchingListeners[id];
550
var listener = match.listener;
551
if (ancestorDepth <= match.maxAncestors) {
552
listener.fn(dataPath, listener.id);
553
}
554
}
555
}
556
ancestorDepth++;
557
expr = expr.getParent();
558
}
559
this.eventCount_++;
560
};
561
562