[ADD] oe_dirty handling via mutation observers

shim necessary for browsers supporting old mutation events but not new mutation observers

bzr revid: xmo@openerp.com-20130828084945-yfqrs0v9v8iecwjm
This commit is contained in:
Xavier Morel
2013-08-28 10:49:45 +02:00
parent 35c8d2fe95
commit a7e58993cd
17 changed files with 1953 additions and 4 deletions
@@ -0,0 +1,9 @@
# Names should be added to this file with this pattern:
#
# For individuals:
# Name <email address>
#
# For organizations:
# Organization <fnmatch pattern>
#
Google Inc. <*@google.com>
@@ -0,0 +1,27 @@
// Copyright (c) 2012 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,542 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is goverened by a BSD-style
* license that can be found in the LICENSE file.
*/
(function(global) {
var registrationsTable = new SideTable();
// We use setImmediate or postMessage for our future callback.
var setImmediate = window.msSetImmediate;
// Use post message to emulate setImmediate.
if (!setImmediate) {
var setImmediateQueue = [];
var sentinel = String(Math.random());
window.addEventListener('message', function(e) {
if (e.data === sentinel) {
var queue = setImmediateQueue;
setImmediateQueue = [];
queue.forEach(function(func) {
func();
});
}
});
setImmediate = function(func) {
setImmediateQueue.push(func);
window.postMessage(sentinel, '*');
};
}
// This is used to ensure that we never schedule 2 callas to setImmediate
var isScheduled = false;
// Keep track of observers that needs to be notified next time.
var scheduledObservers = [];
/**
* Schedules |dispatchCallback| to be called in the future.
* @param {MutationObserver} observer
*/
function scheduleCallback(observer) {
scheduledObservers.push(observer);
if (!isScheduled) {
isScheduled = true;
setImmediate(dispatchCallbacks);
}
}
function wrapIfNeeded(node) {
return window.ShadowDOMPolyfill &&
window.ShadowDOMPolyfill.wrapIfNeeded(node) ||
node;
}
function dispatchCallbacks() {
// http://dom.spec.whatwg.org/#mutation-observers
isScheduled = false; // Used to allow a new setImmediate call above.
var observers = scheduledObservers;
scheduledObservers = [];
// Sort observers based on their creation UID (incremental).
observers.sort(function(o1, o2) {
return o1.uid_ - o2.uid_;
});
var anyNonEmpty = false;
observers.forEach(function(observer) {
// 2.1, 2.2
var queue = observer.takeRecords();
// 2.3. Remove all transient registered observers whose observer is mo.
removeTransientObserversFor(observer);
// 2.4
if (queue.length) {
observer.callback_(queue, observer);
anyNonEmpty = true;
}
});
// 3.
if (anyNonEmpty)
dispatchCallbacks();
}
function removeTransientObserversFor(observer) {
observer.nodes_.forEach(function(node) {
var registrations = registrationsTable.get(node);
if (!registrations)
return;
registrations.forEach(function(registration) {
if (registration.observer === observer)
registration.removeTransientObservers();
});
});
}
/**
* This function is used for the "For each registered observer observer (with
* observer's options as options) in target's list of registered observers,
* run these substeps:" and the "For each ancestor ancestor of target, and for
* each registered observer observer (with options options) in ancestor's list
* of registered observers, run these substeps:" part of the algorithms. The
* |options.subtree| is checked to ensure that the callback is called
* correctly.
*
* @param {Node} target
* @param {function(MutationObserverInit):MutationRecord} callback
*/
function forEachAncestorAndObserverEnqueueRecord(target, callback) {
for (var node = target; node; node = node.parentNode) {
var registrations = registrationsTable.get(node);
if (registrations) {
for (var j = 0; j < registrations.length; j++) {
var registration = registrations[j];
var options = registration.options;
// Only target ignores subtree.
if (node !== target && !options.subtree)
continue;
var record = callback(options);
if (record)
registration.enqueue(record);
}
}
}
}
var uidCounter = 0;
/**
* The class that maps to the DOM MutationObserver interface.
* @param {Function} callback.
* @constructor
*/
function JsMutationObserver(callback) {
this.callback_ = callback;
this.nodes_ = [];
this.records_ = [];
this.uid_ = ++uidCounter;
}
JsMutationObserver.prototype = {
observe: function(target, options) {
target = wrapIfNeeded(target);
// 1.1
if (!options.childList && !options.attributes && !options.characterData ||
// 1.2
options.attributeOldValue && !options.attributes ||
// 1.3
options.attributeFilter && options.attributeFilter.length &&
!options.attributes ||
// 1.4
options.characterDataOldValue && !options.characterData) {
throw new SyntaxError();
}
var registrations = registrationsTable.get(target);
if (!registrations)
registrationsTable.set(target, registrations = []);
// 2
// If target's list of registered observers already includes a registered
// observer associated with the context object, replace that registered
// observer's options with options.
var registration;
for (var i = 0; i < registrations.length; i++) {
if (registrations[i].observer === this) {
registration = registrations[i];
registration.removeListeners();
registration.options = options;
break;
}
}
// 3.
// Otherwise, add a new registered observer to target's list of registered
// observers with the context object as the observer and options as the
// options, and add target to context object's list of nodes on which it
// is registered.
if (!registration) {
registration = new Registration(this, target, options);
registrations.push(registration);
this.nodes_.push(target);
}
registration.addListeners();
},
disconnect: function() {
this.nodes_.forEach(function(node) {
var registrations = registrationsTable.get(node);
for (var i = 0; i < registrations.length; i++) {
var registration = registrations[i];
if (registration.observer === this) {
registration.removeListeners();
registrations.splice(i, 1);
// Each node can only have one registered observer associated with
// this observer.
break;
}
}
}, this);
this.records_ = [];
},
takeRecords: function() {
var copyOfRecords = this.records_;
this.records_ = [];
return copyOfRecords;
}
};
/**
* @param {string} type
* @param {Node} target
* @constructor
*/
function MutationRecord(type, target) {
this.type = type;
this.target = target;
this.addedNodes = [];
this.removedNodes = [];
this.previousSibling = null;
this.nextSibling = null;
this.attributeName = null;
this.attributeNamespace = null;
this.oldValue = null;
}
function copyMutationRecord(original) {
var record = new MutationRecord(original.type, original.target);
record.addedNodes = original.addedNodes.slice();
record.removedNodes = original.removedNodes.slice();
record.previousSibling = original.previousSibling;
record.nextSibling = original.nextSibling;
record.attributeName = original.attributeName;
record.attributeNamespace = original.attributeNamespace;
record.oldValue = original.oldValue;
return record;
};
// We keep track of the two (possibly one) records used in a single mutation.
var currentRecord, recordWithOldValue;
/**
* Creates a record without |oldValue| and caches it as |currentRecord| for
* later use.
* @param {string} oldValue
* @return {MutationRecord}
*/
function getRecord(type, target) {
return currentRecord = new MutationRecord(type, target);
}
/**
* Gets or creates a record with |oldValue| based in the |currentRecord|
* @param {string} oldValue
* @return {MutationRecord}
*/
function getRecordWithOldValue(oldValue) {
if (recordWithOldValue)
return recordWithOldValue;
recordWithOldValue = copyMutationRecord(currentRecord);
recordWithOldValue.oldValue = oldValue;
return recordWithOldValue;
}
function clearRecords() {
currentRecord = recordWithOldValue = undefined;
}
/**
* @param {MutationRecord} record
* @return {boolean} Whether the record represents a record from the current
* mutation event.
*/
function recordRepresentsCurrentMutation(record) {
return record === recordWithOldValue || record === currentRecord;
}
/**
* Selects which record, if any, to replace the last record in the queue.
* This returns |null| if no record should be replaced.
*
* @param {MutationRecord} lastRecord
* @param {MutationRecord} newRecord
* @param {MutationRecord}
*/
function selectRecord(lastRecord, newRecord) {
if (lastRecord === newRecord)
return lastRecord;
// Check if the the record we are adding represents the same record. If
// so, we keep the one with the oldValue in it.
if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord))
return recordWithOldValue;
return null;
}
/**
* Class used to represent a registered observer.
* @param {MutationObserver} observer
* @param {Node} target
* @param {MutationObserverInit} options
* @constructor
*/
function Registration(observer, target, options) {
this.observer = observer;
this.target = target;
this.options = options;
this.transientObservedNodes = [];
}
Registration.prototype = {
enqueue: function(record) {
var records = this.observer.records_;
var length = records.length;
// There are cases where we replace the last record with the new record.
// For example if the record represents the same mutation we need to use
// the one with the oldValue. If we get same record (this can happen as we
// walk up the tree) we ignore the new record.
if (records.length > 0) {
var lastRecord = records[length - 1];
var recordToReplaceLast = selectRecord(lastRecord, record);
if (recordToReplaceLast) {
records[length - 1] = recordToReplaceLast;
return;
}
} else {
scheduleCallback(this.observer);
}
records[length] = record;
},
addListeners: function() {
this.addListeners_(this.target);
},
addListeners_: function(node) {
var options = this.options;
if (options.attributes)
node.addEventListener('DOMAttrModified', this, true);
if (options.characterData)
node.addEventListener('DOMCharacterDataModified', this, true);
if (options.childList)
node.addEventListener('DOMNodeInserted', this, true);
if (options.childList || options.subtree)
node.addEventListener('DOMNodeRemoved', this, true);
},
removeListeners: function() {
this.removeListeners_(this.target);
},
removeListeners_: function(node) {
var options = this.options;
if (options.attributes)
node.removeEventListener('DOMAttrModified', this, true);
if (options.characterData)
node.removeEventListener('DOMCharacterDataModified', this, true);
if (options.childList)
node.removeEventListener('DOMNodeInserted', this, true);
if (options.childList || options.subtree)
node.removeEventListener('DOMNodeRemoved', this, true);
},
/**
* Adds a transient observer on node. The transient observer gets removed
* next time we deliver the change records.
* @param {Node} node
*/
addTransientObserver: function(node) {
// Don't add transient observers on the target itself. We already have all
// the required listeners set up on the target.
if (node === this.target)
return;
this.addListeners_(node);
this.transientObservedNodes.push(node);
var registrations = registrationsTable.get(node);
if (!registrations)
registrationsTable.set(node, registrations = []);
// We know that registrations does not contain this because we already
// checked if node === this.target.
registrations.push(this);
},
removeTransientObservers: function() {
var transientObservedNodes = this.transientObservedNodes;
this.transientObservedNodes = [];
transientObservedNodes.forEach(function(node) {
// Transient observers are never added to the target.
this.removeListeners_(node);
var registrations = registrationsTable.get(node);
for (var i = 0; i < registrations.length; i++) {
if (registrations[i] === this) {
registrations.splice(i, 1);
// Each node can only have one registered observer associated with
// this observer.
break;
}
}
}, this);
},
handleEvent: function(e) {
// Stop propagation since we are managing the propagation manually.
// This means that other mutation events on the page will not work
// correctly but that is by design.
e.stopImmediatePropagation();
switch (e.type) {
case 'DOMAttrModified':
// http://dom.spec.whatwg.org/#concept-mo-queue-attributes
var name = e.attrName;
var namespace = e.relatedNode.namespaceURI;
var target = e.target;
// 1.
var record = new getRecord('attributes', target);
record.attributeName = name;
record.attributeNamespace = namespace;
// 2.
var oldValue =
e.attrChange === MutationEvent.ADDITION ? null : e.prevValue;
forEachAncestorAndObserverEnqueueRecord(target, function(options) {
// 3.1, 4.2
if (!options.attributes)
return;
// 3.2, 4.3
if (options.attributeFilter && options.attributeFilter.length &&
options.attributeFilter.indexOf(name) === -1 &&
options.attributeFilter.indexOf(namespace) === -1) {
return;
}
// 3.3, 4.4
if (options.attributeOldValue)
return getRecordWithOldValue(oldValue);
// 3.4, 4.5
return record;
});
break;
case 'DOMCharacterDataModified':
// http://dom.spec.whatwg.org/#concept-mo-queue-characterdata
var target = e.target;
// 1.
var record = getRecord('characterData', target);
// 2.
var oldValue = e.prevValue;
forEachAncestorAndObserverEnqueueRecord(target, function(options) {
// 3.1, 4.2
if (!options.characterData)
return;
// 3.2, 4.3
if (options.characterDataOldValue)
return getRecordWithOldValue(oldValue);
// 3.3, 4.4
return record;
});
break;
case 'DOMNodeRemoved':
this.addTransientObserver(e.target);
// Fall through.
case 'DOMNodeInserted':
// http://dom.spec.whatwg.org/#concept-mo-queue-childlist
var target = e.relatedNode;
var changedNode = e.target;
var addedNodes, removedNodes;
if (e.type === 'DOMNodeInserted') {
addedNodes = [changedNode];
removedNodes = [];
} else {
addedNodes = [];
removedNodes = [changedNode];
}
var previousSibling = changedNode.previousSibling;
var nextSibling = changedNode.nextSibling;
// 1.
var record = getRecord('childList', target);
record.addedNodes = addedNodes;
record.removedNodes = removedNodes;
record.previousSibling = previousSibling;
record.nextSibling = nextSibling;
forEachAncestorAndObserverEnqueueRecord(target, function(options) {
// 2.1, 3.2
if (!options.childList)
return;
// 2.2, 3.3
return record;
});
}
clearRecords();
}
};
global.JsMutationObserver = JsMutationObserver;
})(this);
@@ -0,0 +1,23 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Polymer project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Polymer, where such license applies only to those
patent claims, both currently owned or controlled by Google and acquired
in the future, licensable by Google that are necessarily infringed by
this implementation of Polymer. This grant does not include claims
that would be infringed only as a consequence of further modification of
this implementation. If you or your agent or exclusive licensee
institute or order or agree to the institution of patent litigation
against any entity (including a cross-claim or counterclaim in a
lawsuit) alleging that this implementation of Polymer or any code
incorporated within this implementation of Polymer constitutes
direct or contributory patent infringement, or inducement of patent
infringement, then any patent rights granted to you under this License
for this implementation of Polymer shall terminate as of the date
such litigation is filed.
@@ -0,0 +1,4 @@
MutationObservers
=================
Mutation Observers Polyfill
@@ -0,0 +1,8 @@
commit 5bed2236ab958adab07e227b22dd4b9a1c2d054d
Merge: 11438c9 acf52bc
Author: Daniel Freedman <dfreedm2@gmail.com>
Date: Wed Jun 5 11:46:06 2013 -0700
Merge pull request #8 from Polymer/master
6/5 master -> stable
@@ -0,0 +1,23 @@
{
"name": "MutationObservers",
"version": "0.0.0",
"description": "ERROR: No README.md file found!",
"main": "MutationObserver.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/Polymer/MutationObservers.git"
},
"author": "",
"license": "BSD",
"gitHead": "fffb15e43b267034671cb01521df35b7fa063b7a",
"devDependencies": {
"chai": "*",
"mocha": "*"
}
}
@@ -0,0 +1,276 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver attributes', function() {
test('attr', function() {
var div = document.createElement('div');
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true
});
div.setAttribute('a', 'A');
div.setAttribute('a', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
expectRecord(records[1], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
});
test('attr with oldValue', function() {
var div = document.createElement('div');
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
attributeOldValue: true
});
div.setAttribute('a', 'A');
div.setAttribute('a', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null,
oldValue: null
});
expectRecord(records[1], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null,
oldValue: 'A'
});
});
test('attr change in subtree should not genereate a record', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true
});
child.setAttribute('a', 'A');
child.setAttribute('a', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 0);
});
test('attr change, subtree', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
subtree: true
});
child.setAttribute('a', 'A');
child.setAttribute('a', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'a'
});
expectRecord(records[1], {
type: 'attributes',
target: child,
attributeName: 'a'
});
});
test('multiple observers on same target', function() {
var div = document.createElement('div');
var observer1 = new JsMutationObserver(function() {});
observer1.observe(div, {
attributes: true,
attributeOldValue: true
});
var observer2 = new JsMutationObserver(function() {});
observer2.observe(div, {
attributes: true,
attributeFilter: ['b']
});
div.setAttribute('a', 'A');
div.setAttribute('a', 'A2');
div.setAttribute('b', 'B');
var records = observer1.takeRecords();
assert.strictEqual(records.length, 3);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a'
});
expectRecord(records[1], {
type: 'attributes',
target: div,
attributeName: 'a',
oldValue: 'A'
});
expectRecord(records[2], {
type: 'attributes',
target: div,
attributeName: 'b'
});
records = observer2.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'b'
});
});
test('observer observes on different target', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(child, {
attributes: true
});
observer.observe(div, {
attributes: true,
subtree: true,
attributeOldValue: true
});
child.setAttribute('a', 'A');
child.setAttribute('a', 'A2');
child.setAttribute('b', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 3);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'a'
});
expectRecord(records[1], {
type: 'attributes',
target: child,
attributeName: 'a',
oldValue: 'A'
});
expectRecord(records[2], {
type: 'attributes',
target: child,
attributeName: 'b'
});
});
test('observing on the same node should update the options', function() {
var div = document.createElement('div');
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
attributeFilter: ['a']
});
observer.observe(div, {
attributes: true,
attributeFilter: ['b']
});
div.setAttribute('a', 'A');
div.setAttribute('b', 'B');
var records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'b'
});
});
test('disconnect should stop all events and empty the records', function() {
var div = document.createElement('div');
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
});
div.setAttribute('a', 'A');
observer.disconnect();
var records = observer.takeRecords();
assert.strictEqual(records.length, 0);
div.setAttribute('b', 'B');
records = observer.takeRecords();
assert.strictEqual(records.length, 0);
});
test('disconnect should not affect other observers', function() {
var div = document.createElement('div');
var observer1 = new JsMutationObserver(function() {});
observer1.observe(div, {
attributes: true,
});
var observer2 = new JsMutationObserver(function() {});
observer2.observe(div, {
attributes: true,
});
div.setAttribute('a', 'A');
observer1.disconnect();
var records1 = observer1.takeRecords();
assert.strictEqual(records1.length, 0);
var records2 = observer2.takeRecords();
assert.strictEqual(records2.length, 1);
expectRecord(records2[0], {
type: 'attributes',
target: div,
attributeName: 'a'
});
div.setAttribute('b', 'B');
records1 = observer1.takeRecords();
assert.strictEqual(records1.length, 0);
records2 = observer2.takeRecords();
assert.strictEqual(records2.length, 1);
expectRecord(records2[0], {
type: 'attributes',
target: div,
attributeName: 'b'
});
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver callback', function() {
test('One observer, two attribute changes', function(cont) {
var div = document.createElement('div');
var observer = new JsMutationObserver(function(records) {
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
expectRecord(records[1], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
cont();
});
observer.observe(div, {
attributes: true
});
div.setAttribute('a', 'A');
div.setAttribute('a', 'B');
});
test('nested changes', function(cont) {
var div = document.createElement('div');
var i = 0;
var observer = new JsMutationObserver(function(records) {
assert.strictEqual(records.length, 1);
if (i === 0) {
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
div.setAttribute('b', 'B');
i++;
} else {
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'b',
attributeNamespace: null
});
cont();
}
});
observer.observe(div, {
attributes: true
});
div.setAttribute('a', 'A');
});
});
@@ -0,0 +1,106 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver characterData', function() {
var testDiv;
setup(function() {
testDiv = document.body.appendChild(document.createElement('div'));
});
teardown(function() {
document.body.removeChild(testDiv);
});
test('characterData', function() {
var text = document.createTextNode('abc');
var observer = new JsMutationObserver(function() {});
observer.observe(text, {
characterData: true
});
text.data = 'def';
text.data = 'ghi';
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'characterData',
target: text
});
expectRecord(records[1], {
type: 'characterData',
target: text
});
});
test('characterData with old value', function() {
var text = testDiv.appendChild(document.createTextNode('abc'));
var observer = new JsMutationObserver(function() {});
observer.observe(text, {
characterData: true,
characterDataOldValue: true
});
text.data = 'def';
text.data = 'ghi';
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'characterData',
target: text,
oldValue: 'abc'
});
expectRecord(records[1], {
type: 'characterData',
target: text,
oldValue: 'def'
});
});
test('characterData change in subtree should not generate a record',
function() {
var div = document.createElement('div');
var text = div.appendChild(document.createTextNode('abc'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
characterData: true
});
text.data = 'def';
text.data = 'ghi';
var records = observer.takeRecords();
assert.strictEqual(records.length, 0);
});
test('characterData change in subtree',
function() {
var div = document.createElement('div');
var text = div.appendChild(document.createTextNode('abc'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
characterData: true,
subtree: true
});
text.data = 'def';
text.data = 'ghi';
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'characterData',
target: text
});
expectRecord(records[1], {
type: 'characterData',
target: text
});
});
});
@@ -0,0 +1,377 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver childList', function() {
var testDiv;
teardown(function() {
document.body.removeChild(testDiv);
});
var addedNodes, removedNodes;
setup(function() {
testDiv = document.body.appendChild(document.createElement('div'));
addedNodes = [];
removedNodes = [];
});
function mergeRecords(records) {
records.forEach(function(record) {
if (record.addedNodes)
addedNodes.push.apply(addedNodes, record.addedNodes);
if (record.removedNodes)
removedNodes.push.apply(removedNodes, record.removedNodes);
});
}
function assertAll(records, expectedProperties) {
records.forEach(function(record) {
for (var propertyName in expectedProperties) {
assert.strictEqual(record[propertyName], expectedProperties[propertyName]);
}
});
}
test('appendChild', function() {
var div = document.createElement('div');
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
var a = document.createElement('a');
var b = document.createElement('b');
div.appendChild(a);
div.appendChild(b);
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: div,
addedNodes: [a]
});
expectRecord(records[1], {
type: 'childList',
target: div,
addedNodes: [b],
previousSibling: a
});
});
test('insertBefore', function() {
var div = document.createElement('div');
var a = document.createElement('a');
var b = document.createElement('b');
var c = document.createElement('c');
div.appendChild(a);
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
div.insertBefore(b, a);
div.insertBefore(c, a);
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: div,
addedNodes: [b],
nextSibling: a
});
expectRecord(records[1], {
type: 'childList',
target: div,
addedNodes: [c],
nextSibling: a,
previousSibling: b
});
});
test('removeChild', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createElement('a'));
var b = div.appendChild(document.createElement('b'));
var c = div.appendChild(document.createElement('c'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
div.removeChild(b);
div.removeChild(a);
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: div,
removedNodes: [b],
nextSibling: c,
previousSibling: a
});
expectRecord(records[1], {
type: 'childList',
target: div,
removedNodes: [a],
nextSibling: c
});
});
test('Direct children', function() {
var div = testDiv.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
var a = document.createElement('a');
var b = document.createElement('b');
div.appendChild(a);
div.insertBefore(b, a);
div.removeChild(b);
var records = observer.takeRecords();
assert.strictEqual(records.length, 3);
expectRecord(records[0], {
type: 'childList',
target: div,
addedNodes: [a]
});
expectRecord(records[1], {
type: 'childList',
target: div,
nextSibling: a,
addedNodes: [b]
});
expectRecord(records[2], {
type: 'childList',
target: div,
nextSibling: a,
removedNodes: [b]
});
});
test('subtree', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(child, {
childList: true
});
var a = document.createTextNode('a');
var b = document.createTextNode('b');
child.appendChild(a);
child.insertBefore(b, a);
child.removeChild(b);
var records = observer.takeRecords();
assert.strictEqual(records.length, 3);
expectRecord(records[0], {
type: 'childList',
target: child,
addedNodes: [a]
});
expectRecord(records[1], {
type: 'childList',
target: child,
nextSibling: a,
addedNodes: [b]
});
expectRecord(records[2], {
type: 'childList',
target: child,
nextSibling: a,
removedNodes: [b]
});
});
test('both direct and subtree', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true,
subtree: true
});
observer.observe(child, {
childList: true
});
var a = document.createTextNode('a');
var b = document.createTextNode('b');
child.appendChild(a);
div.appendChild(b);
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: child,
addedNodes: [a]
});
expectRecord(records[1], {
type: 'childList',
target: div,
addedNodes: [b],
previousSibling: child
});
});
test('Append multiple at once at the end', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createTextNode('a'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
var df = document.createDocumentFragment();
var b = df.appendChild(document.createTextNode('b'));
var c = df.appendChild(document.createTextNode('c'));
var d = df.appendChild(document.createTextNode('d'));
div.appendChild(df);
var records = observer.takeRecords();
mergeRecords(records);
assertArrayEqual(addedNodes, [b, c, d]);
assertArrayEqual(removedNodes, []);
assertAll(records, {
type: 'childList',
target: div
});
});
test('Append multiple at once at the front', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createTextNode('a'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
var df = document.createDocumentFragment();
var b = df.appendChild(document.createTextNode('b'));
var c = df.appendChild(document.createTextNode('c'));
var d = df.appendChild(document.createTextNode('d'));
div.insertBefore(df, a);
var records = observer.takeRecords();
mergeRecords(records);
assertArrayEqual(addedNodes, [b, c, d]);
assertArrayEqual(removedNodes, []);
assertAll(records, {
type: 'childList',
target: div
});
});
test('Append multiple at once in the middle', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createTextNode('a'));
var b = div.appendChild(document.createTextNode('b'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
var df = document.createDocumentFragment();
var c = df.appendChild(document.createTextNode('c'));
var d = df.appendChild(document.createTextNode('d'));
div.insertBefore(df, b);
var records = observer.takeRecords();
mergeRecords(records);
assertArrayEqual(addedNodes, [c, d]);
assertArrayEqual(removedNodes, []);
assertAll(records, {
type: 'childList',
target: div
});
});
test('Remove all children', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createTextNode('a'));
var b = div.appendChild(document.createTextNode('b'));
var c = div.appendChild(document.createTextNode('c'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
div.innerHTML = '';
var records = observer.takeRecords();
mergeRecords(records);
assertArrayEqual(addedNodes, []);
assertArrayEqual(removedNodes, [a, b, c]);
assertAll(records, {
type: 'childList',
target: div
});
});
test('Replace all children using innerHTML', function() {
var div = testDiv.appendChild(document.createElement('div'));
var a = div.appendChild(document.createTextNode('a'));
var b = div.appendChild(document.createTextNode('b'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true
});
div.innerHTML = '<c></c><d></d>';
var c = div.firstChild;
var d = div.lastChild;
var records = observer.takeRecords();
mergeRecords(records);
assertArrayEqual(addedNodes, [c, d]);
assertArrayEqual(removedNodes, [a, b]);
assertAll(records, {
type: 'childList',
target: div
});
});
});
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<!--
Copyright 2012 The Polymer Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<meta charset="utf-8">
<link rel="stylesheet" href="../node_modules/mocha/mocha.css">
<script src="../node_modules/chai/chai.js"></script>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="sidetable.js"></script>
<script src="../MutationObserver.js"></script>
<script>
mocha.setup({
ui: 'tdd',
ignoreLeaks: true
});
var assert = chai.assert;
function assertArrayEqual(a, b, msg) {
assert.equal(a.length, b.length, msg);
for (var i = 0; i < a.length; i++) {
assert.equal(a[i], b[i], msg);
}
}
var expectRecord = (function() {
var useNative = /native/.test(location.search);
var isWebKit = /WebKit/.test(navigator.userAgent);
var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
if (useNative) {
JsMutationObserver =
window.MutationObserver || window.WebKitMutationObserver;
}
// addedNodes/removedNodes are broken in WebKit.
// https://bugs.webkit.org/show_bug.cgi?id=98921
function fixWebKitNodes(nodes) {
if (nodes === null && useNative && isWebKit)
return [];
return nodes;
}
return function expectRecord(record, expected) {
assert.strictEqual(record.type,
expected.type === undefined ? null : expected.type);
assert.strictEqual(record.target,
expected.target === undefined ? null : expected.target);
assertArrayEqual(fixWebKitNodes(record.addedNodes),
expected.addedNodes === undefined ? [] : expected.addedNodes);
assertArrayEqual(fixWebKitNodes(record.removedNodes),
expected.removedNodes === undefined ? [] : expected.removedNodes);
assert.strictEqual(record.previousSibling,
expected.previousSibling === undefined ?
null : expected.previousSibling);
assert.strictEqual(record.nextSibling,
expected.nextSibling === undefined ? null : expected.nextSibling);
assert.strictEqual(record.attributeName,
expected.attributeName === undefined ? null : expected.attributeName);
assert.strictEqual(record.attributeNamespace,
expected.attributeNamespace === undefined ?
null : expected.attributeNamespace);
assert.strictEqual(record.oldValue,
expected.oldValue === undefined ? null : expected.oldValue);
};
})();
</script>
<script src="attributes.js"></script>
<script src="characterData.js"></script>
<script src="childList.js"></script>
<script src="mixed.js"></script>
<script src="callback.js"></script>
<script src="transient.js"></script>
<div id="mocha"></div>
<script>
mocha.run();
</script>
@@ -0,0 +1,36 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver mixed types', function() {
test('attr and characterData', function() {
var div = document.createElement('div');
var text = div.appendChild(document.createTextNode('text'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
characterData: true,
subtree: true
});
div.setAttribute('a', 'A');
div.firstChild.data = 'changed';
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'attributes',
target: div,
attributeName: 'a',
attributeNamespace: null
});
expectRecord(records[1], {
type: 'characterData',
target: div.firstChild
});
});
});
@@ -0,0 +1,35 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is goverened by a BSD-style
* license that can be found in the LICENSE file.
*/
// SideTable is a weak map where possible. If WeakMap is not available the
// association is stored as an expando property.
var SideTable;
// TODO(arv): WeakMap does not allow for Node etc to be keys in Firefox
if (typeof WeakMap !== 'undefined' && navigator.userAgent.indexOf('Firefox/') < 0) {
SideTable = WeakMap;
} else {
(function() {
var defineProperty = Object.defineProperty;
var hasOwnProperty = Object.hasOwnProperty;
var counter = new Date().getTime() % 1e9;
SideTable = function() {
this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');
};
SideTable.prototype = {
set: function(key, value) {
defineProperty(key, this.name, {value: value, writable: true});
},
get: function(key) {
return hasOwnProperty.call(key, this.name) ? key[this.name] : undefined;
},
delete: function(key) {
this.set(key, undefined);
}
}
})();
}
@@ -0,0 +1,283 @@
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
suite('JsMutationObserver transient', function() {
var testDiv;
setup(function() {
testDiv = document.body.appendChild(document.createElement('div'));
});
teardown(function() {
document.body.removeChild(testDiv);
});
test('attr', function() {
var div = testDiv.appendChild(document.createElement('div'));
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
attributes: true,
subtree: true
});
div.removeChild(child);
child.setAttribute('a', 'A');
var records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'a',
attributeNamespace: null
});
child.setAttribute('b', 'B');
records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'b',
attributeNamespace: null
});
});
test('attr callback', function(cont) {
var div = testDiv.appendChild(document.createElement('div'));
var child = div.appendChild(document.createElement('div'));
var i = 0;
var observer = new JsMutationObserver(function(records) {
i++;
if (i > 1)
expect().fail();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'a',
attributeNamespace: null
});
// The transient observers are removed before the callback is called.
child.setAttribute('b', 'B');
records = observer.takeRecords();
assert.strictEqual(records.length, 0);
cont();
});
observer.observe(div, {
attributes: true,
subtree: true
});
div.removeChild(child);
child.setAttribute('a', 'A');
});
test('attr, make sure transient gets removed', function(cont) {
var div = testDiv.appendChild(document.createElement('div'));
var child = div.appendChild(document.createElement('div'));
var i = 0;
var observer = new JsMutationObserver(function(records) {
i++;
if (i > 1)
expect().fail();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'a',
attributeNamespace: null
});
step2();
});
observer.observe(div, {
attributes: true,
subtree: true
});
div.removeChild(child);
child.setAttribute('a', 'A');
function step2() {
var div2 = document.createElement('div');
var observer2 = new JsMutationObserver(function(records) {
i++;
if (i > 2)
expect().fail();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'attributes',
target: child,
attributeName: 'b',
attributeNamespace: null
});
cont();
});
observer2.observe(div2, {
attributes: true,
subtree: true,
});
div2.appendChild(child);
child.setAttribute('b', 'B');
}
});
test('characterData', function() {
var div = document.createElement('div');
var child = div.appendChild(document.createTextNode('text'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
characterData: true,
subtree: true
});
div.removeChild(child);
child.data = 'changed';
var records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'characterData',
target: child
});
child.data += ' again';
records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'characterData',
target: child
});
});
test('characterData callback', function(cont) {
var div = document.createElement('div');
var child = div.appendChild(document.createTextNode('text'));
var i = 0;
var observer = new JsMutationObserver(function(records) {
i++;
if (i > 1)
expect().fail();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'characterData',
target: child
});
// The transient observers are removed before the callback is called.
child.data += ' again';
records = observer.takeRecords();
assert.strictEqual(records.length, 0);
cont();
});
observer.observe(div, {
characterData: true,
subtree: true
});
div.removeChild(child);
child.data = 'changed';
});
test('childList', function() {
var div = testDiv.appendChild(document.createElement('div'));
var child = div.appendChild(document.createElement('div'));
var observer = new JsMutationObserver(function() {});
observer.observe(div, {
childList: true,
subtree: true
});
div.removeChild(child);
var grandChild = child.appendChild(document.createElement('span'));
var records = observer.takeRecords();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: div,
removedNodes: [child]
});
expectRecord(records[1], {
type: 'childList',
target: child,
addedNodes: [grandChild]
});
child.removeChild(grandChild);
records = observer.takeRecords();
assert.strictEqual(records.length, 1);
expectRecord(records[0], {
type: 'childList',
target: child,
removedNodes: [grandChild]
});
});
test('childList callback', function(cont) {
var div = testDiv.appendChild(document.createElement('div'));
var child = div.appendChild(document.createElement('div'));
var i = 0;
var observer = new JsMutationObserver(function(records) {
i++;
if (i > 1)
expect().fail();
assert.strictEqual(records.length, 2);
expectRecord(records[0], {
type: 'childList',
target: div,
removedNodes: [child]
});
expectRecord(records[1], {
type: 'childList',
target: child,
addedNodes: [grandChild]
});
// The transient observers are removed before the callback is called.
child.removeChild(grandChild);
records = observer.takeRecords();
assert.strictEqual(records.length, 0);
cont();
});
observer.observe(div, {
childList: true,
subtree: true
});
div.removeChild(child);
var grandChild = child.appendChild(document.createElement('span'));
});
});
+48 -4
View File
@@ -156,6 +156,7 @@
save: function () {
var self = this;
var defs = [];
observer.disconnect();
$('.oe_dirty').each(function (i, v) {
var $el = $(this);
// TODO: Add a queue with concurrency limit in webclient
@@ -338,10 +339,25 @@
$elements
.not('span, [data-oe-type]')
.each(function () {
var $this = $(this);
CKEDITOR.inline(this, self._config()).on('change', function () {
$this.addClass('oe_dirty');
self.trigger('change', this, null);
var node = this;
var $node = $(node);
var editor = CKEDITOR.inline(this, self._config());
editor.on('loaded', function () {
// CKEditor *still* performs DOM alterations after
// loaded has fired => defer observation to ensure no
// more initialization-related DOM alteration
setTimeout(function () {
observer.observe(node, {
childList: true,
attributes: true,
characterData: true,
subtree: true
});
}, 0);
});
$node.one('content_changed', function (e) {
$node.addClass('oe_dirty');
self.trigger('change');
});
});
},
@@ -855,6 +871,34 @@
}
};
var Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
var observer = new Observer(function (mutations) {
_(mutations).chain()
.filter(function (m) {
switch(m.type) {
case 'attributes':
// ignore cke_focus being added & removed from RTE root
// FIXME: what if snippets are configured by adding/removing classes on their root element?
return !$(m.target).hasClass('oe_editable');
case 'childList':
// <br type="_moz"> appears when focusing RTE in FF, ignore
return m.addedNodes.length !== 1 || m.addedNodes[0].nodeName !== 'BR';
default:
return true;
}
})
.map(function (m) {
var node = m.target;
while (node && !$(node).hasClass('oe_editable')) {
node = node.parentNode;
}
return node;
})
.compact()
.uniq()
.each(function (node) { $(node).trigger('content_changed'); })
});
var all_ready = null;
var dom_ready = $.Deferred();
$(dom_ready.resolve);
+3
View File
@@ -36,6 +36,9 @@
CKEDITOR.dtd.$editable = $.extend( {}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline);
</script>
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script>
<!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
<script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script>
</t>
<script type="text/javascript" src="/website/static/src/js/website.js"></script>