Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(445)

Side by Side Diff: pkg/mutation_observer/lib/mutation_observer.js

Issue 21109007: add mutation observer polyfill package (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: fix for constructor property Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 /*
2 * Copyright 2013 The Polymer Authors. All rights reserved.
3 * Use of this source code is goverened by a BSD-style
4 * license that can be found in the LICENSE file.
5 */
6
7 // TODO(jmesserly): polyfill does not have feature testing or the definition of
8 // SideTable. The extra code is from:
9 // https://github.com/Polymer/CustomElements/blob/master/src/MutationObserver.js
10 // https://github.com/Polymer/CustomElements/blob/master/src/sidetable.js
11 // I also renamed JsMutationObserver -> MutationObserver to correctly interact
12 // with dart2js interceptors.
13 if (!window.MutationObserver && !window.WebKitMutationObserver) {
14
15 (function(global) {
16 // SideTable is a weak map where possible. If WeakMap is not available the
17 // association is stored as an expando property.
18 var SideTable;
19 // TODO(arv): WeakMap does not allow for Node etc to be keys in Firefox
20 if (typeof WeakMap !== 'undefined' && navigator.userAgent.indexOf('Firefox/') < 0) {
21 SideTable = WeakMap;
22 } else {
23 (function() {
24 var defineProperty = Object.defineProperty;
25 var hasOwnProperty = Object.hasOwnProperty;
26 var counter = new Date().getTime() % 1e9;
27
28 SideTable = function() {
29 this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');
30 };
31
32 SideTable.prototype = {
33 set: function(key, value) {
34 defineProperty(key, this.name, {value: value, writable: true});
35 },
36 get: function(key) {
37 return hasOwnProperty.call(key, this.name) ? key[this.name] : undefine d;
38 },
39 delete: function(key) {
40 this.set(key, undefined);
41 }
42 }
43 })();
44 }
45
46 var registrationsTable = new SideTable();
47
48 // We use setImmediate or postMessage for our future callback.
49 var setImmediate = window.msSetImmediate;
50
51 // Use post message to emulate setImmediate.
52 if (!setImmediate) {
53 var setImmediateQueue = [];
54 var sentinel = String(Math.random());
55 window.addEventListener('message', function(e) {
56 if (e.data === sentinel) {
57 var queue = setImmediateQueue;
58 setImmediateQueue = [];
59 queue.forEach(function(func) {
60 func();
61 });
62 }
63 });
64 setImmediate = function(func) {
65 setImmediateQueue.push(func);
66 window.postMessage(sentinel, '*');
67 };
68 }
69
70 // This is used to ensure that we never schedule 2 callas to setImmediate
71 var isScheduled = false;
72
73 // Keep track of observers that needs to be notified next time.
74 var scheduledObservers = [];
75
76 /**
77 * Schedules |dispatchCallback| to be called in the future.
78 * @param {MutationObserver} observer
79 */
80 function scheduleCallback(observer) {
81 scheduledObservers.push(observer);
82 if (!isScheduled) {
83 isScheduled = true;
84 setImmediate(dispatchCallbacks);
85 }
86 }
87
88 function wrapIfNeeded(node) {
89 return window.ShadowDOMPolyfill &&
90 window.ShadowDOMPolyfill.wrapIfNeeded(node) ||
91 node;
92 }
93
94 function dispatchCallbacks() {
95 // http://dom.spec.whatwg.org/#mutation-observers
96
97 isScheduled = false; // Used to allow a new setImmediate call above.
98
99 var observers = scheduledObservers;
100 scheduledObservers = [];
101 // Sort observers based on their creation UID (incremental).
102 observers.sort(function(o1, o2) {
103 return o1.uid_ - o2.uid_;
104 });
105
106 var anyNonEmpty = false;
107 observers.forEach(function(observer) {
108
109 // 2.1, 2.2
110 var queue = observer.takeRecords();
111 // 2.3. Remove all transient registered observers whose observer is mo.
112 removeTransientObserversFor(observer);
113
114 // 2.4
115 if (queue.length) {
116 observer.callback_(queue, observer);
117 anyNonEmpty = true;
118 }
119 });
120
121 // 3.
122 if (anyNonEmpty)
123 dispatchCallbacks();
124 }
125
126 function removeTransientObserversFor(observer) {
127 observer.nodes_.forEach(function(node) {
128 var registrations = registrationsTable.get(node);
129 if (!registrations)
130 return;
131 registrations.forEach(function(registration) {
132 if (registration.observer === observer)
133 registration.removeTransientObservers();
134 });
135 });
136 }
137
138 /**
139 * This function is used for the "For each registered observer observer (with
140 * observer's options as options) in target's list of registered observers,
141 * run these substeps:" and the "For each ancestor ancestor of target, and for
142 * each registered observer observer (with options options) in ancestor's list
143 * of registered observers, run these substeps:" part of the algorithms. The
144 * |options.subtree| is checked to ensure that the callback is called
145 * correctly.
146 *
147 * @param {Node} target
148 * @param {function(MutationObserverInit):MutationRecord} callback
149 */
150 function forEachAncestorAndObserverEnqueueRecord(target, callback) {
151 for (var node = target; node; node = node.parentNode) {
152 var registrations = registrationsTable.get(node);
153
154 if (registrations) {
155 for (var j = 0; j < registrations.length; j++) {
156 var registration = registrations[j];
157 var options = registration.options;
158
159 // Only target ignores subtree.
160 if (node !== target && !options.subtree)
161 continue;
162
163 var record = callback(options);
164 if (record)
165 registration.enqueue(record);
166 }
167 }
168 }
169 }
170
171 var uidCounter = 0;
172
173 /**
174 * The class that maps to the DOM MutationObserver interface.
175 * @param {Function} callback.
176 * @constructor
177 */
178 function MutationObserver(callback) {
179 this.callback_ = callback;
180 this.nodes_ = [];
181 this.records_ = [];
182 this.uid_ = ++uidCounter;
183 }
184
185 MutationObserver.prototype = {
186 // TODO(jmesserly): why is this necessary?
187 get constructor() { return MutationObserver; },
Jennifer Messerly 2013/07/30 03:27:24 I have no idea why this is needed. Without it, the
188
189 observe: function(target, options) {
190 target = wrapIfNeeded(target);
191
192 // 1.1
193 if (!options.childList && !options.attributes && !options.characterData ||
194
195 // 1.2
196 options.attributeOldValue && !options.attributes ||
197
198 // 1.3
199 options.attributeFilter && options.attributeFilter.length &&
200 !options.attributes ||
201
202 // 1.4
203 options.characterDataOldValue && !options.characterData) {
204
205 throw new SyntaxError();
206 }
207
208 var registrations = registrationsTable.get(target);
209 if (!registrations)
210 registrationsTable.set(target, registrations = []);
211
212 // 2
213 // If target's list of registered observers already includes a registered
214 // observer associated with the context object, replace that registered
215 // observer's options with options.
216 var registration;
217 for (var i = 0; i < registrations.length; i++) {
218 if (registrations[i].observer === this) {
219 registration = registrations[i];
220 registration.removeListeners();
221 registration.options = options;
222 break;
223 }
224 }
225
226 // 3.
227 // Otherwise, add a new registered observer to target's list of registered
228 // observers with the context object as the observer and options as the
229 // options, and add target to context object's list of nodes on which it
230 // is registered.
231 if (!registration) {
232 registration = new Registration(this, target, options);
233 registrations.push(registration);
234 this.nodes_.push(target);
235 }
236
237 registration.addListeners();
238 },
239
240 disconnect: function() {
241 this.nodes_.forEach(function(node) {
242 var registrations = registrationsTable.get(node);
243 for (var i = 0; i < registrations.length; i++) {
244 var registration = registrations[i];
245 if (registration.observer === this) {
246 registration.removeListeners();
247 registrations.splice(i, 1);
248 // Each node can only have one registered observer associated with
249 // this observer.
250 break;
251 }
252 }
253 }, this);
254 this.records_ = [];
255 },
256
257 takeRecords: function() {
258 var copyOfRecords = this.records_;
259 this.records_ = [];
260 return copyOfRecords;
261 }
262 };
263
264 /**
265 * @param {string} type
266 * @param {Node} target
267 * @constructor
268 */
269 function MutationRecord(type, target) {
270 this.type = type;
271 this.target = target;
272 this.addedNodes = [];
273 this.removedNodes = [];
274 this.previousSibling = null;
275 this.nextSibling = null;
276 this.attributeName = null;
277 this.attributeNamespace = null;
278 this.oldValue = null;
279 }
280
281 function copyMutationRecord(original) {
282 var record = new MutationRecord(original.type, original.target);
283 record.addedNodes = original.addedNodes.slice();
284 record.removedNodes = original.removedNodes.slice();
285 record.previousSibling = original.previousSibling;
286 record.nextSibling = original.nextSibling;
287 record.attributeName = original.attributeName;
288 record.attributeNamespace = original.attributeNamespace;
289 record.oldValue = original.oldValue;
290 return record;
291 };
292
293 // We keep track of the two (possibly one) records used in a single mutation.
294 var currentRecord, recordWithOldValue;
295
296 /**
297 * Creates a record without |oldValue| and caches it as |currentRecord| for
298 * later use.
299 * @param {string} oldValue
300 * @return {MutationRecord}
301 */
302 function getRecord(type, target) {
303 return currentRecord = new MutationRecord(type, target);
304 }
305
306 /**
307 * Gets or creates a record with |oldValue| based in the |currentRecord|
308 * @param {string} oldValue
309 * @return {MutationRecord}
310 */
311 function getRecordWithOldValue(oldValue) {
312 if (recordWithOldValue)
313 return recordWithOldValue;
314 recordWithOldValue = copyMutationRecord(currentRecord);
315 recordWithOldValue.oldValue = oldValue;
316 return recordWithOldValue;
317 }
318
319 function clearRecords() {
320 currentRecord = recordWithOldValue = undefined;
321 }
322
323 /**
324 * @param {MutationRecord} record
325 * @return {boolean} Whether the record represents a record from the current
326 * mutation event.
327 */
328 function recordRepresentsCurrentMutation(record) {
329 return record === recordWithOldValue || record === currentRecord;
330 }
331
332 /**
333 * Selects which record, if any, to replace the last record in the queue.
334 * This returns |null| if no record should be replaced.
335 *
336 * @param {MutationRecord} lastRecord
337 * @param {MutationRecord} newRecord
338 * @param {MutationRecord}
339 */
340 function selectRecord(lastRecord, newRecord) {
341 if (lastRecord === newRecord)
342 return lastRecord;
343
344 // Check if the the record we are adding represents the same record. If
345 // so, we keep the one with the oldValue in it.
346 if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord))
347 return recordWithOldValue;
348
349 return null;
350 }
351
352 /**
353 * Class used to represent a registered observer.
354 * @param {MutationObserver} observer
355 * @param {Node} target
356 * @param {MutationObserverInit} options
357 * @constructor
358 */
359 function Registration(observer, target, options) {
360 this.observer = observer;
361 this.target = target;
362 this.options = options;
363 this.transientObservedNodes = [];
364 }
365
366 Registration.prototype = {
367 enqueue: function(record) {
368 var records = this.observer.records_;
369 var length = records.length;
370
371 // There are cases where we replace the last record with the new record.
372 // For example if the record represents the same mutation we need to use
373 // the one with the oldValue. If we get same record (this can happen as we
374 // walk up the tree) we ignore the new record.
375 if (records.length > 0) {
376 var lastRecord = records[length - 1];
377 var recordToReplaceLast = selectRecord(lastRecord, record);
378 if (recordToReplaceLast) {
379 records[length - 1] = recordToReplaceLast;
380 return;
381 }
382 } else {
383 scheduleCallback(this.observer);
384 }
385
386 records[length] = record;
387 },
388
389 addListeners: function() {
390 this.addListeners_(this.target);
391 },
392
393 addListeners_: function(node) {
394 var options = this.options;
395 if (options.attributes)
396 node.addEventListener('DOMAttrModified', this, true);
397
398 if (options.characterData)
399 node.addEventListener('DOMCharacterDataModified', this, true);
400
401 if (options.childList)
402 node.addEventListener('DOMNodeInserted', this, true);
403
404 if (options.childList || options.subtree)
405 node.addEventListener('DOMNodeRemoved', this, true);
406 },
407
408 removeListeners: function() {
409 this.removeListeners_(this.target);
410 },
411
412 removeListeners_: function(node) {
413 var options = this.options;
414 if (options.attributes)
415 node.removeEventListener('DOMAttrModified', this, true);
416
417 if (options.characterData)
418 node.removeEventListener('DOMCharacterDataModified', this, true);
419
420 if (options.childList)
421 node.removeEventListener('DOMNodeInserted', this, true);
422
423 if (options.childList || options.subtree)
424 node.removeEventListener('DOMNodeRemoved', this, true);
425 },
426
427 /**
428 * Adds a transient observer on node. The transient observer gets removed
429 * next time we deliver the change records.
430 * @param {Node} node
431 */
432 addTransientObserver: function(node) {
433 // Don't add transient observers on the target itself. We already have all
434 // the required listeners set up on the target.
435 if (node === this.target)
436 return;
437
438 this.addListeners_(node);
439 this.transientObservedNodes.push(node);
440 var registrations = registrationsTable.get(node);
441 if (!registrations)
442 registrationsTable.set(node, registrations = []);
443
444 // We know that registrations does not contain this because we already
445 // checked if node === this.target.
446 registrations.push(this);
447 },
448
449 removeTransientObservers: function() {
450 var transientObservedNodes = this.transientObservedNodes;
451 this.transientObservedNodes = [];
452
453 transientObservedNodes.forEach(function(node) {
454 // Transient observers are never added to the target.
455 this.removeListeners_(node);
456
457 var registrations = registrationsTable.get(node);
458 for (var i = 0; i < registrations.length; i++) {
459 if (registrations[i] === this) {
460 registrations.splice(i, 1);
461 // Each node can only have one registered observer associated with
462 // this observer.
463 break;
464 }
465 }
466 }, this);
467 },
468
469 handleEvent: function(e) {
470 // Stop propagation since we are managing the propagation manually.
471 // This means that other mutation events on the page will not work
472 // correctly but that is by design.
473 e.stopImmediatePropagation();
474
475 switch (e.type) {
476 case 'DOMAttrModified':
477 // http://dom.spec.whatwg.org/#concept-mo-queue-attributes
478
479 var name = e.attrName;
480 var namespace = e.relatedNode.namespaceURI;
481 var target = e.target;
482
483 // 1.
484 var record = new getRecord('attributes', target);
485 record.attributeName = name;
486 record.attributeNamespace = namespace;
487
488 // 2.
489 var oldValue =
490 e.attrChange === MutationEvent.ADDITION ? null : e.prevValue;
491
492 forEachAncestorAndObserverEnqueueRecord(target, function(options) {
493 // 3.1, 4.2
494 if (!options.attributes)
495 return;
496
497 // 3.2, 4.3
498 if (options.attributeFilter && options.attributeFilter.length &&
499 options.attributeFilter.indexOf(name) === -1 &&
500 options.attributeFilter.indexOf(namespace) === -1) {
501 return;
502 }
503 // 3.3, 4.4
504 if (options.attributeOldValue)
505 return getRecordWithOldValue(oldValue);
506
507 // 3.4, 4.5
508 return record;
509 });
510
511 break;
512
513 case 'DOMCharacterDataModified':
514 // http://dom.spec.whatwg.org/#concept-mo-queue-characterdata
515 var target = e.target;
516
517 // 1.
518 var record = getRecord('characterData', target);
519
520 // 2.
521 var oldValue = e.prevValue;
522
523
524 forEachAncestorAndObserverEnqueueRecord(target, function(options) {
525 // 3.1, 4.2
526 if (!options.characterData)
527 return;
528
529 // 3.2, 4.3
530 if (options.characterDataOldValue)
531 return getRecordWithOldValue(oldValue);
532
533 // 3.3, 4.4
534 return record;
535 });
536
537 break;
538
539 case 'DOMNodeRemoved':
540 this.addTransientObserver(e.target);
541 // Fall through.
542 case 'DOMNodeInserted':
543 // http://dom.spec.whatwg.org/#concept-mo-queue-childlist
544 var target = e.relatedNode;
545 var changedNode = e.target;
546 var addedNodes, removedNodes;
547 if (e.type === 'DOMNodeInserted') {
548 addedNodes = [changedNode];
549 removedNodes = [];
550 } else {
551
552 addedNodes = [];
553 removedNodes = [changedNode];
554 }
555 var previousSibling = changedNode.previousSibling;
556 var nextSibling = changedNode.nextSibling;
557
558 // 1.
559 var record = getRecord('childList', target);
560 record.addedNodes = addedNodes;
561 record.removedNodes = removedNodes;
562 record.previousSibling = previousSibling;
563 record.nextSibling = nextSibling;
564
565 forEachAncestorAndObserverEnqueueRecord(target, function(options) {
566 // 2.1, 3.2
567 if (!options.childList)
568 return;
569
570 // 2.2, 3.3
571 return record;
572 });
573
574 }
575
576 clearRecords();
577 }
578 };
579
580 global.MutationObserver = MutationObserver;
581 })(window);
582
583 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698