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

Unified Diff: pkg/shadow_dom/lib/shadow_dom.debug.js

Issue 24129004: fix shadowdom polyfill (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 3 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/pkg.status ('k') | pkg/shadow_dom/lib/shadow_dom.min.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/shadow_dom/lib/shadow_dom.debug.js
diff --git a/pkg/shadow_dom/lib/shadow_dom.debug.js b/pkg/shadow_dom/lib/shadow_dom.debug.js
index 07b283e64de2933b022ee187a427d77c69176556..b889a5ef7641e88b18def663b474fd422bcafc32 100644
--- a/pkg/shadow_dom/lib/shadow_dom.debug.js
+++ b/pkg/shadow_dom/lib/shadow_dom.debug.js
@@ -16,28 +16,1350 @@ if ((!HTMLElement.prototype.createShadowRoot &&
}
})();
+// Copyright 2012 Google Inc.
+//
+// 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.
+
+(function(global) {
+ 'use strict';
+
+ function detectObjectObserve() {
+ if (typeof Object.observe !== 'function' ||
+ typeof Array.observe !== 'function') {
+ return false;
+ }
+
+ var gotSplice = false;
+ function callback(records) {
+ if (records[0].type === 'splice' && records[1].type === 'splice')
+ gotSplice = true;
+ }
+
+ var test = [0];
+ Array.observe(test, callback);
+ test[1] = 1;
+ test.length = 0;
+ Object.deliverChangeRecords(callback);
+ return gotSplice;
+ }
+
+ var hasObserve = detectObjectObserve();
+
+ function detectEval() {
+ // don't test for eval if document has CSP securityPolicy object and we can see that
+ // eval is not supported. This avoids an error message in console even when the exception
+ // is caught
+ if (global.document &&
+ 'securityPolicy' in global.document &&
+ !global.document.securityPolicy.allowsEval) {
+ return false;
+ }
+
+ try {
+ var f = new Function('', 'return true;');
+ return f();
+ } catch (ex) {
+ return false;
+ }
+ }
+
+ var hasEval = detectEval();
+
+ function isIndex(s) {
+ return +s === s >>> 0;
+ }
+
+ function toNumber(s) {
+ return +s;
+ }
+
+ function isObject(obj) {
+ return obj === Object(obj);
+ }
+
+ var numberIsNaN = global.Number.isNaN || function isNaN(value) {
+ return typeof value === 'number' && global.isNaN(value);
+ }
+
+ function areSameValue(left, right) {
+ if (left === right)
+ return left !== 0 || 1 / left === 1 / right;
+ if (numberIsNaN(left) && numberIsNaN(right))
+ return true;
+
+ return left !== left && right !== right;
+ }
+
+ var createObject = ('__proto__' in {}) ?
+ function(obj) { return obj; } :
+ function(obj) {
+ var proto = obj.__proto__;
+ if (!proto)
+ return obj;
+ var newObject = Object.create(proto);
+ Object.getOwnPropertyNames(obj).forEach(function(name) {
+ Object.defineProperty(newObject, name,
+ Object.getOwnPropertyDescriptor(obj, name));
+ });
+ return newObject;
+ };
+
+ var identStart = '[\$_a-zA-Z]';
+ var identPart = '[\$_a-zA-Z0-9]';
+ var ident = identStart + '+' + identPart + '*';
+ var elementIndex = '(?:[0-9]|[1-9]+[0-9]+)';
+ var identOrElementIndex = '(?:' + ident + '|' + elementIndex + ')';
+ var path = '(?:' + identOrElementIndex + ')(?:\\s*\\.\\s*' + identOrElementIndex + ')*';
+ var pathRegExp = new RegExp('^' + path + '$');
+
+ function isPathValid(s) {
+ if (typeof s != 'string')
+ return false;
+ s = s.trim();
+
+ if (s == '')
+ return true;
+
+ if (s[0] == '.')
+ return false;
+
+ return pathRegExp.test(s);
+ }
+
+ var constructorIsPrivate = {};
+
+ function Path(s, privateToken) {
+ if (privateToken !== constructorIsPrivate)
+ throw Error('Use Path.get to retrieve path objects');
+
+ if (s.trim() == '')
+ return this;
+
+ if (isIndex(s)) {
+ this.push(s);
+ return this;
+ }
+
+ s.split(/\s*\.\s*/).filter(function(part) {
+ return part;
+ }).forEach(function(part) {
+ this.push(part);
+ }, this);
+
+ if (hasEval && !hasObserve && this.length) {
+ this.getValueFrom = this.compiledGetValueFromFn();
+ }
+ }
+
+ // TODO(rafaelw): Make simple LRU cache
+ var pathCache = {};
+
+ function getPath(pathString) {
+ if (pathString instanceof Path)
+ return pathString;
+
+ if (pathString == null)
+ pathString = '';
+
+ if (typeof pathString !== 'string')
+ pathString = String(pathString);
+
+ var path = pathCache[pathString];
+ if (path)
+ return path;
+ if (!isPathValid(pathString))
+ return invalidPath;
+ var path = new Path(pathString, constructorIsPrivate);
+ pathCache[pathString] = path;
+ return path;
+ }
+
+ Path.get = getPath;
+
+ Path.prototype = createObject({
+ __proto__: [],
+ valid: true,
+
+ toString: function() {
+ return this.join('.');
+ },
+
+ getValueFrom: function(obj, observedSet) {
+ for (var i = 0; i < this.length; i++) {
+ if (obj == null)
+ return;
+ if (observedSet)
+ observedSet.observe(obj);
+ obj = obj[this[i]];
+ }
+ return obj;
+ },
+
+ compiledGetValueFromFn: function() {
+ var accessors = this.map(function(ident) {
+ return isIndex(ident) ? '["' + ident + '"]' : '.' + ident;
+ });
+
+ var str = '';
+ var pathString = 'obj';
+ str += 'if (obj != null';
+ var i = 0;
+ for (; i < (this.length - 1); i++) {
+ var ident = this[i];
+ pathString += accessors[i];
+ str += ' &&\n ' + pathString + ' != null';
+ }
+ str += ')\n';
+
+ pathString += accessors[i];
+
+ str += ' return ' + pathString + ';\nelse\n return undefined;';
+ return new Function('obj', str);
+ },
+
+ setValueFrom: function(obj, value) {
+ if (!this.length)
+ return false;
+
+ for (var i = 0; i < this.length - 1; i++) {
+ if (!isObject(obj))
+ return false;
+ obj = obj[this[i]];
+ }
+
+ if (!isObject(obj))
+ return false;
+
+ obj[this[i]] = value;
+ return true;
+ }
+ });
+
+ var invalidPath = new Path('', constructorIsPrivate);
+ invalidPath.valid = false;
+ invalidPath.getValueFrom = invalidPath.setValueFrom = function() {};
+
+ var MAX_DIRTY_CHECK_CYCLES = 1000;
+
+ function dirtyCheck(observer) {
+ var cycles = 0;
+ while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check()) {
+ observer.report();
+ cycles++;
+ }
+ if (global.testingExposeCycleCount)
+ global.dirtyCheckCycleCount = cycles;
+ }
+
+ function objectIsEmpty(object) {
+ for (var prop in object)
+ return false;
+ return true;
+ }
+
+ function diffIsEmpty(diff) {
+ return objectIsEmpty(diff.added) &&
+ objectIsEmpty(diff.removed) &&
+ objectIsEmpty(diff.changed);
+ }
+
+ function diffObjectFromOldObject(object, oldObject) {
+ var added = {};
+ var removed = {};
+ var changed = {};
+ var oldObjectHas = {};
+
+ for (var prop in oldObject) {
+ var newValue = object[prop];
+
+ if (newValue !== undefined && newValue === oldObject[prop])
+ continue;
+
+ if (!(prop in object)) {
+ removed[prop] = undefined;
+ continue;
+ }
+
+ if (newValue !== oldObject[prop])
+ changed[prop] = newValue;
+ }
+
+ for (var prop in object) {
+ if (prop in oldObject)
+ continue;
+
+ added[prop] = object[prop];
+ }
+
+ if (Array.isArray(object) && object.length !== oldObject.length)
+ changed.length = object.length;
+
+ return {
+ added: added,
+ removed: removed,
+ changed: changed
+ };
+ }
+
+ function copyObject(object, opt_copy) {
+ var copy = opt_copy || (Array.isArray(object) ? [] : {});
+ for (var prop in object) {
+ copy[prop] = object[prop];
+ };
+ if (Array.isArray(object))
+ copy.length = object.length;
+ return copy;
+ }
+
+ function Observer(object, callback, target, token) {
+ this.closed = false;
+ this.object = object;
+ this.callback = callback;
+ // TODO(rafaelw): Hold this.target weakly when WeakRef is available.
+ this.target = target;
+ this.token = token;
+ this.reporting = true;
+ if (hasObserve) {
+ var self = this;
+ this.boundInternalCallback = function(records) {
+ self.internalCallback(records);
+ };
+ }
+
+ addToAll(this);
+ }
+
+ Observer.prototype = {
+ internalCallback: function(records) {
+ if (this.closed)
+ return;
+ if (this.reporting && this.check(records)) {
+ this.report();
+ if (this.testingResults)
+ this.testingResults.anyChanged = true;
+ }
+ },
+
+ close: function() {
+ if (this.closed)
+ return;
+ if (this.object && typeof this.object.close === 'function')
+ this.object.close();
+
+ this.disconnect();
+ this.object = undefined;
+ this.closed = true;
+ },
+
+ deliver: function(testingResults) {
+ if (this.closed)
+ return;
+ if (hasObserve) {
+ this.testingResults = testingResults;
+ Object.deliverChangeRecords(this.boundInternalCallback);
+ this.testingResults = undefined;
+ } else {
+ dirtyCheck(this);
+ }
+ },
+
+ report: function() {
+ if (!this.reporting)
+ return;
+
+ this.sync(false);
+ if (this.callback) {
+ this.reportArgs.push(this.token);
+ this.invokeCallback(this.reportArgs);
+ }
+ this.reportArgs = undefined;
+ },
+
+ invokeCallback: function(args) {
+ try {
+ this.callback.apply(this.target, args);
+ } catch (ex) {
+ Observer._errorThrownDuringCallback = true;
+ console.error('Exception caught during observer callback: ' + (ex.stack || ex));
+ }
+ },
+
+ reset: function() {
+ if (this.closed)
+ return;
+
+ if (hasObserve) {
+ this.reporting = false;
+ Object.deliverChangeRecords(this.boundInternalCallback);
+ this.reporting = true;
+ }
+
+ this.sync(true);
+ }
+ }
+
+ var collectObservers = !hasObserve || global.forceCollectObservers;
+ var allObservers;
+ Observer._allObserversCount = 0;
+
+ if (collectObservers) {
+ allObservers = [];
+ }
+
+ function addToAll(observer) {
+ if (!collectObservers)
+ return;
+
+ allObservers.push(observer);
+ Observer._allObserversCount++;
+ }
+
+ var runningMicrotaskCheckpoint = false;
+
+ var hasDebugForceFullDelivery = typeof Object.deliverAllChangeRecords == 'function';
+
+ global.Platform = global.Platform || {};
+
+ global.Platform.performMicrotaskCheckpoint = function() {
+ if (runningMicrotaskCheckpoint)
+ return;
+
+ if (hasDebugForceFullDelivery) {
+ Object.deliverAllChangeRecords();
+ return;
+ }
+
+ if (!collectObservers)
+ return;
+
+ runningMicrotaskCheckpoint = true;
+
+ var cycles = 0;
+ var results = {};
+
+ do {
+ cycles++;
+ var toCheck = allObservers;
+ allObservers = [];
+ results.anyChanged = false;
+
+ for (var i = 0; i < toCheck.length; i++) {
+ var observer = toCheck[i];
+ if (observer.closed)
+ continue;
+
+ if (hasObserve) {
+ observer.deliver(results);
+ } else if (observer.check()) {
+ results.anyChanged = true;
+ observer.report();
+ }
+
+ allObservers.push(observer);
+ }
+ } while (cycles < MAX_DIRTY_CHECK_CYCLES && results.anyChanged);
+
+ if (global.testingExposeCycleCount)
+ global.dirtyCheckCycleCount = cycles;
+
+ Observer._allObserversCount = allObservers.length;
+ runningMicrotaskCheckpoint = false;
+ };
+
+ if (collectObservers) {
+ global.Platform.clearObservers = function() {
+ allObservers = [];
+ };
+ }
+
+ function ObjectObserver(object, callback, target, token) {
+ Observer.call(this, object, callback, target, token);
+ this.connect();
+ this.sync(true);
+ }
+
+ ObjectObserver.prototype = createObject({
+ __proto__: Observer.prototype,
+
+ connect: function() {
+ if (hasObserve)
+ Object.observe(this.object, this.boundInternalCallback);
+ },
+
+ sync: function(hard) {
+ if (!hasObserve)
+ this.oldObject = copyObject(this.object);
+ },
+
+ check: function(changeRecords) {
+ var diff;
+ var oldValues;
+ if (hasObserve) {
+ if (!changeRecords)
+ return false;
+
+ oldValues = {};
+ diff = diffObjectFromChangeRecords(this.object, changeRecords,
+ oldValues);
+ } else {
+ oldValues = this.oldObject;
+ diff = diffObjectFromOldObject(this.object, this.oldObject);
+ }
+
+ if (diffIsEmpty(diff))
+ return false;
+
+ this.reportArgs =
+ [diff.added || {}, diff.removed || {}, diff.changed || {}];
+ this.reportArgs.push(function(property) {
+ return oldValues[property];
+ });
+
+ return true;
+ },
+
+ disconnect: function() {
+ if (!hasObserve)
+ this.oldObject = undefined;
+ else if (this.object)
+ Object.unobserve(this.object, this.boundInternalCallback);
+ }
+ });
+
+ function ArrayObserver(array, callback, target, token) {
+ if (!Array.isArray(array))
+ throw Error('Provided object is not an Array');
+ ObjectObserver.call(this, array, callback, target, token);
+ }
+
+ ArrayObserver.prototype = createObject({
+ __proto__: ObjectObserver.prototype,
+
+ connect: function() {
+ if (hasObserve)
+ Array.observe(this.object, this.boundInternalCallback);
+ },
+
+ sync: function() {
+ if (!hasObserve)
+ this.oldObject = this.object.slice();
+ },
+
+ check: function(changeRecords) {
+ var splices;
+ if (hasObserve) {
+ if (!changeRecords)
+ return false;
+ splices = projectArraySplices(this.object, changeRecords);
+ } else {
+ splices = calcSplices(this.object, 0, this.object.length,
+ this.oldObject, 0, this.oldObject.length);
+ }
+
+ if (!splices || !splices.length)
+ return false;
+
+ this.reportArgs = [splices];
+ return true;
+ }
+ });
+
+ ArrayObserver.applySplices = function(previous, current, splices) {
+ splices.forEach(function(splice) {
+ var spliceArgs = [splice.index, splice.removed.length];
+ var addIndex = splice.index;
+ while (addIndex < splice.index + splice.addedCount) {
+ spliceArgs.push(current[addIndex]);
+ addIndex++;
+ }
+
+ Array.prototype.splice.apply(previous, spliceArgs);
+ });
+ };
+
+ function ObservedSet(callback) {
+ this.arr = [];
+ this.callback = callback;
+ this.isObserved = true;
+ }
+
+ var objProto = Object.getPrototypeOf({});
+ var arrayProto = Object.getPrototypeOf([]);
+ ObservedSet.prototype = {
+ reset: function() {
+ this.isObserved = !this.isObserved;
+ },
+
+ observe: function(obj) {
+ if (!isObject(obj) || obj === objProto || obj === arrayProto)
+ return;
+ var i = this.arr.indexOf(obj);
+ if (i >= 0 && this.arr[i+1] === this.isObserved)
+ return;
+
+ if (i < 0) {
+ i = this.arr.length;
+ this.arr[i] = obj;
+ Object.observe(obj, this.callback);
+ }
+
+ this.arr[i+1] = this.isObserved;
+ this.observe(Object.getPrototypeOf(obj));
+ },
+
+ cleanup: function() {
+ var i = 0, j = 0;
+ var isObserved = this.isObserved;
+ while(j < this.arr.length) {
+ var obj = this.arr[j];
+ if (this.arr[j + 1] == isObserved) {
+ if (i < j) {
+ this.arr[i] = obj;
+ this.arr[i + 1] = isObserved;
+ }
+ i += 2;
+ } else {
+ Object.unobserve(obj, this.callback);
+ }
+ j += 2;
+ }
+
+ this.arr.length = i;
+ }
+ };
+
+ function PathObserver(object, path, callback, target, token, valueFn,
+ setValueFn) {
+ var path = path instanceof Path ? path : getPath(path);
+ if (!path || !path.length || !isObject(object)) {
+ this.value_ = path ? path.getValueFrom(object) : undefined;
+ this.value = valueFn ? valueFn(this.value_) : this.value_;
+ this.closed = true;
+ return;
+ }
+
+ Observer.call(this, object, callback, target, token);
+ this.valueFn = valueFn;
+ this.setValueFn = setValueFn;
+ this.path = path;
+
+ this.connect();
+ this.sync(true);
+ }
+
+ PathObserver.prototype = createObject({
+ __proto__: Observer.prototype,
+
+ connect: function() {
+ if (hasObserve)
+ this.observedSet = new ObservedSet(this.boundInternalCallback);
+ },
+
+ disconnect: function() {
+ this.value = undefined;
+ this.value_ = undefined;
+ if (this.observedSet) {
+ this.observedSet.reset();
+ this.observedSet.cleanup();
+ this.observedSet = undefined;
+ }
+ },
+
+ check: function() {
+ // Note: Extracting this to a member function for use here and below
+ // regresses dirty-checking path perf by about 25% =-(.
+ if (this.observedSet)
+ this.observedSet.reset();
+
+ this.value_ = this.path.getValueFrom(this.object, this.observedSet);
+
+ if (this.observedSet)
+ this.observedSet.cleanup();
+
+ if (areSameValue(this.value_, this.oldValue_))
+ return false;
+
+ this.value = this.valueFn ? this.valueFn(this.value_) : this.value_;
+ this.reportArgs = [this.value, this.oldValue];
+ return true;
+ },
+
+ sync: function(hard) {
+ if (hard) {
+ if (this.observedSet)
+ this.observedSet.reset();
+
+ this.value_ = this.path.getValueFrom(this.object, this.observedSet);
+ this.value = this.valueFn ? this.valueFn(this.value_) : this.value_;
+
+ if (this.observedSet)
+ this.observedSet.cleanup();
+ }
+
+ this.oldValue_ = this.value_;
+ this.oldValue = this.value;
+ },
+
+ setValue: function(newValue) {
+ if (!this.path)
+ return;
+ if (typeof this.setValueFn === 'function')
+ newValue = this.setValueFn(newValue);
+ this.path.setValueFrom(this.object, newValue);
+ }
+ });
+
+ function CompoundPathObserver(callback, target, token, valueFn) {
+ Observer.call(this, undefined, callback, target, token);
+ this.valueFn = valueFn;
+
+ this.observed = [];
+ this.values = [];
+ this.started = false;
+ }
+
+ CompoundPathObserver.prototype = createObject({
+ __proto__: PathObserver.prototype,
+
+ addPath: function(object, path) {
+ if (this.started)
+ throw Error('Cannot add more paths once started.');
+
+ var path = path instanceof Path ? path : getPath(path);
+ var value = path ? path.getValueFrom(object) : undefined;
+
+ this.observed.push(object, path);
+ this.values.push(value);
+ },
+
+ start: function() {
+ this.connect();
+ this.sync(true);
+ },
+
+ getValues: function() {
+ if (this.observedSet)
+ this.observedSet.reset();
+
+ var anyChanged = false;
+ for (var i = 0; i < this.observed.length; i = i+2) {
+ var path = this.observed[i+1];
+ if (!path)
+ continue;
+ var object = this.observed[i];
+ var value = path.getValueFrom(object, this.observedSet);
+ var oldValue = this.values[i/2];
+ if (!areSameValue(value, oldValue)) {
+ this.values[i/2] = value;
+ anyChanged = true;
+ }
+ }
+
+ if (this.observedSet)
+ this.observedSet.cleanup();
+
+ return anyChanged;
+ },
+
+ check: function() {
+ if (!this.getValues())
+ return;
+
+ this.value = this.valueFn(this.values);
+
+ if (areSameValue(this.value, this.oldValue))
+ return false;
+
+ this.reportArgs = [this.value, this.oldValue];
+ return true;
+ },
+
+ sync: function(hard) {
+ if (hard) {
+ this.getValues();
+ this.value = this.valueFn(this.values);
+ }
+
+ this.oldValue = this.value;
+ },
+
+ close: function() {
+ if (this.observed) {
+ for (var i = 0; i < this.observed.length; i = i + 2) {
+ var object = this.observed[i];
+ if (object && typeof object.close === 'function')
+ object.close();
+ }
+ this.observed = undefined;
+ this.values = undefined;
+ }
+
+ Observer.prototype.close.call(this);
+ }
+ });
+
+ var knownRecordTypes = {
+ 'new': true,
+ 'updated': true,
+ 'deleted': true
+ };
+
+ function notifyFunction(object, name) {
+ if (typeof Object.observe !== 'function')
+ return;
+
+ var notifier = Object.getNotifier(object);
+ return function(type, oldValue) {
+ var changeRecord = {
+ object: object,
+ type: type,
+ name: name
+ };
+ if (arguments.length === 2)
+ changeRecord.oldValue = oldValue;
+ notifier.notify(changeRecord);
+ }
+ }
+
+ // TODO(rafaelw): It should be possible for the Object.observe case to have
+ // every PathObserver used by defineProperty share a single Object.observe
+ // callback, and thus get() can simply call observer.deliver() and any changes
+ // to any dependent value will be observed.
+ PathObserver.defineProperty = function(object, name, descriptor) {
+ // TODO(rafaelw): Validate errors
+ var obj = descriptor.object;
+ var path = getPath(descriptor.path);
+ var notify = notifyFunction(object, name);
+
+ var observer = new PathObserver(obj, descriptor.path,
+ function(newValue, oldValue) {
+ if (notify)
+ notify('updated', oldValue);
+ }
+ );
+
+ Object.defineProperty(object, name, {
+ get: function() {
+ return path.getValueFrom(obj);
+ },
+ set: function(newValue) {
+ path.setValueFrom(obj, newValue);
+ },
+ configurable: true
+ });
+
+ return {
+ close: function() {
+ var oldValue = path.getValueFrom(obj);
+ if (notify)
+ observer.deliver();
+ observer.close();
+ Object.defineProperty(object, name, {
+ value: oldValue,
+ writable: true,
+ configurable: true
+ });
+ }
+ };
+ }
+
+ function diffObjectFromChangeRecords(object, changeRecords, oldValues) {
+ var added = {};
+ var removed = {};
+
+ for (var i = 0; i < changeRecords.length; i++) {
+ var record = changeRecords[i];
+ if (!knownRecordTypes[record.type]) {
+ console.error('Unknown changeRecord type: ' + record.type);
+ console.error(record);
+ continue;
+ }
+
+ if (!(record.name in oldValues))
+ oldValues[record.name] = record.oldValue;
+
+ if (record.type == 'updated')
+ continue;
+
+ if (record.type == 'new') {
+ if (record.name in removed)
+ delete removed[record.name];
+ else
+ added[record.name] = true;
+
+ continue;
+ }
+
+ // type = 'deleted'
+ if (record.name in added) {
+ delete added[record.name];
+ delete oldValues[record.name];
+ } else {
+ removed[record.name] = true;
+ }
+ }
+
+ for (var prop in added)
+ added[prop] = object[prop];
+
+ for (var prop in removed)
+ removed[prop] = undefined;
+
+ var changed = {};
+ for (var prop in oldValues) {
+ if (prop in added || prop in removed)
+ continue;
+
+ var newValue = object[prop];
+ if (oldValues[prop] !== newValue)
+ changed[prop] = newValue;
+ }
+
+ return {
+ added: added,
+ removed: removed,
+ changed: changed
+ };
+ }
+
+ function newSplice(index, removed, addedCount) {
+ return {
+ index: index,
+ removed: removed,
+ addedCount: addedCount
+ };
+ }
+
+ var EDIT_LEAVE = 0;
+ var EDIT_UPDATE = 1;
+ var EDIT_ADD = 2;
+ var EDIT_DELETE = 3;
+
+ function ArraySplice() {}
+
+ ArraySplice.prototype = {
+
+ // Note: This function is *based* on the computation of the Levenshtein
+ // "edit" distance. The one change is that "updates" are treated as two
+ // edits - not one. With Array splices, an update is really a delete
+ // followed by an add. By retaining this, we optimize for "keeping" the
+ // maximum array items in the original array. For example:
+ //
+ // 'xxxx123' -> '123yyyy'
+ //
+ // With 1-edit updates, the shortest path would be just to update all seven
+ // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
+ // leaves the substring '123' intact.
+ calcEditDistances: function(current, currentStart, currentEnd,
+ old, oldStart, oldEnd) {
+ // "Deletion" columns
+ var rowCount = oldEnd - oldStart + 1;
+ var columnCount = currentEnd - currentStart + 1;
+ var distances = new Array(rowCount);
+
+ // "Addition" rows. Initialize null column.
+ for (var i = 0; i < rowCount; i++) {
+ distances[i] = new Array(columnCount);
+ distances[i][0] = i;
+ }
+
+ // Initialize null row
+ for (var j = 0; j < columnCount; j++)
+ distances[0][j] = j;
+
+ for (var i = 1; i < rowCount; i++) {
+ for (var j = 1; j < columnCount; j++) {
+ if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1]))
+ distances[i][j] = distances[i - 1][j - 1];
+ else {
+ var north = distances[i - 1][j] + 1;
+ var west = distances[i][j - 1] + 1;
+ distances[i][j] = north < west ? north : west;
+ }
+ }
+ }
+
+ return distances;
+ },
+
+ // This starts at the final weight, and walks "backward" by finding
+ // the minimum previous weight recursively until the origin of the weight
+ // matrix.
+ spliceOperationsFromEditDistances: function(distances) {
+ var i = distances.length - 1;
+ var j = distances[0].length - 1;
+ var current = distances[i][j];
+ var edits = [];
+ while (i > 0 || j > 0) {
+ if (i == 0) {
+ edits.push(EDIT_ADD);
+ j--;
+ continue;
+ }
+ if (j == 0) {
+ edits.push(EDIT_DELETE);
+ i--;
+ continue;
+ }
+ var northWest = distances[i - 1][j - 1];
+ var west = distances[i - 1][j];
+ var north = distances[i][j - 1];
+
+ var min;
+ if (west < north)
+ min = west < northWest ? west : northWest;
+ else
+ min = north < northWest ? north : northWest;
+
+ if (min == northWest) {
+ if (northWest == current) {
+ edits.push(EDIT_LEAVE);
+ } else {
+ edits.push(EDIT_UPDATE);
+ current = northWest;
+ }
+ i--;
+ j--;
+ } else if (min == west) {
+ edits.push(EDIT_DELETE);
+ i--;
+ current = west;
+ } else {
+ edits.push(EDIT_ADD);
+ j--;
+ current = north;
+ }
+ }
+
+ edits.reverse();
+ return edits;
+ },
+
+ /**
+ * Splice Projection functions:
+ *
+ * A splice map is a representation of how a previous array of items
+ * was transformed into a new array of items. Conceptually it is a list of
+ * tuples of
+ *
+ * <index, removed, addedCount>
+ *
+ * which are kept in ascending index order of. The tuple represents that at
+ * the |index|, |removed| sequence of items were removed, and counting forward
+ * from |index|, |addedCount| items were added.
+ */
+
+ /**
+ * Lacking individual splice mutation information, the minimal set of
+ * splices can be synthesized given the previous state and final state of an
+ * array. The basic approach is to calculate the edit distance matrix and
+ * choose the shortest path through it.
+ *
+ * Complexity: O(l * p)
+ * l: The length of the current array
+ * p: The length of the old array
+ */
+ calcSplices: function(current, currentStart, currentEnd,
+ old, oldStart, oldEnd) {
+ var prefixCount = 0;
+ var suffixCount = 0;
+
+ var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
+ if (currentStart == 0 && oldStart == 0)
+ prefixCount = this.sharedPrefix(current, old, minLength);
+
+ if (currentEnd == current.length && oldEnd == old.length)
+ suffixCount = this.sharedSuffix(current, old, minLength - prefixCount);
+
+ currentStart += prefixCount;
+ oldStart += prefixCount;
+ currentEnd -= suffixCount;
+ oldEnd -= suffixCount;
+
+ if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0)
+ return [];
+
+ if (currentStart == currentEnd) {
+ var splice = newSplice(currentStart, [], 0);
+ while (oldStart < oldEnd)
+ splice.removed.push(old[oldStart++]);
+
+ return [ splice ];
+ } else if (oldStart == oldEnd)
+ return [ newSplice(currentStart, [], currentEnd - currentStart) ];
+
+ var ops = this.spliceOperationsFromEditDistances(
+ this.calcEditDistances(current, currentStart, currentEnd,
+ old, oldStart, oldEnd));
+
+ var splice = undefined;
+ var splices = [];
+ var index = currentStart;
+ var oldIndex = oldStart;
+ for (var i = 0; i < ops.length; i++) {
+ switch(ops[i]) {
+ case EDIT_LEAVE:
+ if (splice) {
+ splices.push(splice);
+ splice = undefined;
+ }
+
+ index++;
+ oldIndex++;
+ break;
+ case EDIT_UPDATE:
+ if (!splice)
+ splice = newSplice(index, [], 0);
+
+ splice.addedCount++;
+ index++;
+
+ splice.removed.push(old[oldIndex]);
+ oldIndex++;
+ break;
+ case EDIT_ADD:
+ if (!splice)
+ splice = newSplice(index, [], 0);
+
+ splice.addedCount++;
+ index++;
+ break;
+ case EDIT_DELETE:
+ if (!splice)
+ splice = newSplice(index, [], 0);
+
+ splice.removed.push(old[oldIndex]);
+ oldIndex++;
+ break;
+ }
+ }
+
+ if (splice) {
+ splices.push(splice);
+ }
+ return splices;
+ },
+
+ sharedPrefix: function(current, old, searchLength) {
+ for (var i = 0; i < searchLength; i++)
+ if (!this.equals(current[i], old[i]))
+ return i;
+ return searchLength;
+ },
+
+ sharedSuffix: function(current, old, searchLength) {
+ var index1 = current.length;
+ var index2 = old.length;
+ var count = 0;
+ while (count < searchLength && this.equals(current[--index1], old[--index2]))
+ count++;
+
+ return count;
+ },
+
+ calculateSplices: function(current, previous) {
+ return this.calcSplices(current, 0, current.length, previous, 0,
+ previous.length);
+ },
+
+ equals: function(currentValue, previousValue) {
+ return currentValue === previousValue;
+ }
+ };
+
+ var arraySplice = new ArraySplice();
+
+ function calcSplices(current, currentStart, currentEnd,
+ old, oldStart, oldEnd) {
+ return arraySplice.calcSplices(current, currentStart, currentEnd,
+ old, oldStart, oldEnd);
+ }
+
+ function intersect(start1, end1, start2, end2) {
+ // Disjoint
+ if (end1 < start2 || end2 < start1)
+ return -1;
+
+ // Adjacent
+ if (end1 == start2 || end2 == start1)
+ return 0;
+
+ // Non-zero intersect, span1 first
+ if (start1 < start2) {
+ if (end1 < end2)
+ return end1 - start2; // Overlap
+ else
+ return end2 - start2; // Contained
+ } else {
+ // Non-zero intersect, span2 first
+ if (end2 < end1)
+ return end2 - start1; // Overlap
+ else
+ return end1 - start1; // Contained
+ }
+ }
+
+ function mergeSplice(splices, index, removed, addedCount) {
+
+ var splice = newSplice(index, removed, addedCount);
+
+ var inserted = false;
+ var insertionOffset = 0;
+
+ for (var i = 0; i < splices.length; i++) {
+ var current = splices[i];
+ current.index += insertionOffset;
+
+ if (inserted)
+ continue;
+
+ var intersectCount = intersect(splice.index,
+ splice.index + splice.removed.length,
+ current.index,
+ current.index + current.addedCount);
+
+ if (intersectCount >= 0) {
+ // Merge the two splices
+
+ splices.splice(i, 1);
+ i--;
+
+ insertionOffset -= current.addedCount - current.removed.length;
+
+ splice.addedCount += current.addedCount - intersectCount;
+ var deleteCount = splice.removed.length +
+ current.removed.length - intersectCount;
+
+ if (!splice.addedCount && !deleteCount) {
+ // merged splice is a noop. discard.
+ inserted = true;
+ } else {
+ var removed = current.removed;
+
+ if (splice.index < current.index) {
+ // some prefix of splice.removed is prepended to current.removed.
+ var prepend = splice.removed.slice(0, current.index - splice.index);
+ Array.prototype.push.apply(prepend, removed);
+ removed = prepend;
+ }
+
+ if (splice.index + splice.removed.length > current.index + current.addedCount) {
+ // some suffix of splice.removed is appended to current.removed.
+ var append = splice.removed.slice(current.index + current.addedCount - splice.index);
+ Array.prototype.push.apply(removed, append);
+ }
+
+ splice.removed = removed;
+ if (current.index < splice.index) {
+ splice.index = current.index;
+ }
+ }
+ } else if (splice.index < current.index) {
+ // Insert splice here.
+
+ inserted = true;
+
+ splices.splice(i, 0, splice);
+ i++;
+
+ var offset = splice.addedCount - splice.removed.length
+ current.index += offset;
+ insertionOffset += offset;
+ }
+ }
+
+ if (!inserted)
+ splices.push(splice);
+ }
+
+ function createInitialSplices(array, changeRecords) {
+ var splices = [];
+
+ for (var i = 0; i < changeRecords.length; i++) {
+ var record = changeRecords[i];
+ switch(record.type) {
+ case 'splice':
+ mergeSplice(splices, record.index, record.removed.slice(), record.addedCount);
+ break;
+ case 'new':
+ case 'updated':
+ case 'deleted':
+ if (!isIndex(record.name))
+ continue;
+ var index = toNumber(record.name);
+ if (index < 0)
+ continue;
+ mergeSplice(splices, index, [record.oldValue], 1);
+ break;
+ default:
+ console.error('Unexpected record type: ' + JSON.stringify(record));
+ break;
+ }
+ }
+
+ return splices;
+ }
+
+ function projectArraySplices(array, changeRecords) {
+ var splices = [];
+
+ createInitialSplices(array, changeRecords).forEach(function(splice) {
+ if (splice.addedCount == 1 && splice.removed.length == 1) {
+ if (splice.removed[0] !== array[splice.index])
+ splices.push(splice);
+
+ return
+ };
+
+ splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount,
+ splice.removed, 0, splice.removed.length));
+ });
+
+ return splices;
+ }
+
+ global.Observer = Observer;
+ global.Observer.hasObjectObserve = hasObserve;
+ global.ArrayObserver = ArrayObserver;
+ global.ArrayObserver.calculateSplices = function(current, previous) {
+ return arraySplice.calculateSplices(current, previous);
+ };
+
+ global.ArraySplice = ArraySplice;
+ global.ObjectObserver = ObjectObserver;
+ global.PathObserver = PathObserver;
+ global.CompoundPathObserver = CompoundPathObserver;
+ global.Path = Path;
+})(typeof global !== 'undefined' && global ? global : this);
+
/*
* Copyright 2012 The Polymer Authors. All rights reserved.
- * Use of this source code is goverened by a BSD-style
+ * Use of this source code is governed 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;
+// If WeakMap is not available, the association is stored as an expando property on the "key".
// 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 {
+if (typeof WeakMap === 'undefined' || navigator.userAgent.indexOf('Firefox/') > -1) {
(function() {
var defineProperty = Object.defineProperty;
var counter = Date.now() % 1e9;
- SideTable = function() {
+ var WeakMap = function() {
this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');
};
- SideTable.prototype = {
+ WeakMap.prototype = {
set: function(key, value) {
var entry = key[this.name];
if (entry && entry[0] === key)
@@ -53,7 +1375,9 @@ if (typeof WeakMap !== 'undefined' && navigator.userAgent.indexOf('Firefox/') <
delete: function(key) {
this.set(key, undefined);
}
- }
+ };
+
+ window.WeakMap = WeakMap;
})();
}
@@ -66,11 +1390,23 @@ var ShadowDOMPolyfill = {};
(function(scope) {
'use strict';
- var wrapperTable = new SideTable();
- var constructorTable = new SideTable();
- var nativePrototypeTable = new SideTable();
+ var constructorTable = new WeakMap();
+ var nativePrototypeTable = new WeakMap();
var wrappers = Object.create(null);
+ // Don't test for eval if document has CSP securityPolicy object and we can
+ // see that eval is not supported. This avoids an error message in console
+ // even when the exception is caught
+ var hasEval = !('securityPolicy' in document) ||
+ document.securityPolicy.allowsEval;
+ if (hasEval) {
+ try {
+ var f = new Function('', 'return true;');
+ hasEval = f();
+ } catch (ex) {
+ }
+ }
+
function assert(b) {
if (!b)
throw new Error('Assertion failed');
@@ -150,6 +1486,25 @@ var ShadowDOMPolyfill = {};
return /^on[a-z]+$/.test(name);
}
+ function getGetter(name) {
+ return hasEval ?
+ new Function('return this.impl.' + name) :
+ function() { return this.impl[name]; };
+ }
+
+ function getSetter(name) {
+ return hasEval ?
+ new Function('v', 'this.impl.' + name + ' = v') :
+ function(v) { this.impl[name] = v; };
+ }
+
+ function getMethod(name) {
+ return hasEval ?
+ new Function('return this.impl.' + name +
+ '.apply(this.impl, arguments)') :
+ function() { return this.impl[name].apply(this.impl, arguments); };
+ }
+
function installProperty(source, target, allowMethod) {
Object.getOwnPropertyNames(source).forEach(function(name) {
if (name in target)
@@ -170,29 +1525,21 @@ var ShadowDOMPolyfill = {};
}
var getter, setter;
if (allowMethod && typeof descriptor.value === 'function') {
- target[name] = function() {
- return this.impl[name].apply(this.impl, arguments);
- };
+ target[name] = getMethod(name);
return;
}
var isEvent = isEventHandlerName(name);
- if (isEvent) {
+ if (isEvent)
getter = scope.getEventHandlerGetter(name);
- } else {
- getter = function() {
- return this.impl[name];
- };
- }
+ else
+ getter = getGetter(name);
if (descriptor.writable || descriptor.set) {
- if (isEvent) {
+ if (isEvent)
setter = scope.getEventHandlerSetter(name);
- } else {
- setter = function(value) {
- this.impl[name] = value;
- };
- }
+ else
+ setter = getSetter(name);
}
Object.defineProperty(target, name, {
@@ -296,10 +1643,8 @@ var ShadowDOMPolyfill = {};
return null;
assert(isNative(impl));
- var wrapper = wrapperTable.get(impl);
- if (!wrapper)
- wrapperTable.set(impl, wrapper = new (getWrapperConstructor(impl))(impl));
- return wrapper;
+ return impl.polymerWrapper_ ||
+ (impl.polymerWrapper_ = new (getWrapperConstructor(impl))(impl));
}
/**
@@ -343,7 +1688,7 @@ var ShadowDOMPolyfill = {};
return;
assert(isNative(node));
assert(wrapper === undefined || isWrapper(wrapper));
- wrapperTable.set(node, wrapper);
+ node.polymerWrapper_ = wrapper;
}
function defineGetter(constructor, name, getter) {
@@ -411,17 +1756,17 @@ var ShadowDOMPolyfill = {};
var wrap = scope.wrap;
var wrappers = scope.wrappers;
- var wrappedFuns = new SideTable();
- var listenersTable = new SideTable();
- var handledEventsTable = new SideTable();
- var targetTable = new SideTable();
- var currentTargetTable = new SideTable();
- var relatedTargetTable = new SideTable();
- var eventPhaseTable = new SideTable();
- var stopPropagationTable = new SideTable();
- var stopImmediatePropagationTable = new SideTable();
- var eventHandlersTable = new SideTable();
- var eventPathTable = new SideTable();
+ var wrappedFuns = new WeakMap();
+ var listenersTable = new WeakMap();
+ var handledEventsTable = new WeakMap();
+ var targetTable = new WeakMap();
+ var currentTargetTable = new WeakMap();
+ var relatedTargetTable = new WeakMap();
+ var eventPhaseTable = new WeakMap();
+ var stopPropagationTable = new WeakMap();
+ var stopImmediatePropagationTable = new WeakMap();
+ var eventHandlersTable = new WeakMap();
+ var eventPathTable = new WeakMap();
function isShadowRoot(node) {
return node instanceof wrappers.ShadowRoot;
@@ -578,7 +1923,6 @@ var ShadowDOMPolyfill = {};
return enclosedBy(rootOfNode(host), b);
}
return false;
-
}
function isMutationEvent(type) {
@@ -1042,7 +2386,6 @@ var ShadowDOMPolyfill = {};
}
},
dispatchEvent: function(event) {
- scope.renderAllPending();
var target = getTargetToListenAt(this);
return target.dispatchEvent_(unwrap(event));
}
@@ -1211,7 +2554,7 @@ var ShadowDOMPolyfill = {};
* This updates the internal pointers for node, previousNode and nextNode.
*/
function collectNodes(node, parentNode, previousNode, nextNode) {
- if (node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
+ if (!(node instanceof DocumentFragment)) {
if (node.parentNode)
node.parentNode.removeChild(node);
node.parentNode_ = parentNode;
@@ -1245,6 +2588,24 @@ var ShadowDOMPolyfill = {};
return nodes;
}
+ function collectNodesNoNeedToUpdatePointers(node) {
+ if (node instanceof DocumentFragment) {
+ var nodes = [];
+ var i = 0;
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ nodes[i++] = child;
+ }
+ return nodes;
+ }
+ return [node];
+ }
+
+ function nodesWereAdded(nodes) {
+ for (var i = 0; i < nodes.length; i++) {
+ nodes[i].nodeWasAdded_();
+ }
+ }
+
function ensureSameOwnerDocument(parent, child) {
var ownerDoc = parent.nodeType === Node.DOCUMENT_NODE ?
parent : parent.ownerDocument;
@@ -1373,10 +2734,12 @@ var ShadowDOMPolyfill = {};
appendChild: function(childWrapper) {
assertIsNodeWrapper(childWrapper);
+ var nodes;
+
if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) {
var previousNode = this.lastChild;
var nextNode = null;
- var nodes = collectNodes(childWrapper, this, previousNode, nextNode);
+ nodes = collectNodes(childWrapper, this, previousNode, nextNode);
this.lastChild_ = nodes[nodes.length - 1];
if (!previousNode)
@@ -1384,11 +2747,12 @@ var ShadowDOMPolyfill = {};
originalAppendChild.call(this.impl, unwrapNodesForInsertion(this, nodes));
} else {
+ nodes = collectNodesNoNeedToUpdatePointers(childWrapper)
ensureSameOwnerDocument(this, childWrapper);
originalAppendChild.call(this.impl, unwrap(childWrapper));
}
- childWrapper.nodeWasAdded_();
+ nodesWereAdded(nodes);
return childWrapper;
},
@@ -1402,10 +2766,12 @@ var ShadowDOMPolyfill = {};
assertIsNodeWrapper(refWrapper);
assert(refWrapper.parentNode === this);
+ var nodes;
+
if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) {
var previousNode = refWrapper.previousSibling;
var nextNode = refWrapper;
- var nodes = collectNodes(childWrapper, this, previousNode, nextNode);
+ nodes = collectNodes(childWrapper, this, previousNode, nextNode);
if (this.firstChild === refWrapper)
this.firstChild_ = nodes[0];
@@ -1423,12 +2789,13 @@ var ShadowDOMPolyfill = {};
adoptNodesIfNeeded(this, nodes);
}
} else {
+ nodes = collectNodesNoNeedToUpdatePointers(childWrapper);
ensureSameOwnerDocument(this, childWrapper);
originalInsertBefore.call(this.impl, unwrap(childWrapper),
unwrap(refWrapper));
}
- childWrapper.nodeWasAdded_();
+ nodesWereAdded(nodes);
return childWrapper;
},
@@ -1485,6 +2852,7 @@ var ShadowDOMPolyfill = {};
}
var oldChildNode = unwrap(oldChildWrapper);
+ var nodes;
if (this.invalidateShadowRenderer() ||
invalidateParent(newChildWrapper)) {
@@ -1492,8 +2860,7 @@ var ShadowDOMPolyfill = {};
var nextNode = oldChildWrapper.nextSibling;
if (nextNode === newChildWrapper)
nextNode = newChildWrapper.nextSibling;
- var nodes = collectNodes(newChildWrapper, this,
- previousNode, nextNode);
+ nodes = collectNodes(newChildWrapper, this, previousNode, nextNode);
if (this.firstChild === oldChildWrapper)
this.firstChild_ = nodes[0];
@@ -1511,12 +2878,13 @@ var ShadowDOMPolyfill = {};
oldChildNode);
}
} else {
+ nodes = collectNodesNoNeedToUpdatePointers(newChildWrapper);
ensureSameOwnerDocument(this, newChildWrapper);
originalReplaceChild.call(this.impl, unwrap(newChildWrapper),
oldChildNode);
}
- newChildWrapper.nodeWasAdded_();
+ nodesWereAdded(nodes);
return oldChildWrapper;
},
@@ -1635,19 +3003,10 @@ var ShadowDOMPolyfill = {};
// This only wraps, it therefore only operates on the composed DOM and not
// the logical DOM.
return originalCompareDocumentPosition.call(this.impl, unwrap(otherNode));
- },
-
- // TODO(jmesserly): this is a workaround for
- // https://github.com/Polymer/ShadowDOM/issues/200
- get ownerDocument() {
- scope.renderAllPending();
- return wrap(this.impl.ownerDocument);
}
});
- // TODO(jmesserly): this is commented out to workaround:
- // https://github.com/Polymer/ShadowDOM/issues/200
- //defineWrapGetter(Node, 'ownerDocument');
+ defineWrapGetter(Node, 'ownerDocument');
// We use a DocumentFragment as a base and then delete the properties of
// DocumentFragment.prototype from the wrapper Node. Since delete makes
@@ -1858,7 +3217,7 @@ var ShadowDOMPolyfill = {};
var registerWrapper = scope.registerWrapper;
var wrappers = scope.wrappers;
- var shadowRootTable = new SideTable();
+ var shadowRootTable = new WeakMap();
var OriginalElement = window.Element;
@@ -2224,8 +3583,8 @@ var ShadowDOMPolyfill = {};
var unwrap = scope.unwrap;
var wrap = scope.wrap;
- var contentTable = new SideTable();
- var templateContentsOwnerTable = new SideTable();
+ var contentTable = new WeakMap();
+ var templateContentsOwnerTable = new WeakMap();
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
function getTemplateContentsOwner(doc) {
@@ -2362,8 +3721,8 @@ var ShadowDOMPolyfill = {};
var setInnerHTML = scope.setInnerHTML;
var unwrap = scope.unwrap;
- var shadowHostTable = new SideTable();
- var nextOlderShadowTreeTable = new SideTable();
+ var shadowHostTable = new WeakMap();
+ var nextOlderShadowTreeTable = new WeakMap();
function ShadowRoot(hostWrapper) {
var node = unwrap(hostWrapper.impl.ownerDocument.createDocumentFragment());
@@ -2417,6 +3776,7 @@ var ShadowDOMPolyfill = {};
(function(scope) {
'use strict';
+ var Element = scope.wrappers.Element;
var HTMLContentElement = scope.wrappers.HTMLContentElement;
var HTMLShadowElement = scope.wrappers.HTMLShadowElement;
var Node = scope.wrappers.Node;
@@ -2460,80 +3820,60 @@ var ShadowDOMPolyfill = {};
updateWrapperDown(parentNodeWrapper);
}
- // This object groups DOM operations. This is supposed to be the DOM as the
- // browser/render tree sees it.
- // When changes are done to the visual DOM the logical DOM needs to be updated
- // to reflect the correct tree.
- function removeAllChildNodes(parentNodeWrapper) {
+ function insertBefore(parentNodeWrapper, newChildWrapper, refChildWrapper) {
var parentNode = unwrap(parentNodeWrapper);
- updateAllChildNodes(parentNodeWrapper);
- if (parentNode.firstChild)
- parentNode.textContent = '';
- }
+ var newChild = unwrap(newChildWrapper);
+ var refChild = refChildWrapper ? unwrap(refChildWrapper) : null;
- function appendChild(parentNodeWrapper, childWrapper) {
- var parentNode = unwrap(parentNodeWrapper);
- var child = unwrap(childWrapper);
- if (child.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
- updateAllChildNodes(childWrapper);
+ remove(newChildWrapper);
+ updateWrapperUpAndSideways(newChildWrapper);
- } else {
- remove(childWrapper);
- updateWrapperUpAndSideways(childWrapper);
- }
+ if (!refChildWrapper) {
+ parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild;
+ if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild)
+ parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild;
- parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild;
- if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild)
- parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild;
+ var lastChildWrapper = wrap(parentNode.lastChild);
+ if (lastChildWrapper)
+ lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling;
+ } else {
+ if (parentNodeWrapper.firstChild === refChildWrapper)
+ parentNodeWrapper.firstChild_ = refChildWrapper;
- var lastChildWrapper = wrap(parentNode.lastChild);
- if (lastChildWrapper) {
- lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling;
+ refChildWrapper.previousSibling_ = refChildWrapper.previousSibling;
}
- parentNode.appendChild(child);
- }
-
- function removeChild(parentNodeWrapper, childWrapper) {
- var parentNode = unwrap(parentNodeWrapper);
- var child = unwrap(childWrapper);
-
- updateWrapperUpAndSideways(childWrapper);
-
- if (childWrapper.previousSibling)
- childWrapper.previousSibling.nextSibling_ = childWrapper;
- if (childWrapper.nextSibling)
- childWrapper.nextSibling.previousSibling_ = childWrapper;
-
- if (parentNodeWrapper.lastChild === childWrapper)
- parentNodeWrapper.lastChild_ = childWrapper;
- if (parentNodeWrapper.firstChild === childWrapper)
- parentNodeWrapper.firstChild_ = childWrapper;
-
- parentNode.removeChild(child);
+ parentNode.insertBefore(newChild, refChild);
}
function remove(nodeWrapper) {
var node = unwrap(nodeWrapper)
var parentNode = node.parentNode;
- if (parentNode)
- removeChild(wrap(parentNode), nodeWrapper);
- }
+ if (!parentNode)
+ return;
+
+ var parentNodeWrapper = wrap(parentNode);
+ updateWrapperUpAndSideways(nodeWrapper);
- var distributedChildNodesTable = new SideTable();
- var eventParentsTable = new SideTable();
- var insertionParentTable = new SideTable();
- var rendererForHostTable = new SideTable();
- var shadowDOMRendererTable = new SideTable();
+ if (nodeWrapper.previousSibling)
+ nodeWrapper.previousSibling.nextSibling_ = nodeWrapper;
+ if (nodeWrapper.nextSibling)
+ nodeWrapper.nextSibling.previousSibling_ = nodeWrapper;
- var reprCounter = 0;
+ if (parentNodeWrapper.lastChild === nodeWrapper)
+ parentNodeWrapper.lastChild_ = nodeWrapper;
+ if (parentNodeWrapper.firstChild === nodeWrapper)
+ parentNodeWrapper.firstChild_ = nodeWrapper;
- function repr(node) {
- if (!node.displayName)
- node.displayName = node.nodeName + '-' + ++reprCounter;
- return node.displayName;
+ parentNode.removeChild(node);
}
+ var distributedChildNodesTable = new WeakMap();
+ var eventParentsTable = new WeakMap();
+ var insertionParentTable = new WeakMap();
+ var rendererForHostTable = new WeakMap();
+ var shadowDOMRendererTable = new WeakMap();
+
function distributeChildToInsertionPoint(child, insertionPoint) {
getDistributedChildNodes(insertionPoint).push(child);
assignToInsertionPoint(child, insertionPoint);
@@ -2569,9 +3909,7 @@ var ShadowDOMPolyfill = {};
*/
function visit(tree, predicate, visitor) {
// This operates on logical DOM.
- var nodes = getChildNodesSnapshot(tree);
- for (var i = 0; i < nodes.length; i++) {
- var node = nodes[i];
+ for (var node = tree.firstChild; node; node = node.nextSibling) {
if (predicate(node)) {
if (visitor(node) === false)
return;
@@ -2622,14 +3960,14 @@ var ShadowDOMPolyfill = {};
if (!select)
return true;
- if (node.nodeType !== Node.ELEMENT_NODE)
+ if (!(node instanceof Element))
return false;
// TODO(arv): This does not seem right. Need to check for a simple selector.
if (!selectorMatchRegExp.test(select))
return false;
- if (select[0] === ':' &&!allowedPseudoRegExp.test(select))
+ if (select[0] === ':' && !allowedPseudoRegExp.test(select))
return false;
try {
@@ -2651,18 +3989,15 @@ var ShadowDOMPolyfill = {};
var renderTimer;
function renderAllPending() {
- renderTimer = null;
- pendingDirtyRenderers.forEach(function(owner) {
- owner.render();
- });
+ for (var i = 0; i < pendingDirtyRenderers.length; i++) {
+ pendingDirtyRenderers[i].render();
+ }
pendingDirtyRenderers = [];
}
- function ShadowRenderer(host) {
- this.host = host;
- this.dirty = false;
- this.invalidateAttributes();
- this.associateNode(host);
+ function handleRequestAnimationFrame() {
+ renderTimer = null;
+ renderAllPending();
}
/**
@@ -2691,10 +4026,92 @@ var ShadowDOMPolyfill = {};
return getRendererForHost(getHostForShadowRoot(shadowRoot));
}
+ var spliceDiff = new ArraySplice();
+ spliceDiff.equals = function(renderNode, rawNode) {
+ return unwrap(renderNode.node) === rawNode;
+ };
+
+ /**
+ * RenderNode is used as an in memory "render tree". When we render the
+ * composed tree we create a tree of RenderNodes, then we diff this against
+ * the real DOM tree and make minimal changes as needed.
+ */
+ function RenderNode(node) {
+ this.skip = false;
+ this.node = node;
+ this.childNodes = [];
+ }
+
+ RenderNode.prototype = {
+ append: function(node) {
+ var rv = new RenderNode(node);
+ this.childNodes.push(rv);
+ return rv;
+ },
+
+ sync: function(opt_added) {
+ if (this.skip)
+ return;
+
+ var nodeWrapper = this.node;
+ // plain array of RenderNodes
+ var newChildren = this.childNodes;
+ // plain array of real nodes.
+ var oldChildren = getChildNodesSnapshot(unwrap(nodeWrapper));
+ var added = opt_added || new WeakMap();
+
+ var splices = spliceDiff.calculateSplices(newChildren, oldChildren);
+
+ var newIndex = 0, oldIndex = 0;
+ var lastIndex = 0;
+ for (var i = 0; i < splices.length; i++) {
+ var splice = splices[i];
+ for (; lastIndex < splice.index; lastIndex++) {
+ oldIndex++;
+ newChildren[newIndex++].sync(added);
+ }
+
+ var removedCount = splice.removed.length;
+ for (var j = 0; j < removedCount; j++) {
+ var wrapper = wrap(oldChildren[oldIndex++]);
+ if (!added.get(wrapper))
+ remove(wrapper);
+ }
+
+ var addedCount = splice.addedCount;
+ var refNode = oldChildren[oldIndex] && wrap(oldChildren[oldIndex]);
+ for (var j = 0; j < addedCount; j++) {
+ var newChildRenderNode = newChildren[newIndex++];
+ var newChildWrapper = newChildRenderNode.node;
+ insertBefore(nodeWrapper, newChildWrapper, refNode);
+
+ // Keep track of added so that we do not remove the node after it
+ // has been added.
+ added.set(newChildWrapper, true);
+
+ newChildRenderNode.sync(added);
+ }
+
+ lastIndex += addedCount;
+ }
+
+ for (var i = lastIndex; i < newChildren.length; i++) {
+ newChildren[i++].sync(added);
+ }
+ }
+ };
+
+ function ShadowRenderer(host) {
+ this.host = host;
+ this.dirty = false;
+ this.invalidateAttributes();
+ this.associateNode(host);
+ }
+
ShadowRenderer.prototype = {
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees
- render: function() {
+ render: function(opt_renderNode) {
if (!this.dirty)
return;
@@ -2702,14 +4119,18 @@ var ShadowDOMPolyfill = {};
this.treeComposition();
var host = this.host;
- var shadowDOM = host.shadowRoot;
+ var shadowRoot = host.shadowRoot;
- this.removeAllChildNodes(this.host);
+ this.associateNode(host);
+ var topMostRenderer = !renderNode;
+ var renderNode = opt_renderNode || new RenderNode(host);
- var shadowDOMChildNodes = getChildNodesSnapshot(shadowDOM);
- shadowDOMChildNodes.forEach(function(node) {
- this.renderNode(host, shadowDOM, node, false);
- }, this);
+ for (var node = shadowRoot.firstChild; node; node = node.nextSibling) {
+ this.renderNode(shadowRoot, renderNode, node, false);
+ }
+
+ if (topMostRenderer)
+ renderNode.sync();
this.dirty = false;
},
@@ -2720,81 +4141,81 @@ var ShadowDOMPolyfill = {};
pendingDirtyRenderers.push(this);
if (renderTimer)
return;
- renderTimer = window[request](renderAllPending, 0);
+ renderTimer = window[request](handleRequestAnimationFrame, 0);
}
},
- renderNode: function(visualParent, tree, node, isNested) {
+ renderNode: function(shadowRoot, renderNode, node, isNested) {
if (isShadowHost(node)) {
- this.appendChild(visualParent, node);
+ renderNode = renderNode.append(node);
var renderer = getRendererForHost(node);
renderer.dirty = true; // Need to rerender due to reprojection.
- renderer.render();
+ renderer.render(renderNode);
} else if (isInsertionPoint(node)) {
- this.renderInsertionPoint(visualParent, tree, node, isNested);
+ this.renderInsertionPoint(shadowRoot, renderNode, node, isNested);
} else if (isShadowInsertionPoint(node)) {
- this.renderShadowInsertionPoint(visualParent, tree, node);
+ this.renderShadowInsertionPoint(shadowRoot, renderNode, node);
} else {
- this.renderAsAnyDomTree(visualParent, tree, node, isNested);
+ this.renderAsAnyDomTree(shadowRoot, renderNode, node, isNested);
}
},
- renderAsAnyDomTree: function(visualParent, tree, node, isNested) {
- this.appendChild(visualParent, node);
+ renderAsAnyDomTree: function(shadowRoot, renderNode, node, isNested) {
+ renderNode = renderNode.append(node);
if (isShadowHost(node)) {
- render(node);
+ var renderer = getRendererForHost(node);
+ renderNode.skip = !renderer.dirty;
+ renderer.render(renderNode);
} else {
- var parent = node;
- var logicalChildNodes = getChildNodesSnapshot(parent);
- // We associate the parent of a content/shadow with the renderer
- // because we may need to remove stale childNodes.
- if (shadowDOMRendererTable.get(parent))
- this.removeAllChildNodes(parent);
- logicalChildNodes.forEach(function(node) {
- this.renderNode(parent, tree, node, isNested);
- }, this);
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ this.renderNode(shadowRoot, renderNode, child, isNested);
+ }
}
},
- renderInsertionPoint: function(visualParent, tree, insertionPoint, isNested) {
+ renderInsertionPoint: function(shadowRoot, renderNode, insertionPoint,
+ isNested) {
var distributedChildNodes = getDistributedChildNodes(insertionPoint);
if (distributedChildNodes.length) {
- this.removeAllChildNodes(insertionPoint);
+ this.associateNode(insertionPoint);
- distributedChildNodes.forEach(function(child) {
+ for (var i = 0; i < distributedChildNodes.length; i++) {
+ var child = distributedChildNodes[i];
if (isInsertionPoint(child) && isNested)
- this.renderInsertionPoint(visualParent, tree, child, isNested);
+ this.renderInsertionPoint(shadowRoot, renderNode, child, isNested);
else
- this.renderAsAnyDomTree(visualParent, tree, child, isNested);
- }, this);
+ this.renderAsAnyDomTree(shadowRoot, renderNode, child, isNested);
+ }
} else {
- this.renderFallbackContent(visualParent, insertionPoint);
+ this.renderFallbackContent(shadowRoot, renderNode, insertionPoint);
}
- this.remove(insertionPoint);
+ this.associateNode(insertionPoint.parentNode);
},
- renderShadowInsertionPoint: function(visualParent, tree, shadowInsertionPoint) {
- var nextOlderTree = tree.olderShadowRoot;
+ renderShadowInsertionPoint: function(shadowRoot, renderNode,
+ shadowInsertionPoint) {
+ var nextOlderTree = shadowRoot.olderShadowRoot;
if (nextOlderTree) {
assignToInsertionPoint(nextOlderTree, shadowInsertionPoint);
- this.remove(shadowInsertionPoint);
- var shadowDOMChildNodes = getChildNodesSnapshot(nextOlderTree);
- shadowDOMChildNodes.forEach(function(node) {
- this.renderNode(visualParent, nextOlderTree, node, true);
- }, this);
+ this.associateNode(shadowInsertionPoint.parentNode);
+ for (var node = nextOlderTree.firstChild;
+ node;
+ node = node.nextSibling) {
+ this.renderNode(nextOlderTree, renderNode, node, true);
+ }
} else {
- this.renderFallbackContent(visualParent, shadowInsertionPoint);
+ this.renderFallbackContent(shadowRoot, renderNode,
+ shadowInsertionPoint);
}
},
- renderFallbackContent: function (visualParent, fallbackHost) {
- var logicalChildNodes = getChildNodesSnapshot(fallbackHost);
+ renderFallbackContent: function(shadowRoot, renderNode, fallbackHost) {
this.associateNode(fallbackHost);
- this.remove(fallbackHost);
- logicalChildNodes.forEach(function(node) {
- this.appendChild(visualParent, node);
- }, this);
+ this.associateNode(fallbackHost.parentNode);
+ for (var node = fallbackHost.firstChild; node; node = node.nextSibling) {
+ this.renderAsAnyDomTree(shadowRoot, renderNode, node, false);
+ }
},
/**
@@ -2837,7 +4258,6 @@ var ShadowDOMPolyfill = {};
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-distribution-algorithm
distribute: function(tree, pool) {
- var anyRemoved = false;
var self = this;
visit(tree, isActiveInsertionPoint,
@@ -2853,17 +4273,9 @@ var ShadowDOMPolyfill = {};
if (matchesCriteria(node, insertionPoint)) { // 1.2.2
distributeChildToInsertionPoint(node, insertionPoint); // 1.2.2.1
pool[i] = undefined; // 1.2.2.2
- anyRemoved = true;
}
}
});
-
- if (!anyRemoved)
- return pool;
-
- return pool.filter(function(item) {
- return item !== undefined;
- });
},
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-tree-composition
@@ -2871,8 +4283,10 @@ var ShadowDOMPolyfill = {};
var shadowHost = this.host;
var tree = shadowHost.shadowRoot; // 1.
var pool = []; // 2.
- var shadowHostChildNodes = getChildNodesSnapshot(shadowHost);
- shadowHostChildNodes.forEach(function(child) { // 3.
+
+ for (var child = shadowHost.firstChild;
+ child;
+ child = child.nextSibling) { // 3.
if (isInsertionPoint(child)) { // 3.2.
var reprojected = getDistributedChildNodes(child); // 3.2.1.
// if reprojected is undef... reset it?
@@ -2882,7 +4296,7 @@ var ShadowDOMPolyfill = {};
} else {
pool.push(child); // 3.3.
}
- });
+ }
var shadowInsertionPoint, point;
while (tree) { // 4.
@@ -2894,7 +4308,7 @@ var ShadowDOMPolyfill = {};
});
point = shadowInsertionPoint;
- pool = this.distribute(tree, pool); // 4.2.
+ this.distribute(tree, pool); // 4.2.
if (point) { // 4.3.
var nextOlderTree = tree.olderShadowRoot; // 4.3.1.
if (!nextOlderTree) {
@@ -2910,23 +4324,6 @@ var ShadowDOMPolyfill = {};
}
},
- appendChild: function(parent, child) {
- // this.associateNode(child);
- this.associateNode(parent);
- appendChild(parent, child);
- },
-
- remove: function(node) {
- // this.associateNode(node);
- this.associateNode(node.parentNode);
- remove(node);
- },
-
- removeAllChildNodes: function(parent) {
- this.associateNode(parent);
- removeAllChildNodes(parent);
- },
-
associateNode: function(node) {
shadowDOMRendererTable.set(node, this);
}
@@ -2934,21 +4331,21 @@ var ShadowDOMPolyfill = {};
function isInsertionPoint(node) {
// Should this include <shadow>?
- return node.localName === 'content';
+ return node instanceof HTMLContentElement;
}
function isActiveInsertionPoint(node) {
// <content> inside another <content> or <shadow> is considered inactive.
- return node.localName === 'content';
+ return node instanceof HTMLContentElement;
}
function isShadowInsertionPoint(node) {
- return node.localName === 'shadow';
+ return node instanceof HTMLShadowElement;
}
function isActiveShadowInsertionPoint(node) {
// <shadow> inside another <content> or <shadow> is considered inactive.
- return node.localName === 'shadow';
+ return node instanceof HTMLShadowElement;
}
function isShadowHost(shadowHost) {
@@ -3025,9 +4422,8 @@ var ShadowDOMPolyfill = {};
// Exposed for testing
scope.visual = {
- removeAllChildNodes: removeAllChildNodes,
- appendChild: appendChild,
- removeChild: removeChild
+ insertBefore: insertBefore,
+ remove: remove,
};
})(this.ShadowDOMPolyfill);
@@ -3109,7 +4505,7 @@ var ShadowDOMPolyfill = {};
var wrapEventTargetMethods = scope.wrapEventTargetMethods;
var wrapNodeList = scope.wrapNodeList;
- var implementationTable = new SideTable();
+ var implementationTable = new WeakMap();
function Document(node) {
Node.call(this, node);
@@ -3384,6 +4780,7 @@ var ShadowDOMPolyfill = {};
var unwrap = scope.unwrap;
var unwrapIfNeeded = scope.unwrapIfNeeded;
var wrap = scope.wrap;
+ var renderAllPending = scope.renderAllPending;
var OriginalWindow = window.Window;
@@ -3394,6 +4791,7 @@ var ShadowDOMPolyfill = {};
var originalGetComputedStyle = window.getComputedStyle;
OriginalWindow.prototype.getComputedStyle = function(el, pseudo) {
+ renderAllPending();
return originalGetComputedStyle.call(this || window, unwrapIfNeeded(el),
pseudo);
};
@@ -3745,11 +5143,9 @@ var ShadowDOMPolyfill = {};
// Fix up class names for Firefox.
// For some of them (like HTMLFormElement and HTMLInputElement),
// the "constructor" property of the unwrapped nodes points at the
- // wrapper.
- // Note: it is safe to check for the GeneratedWrapper string because
- // we know it is some kind of Shadow DOM wrapper object.
- var ctor = obj.constructor;
- if (ctor && ctor.name == 'GeneratedWrapper') {
+ // same constructor as the wrapper.
+ var ctor = obj.constructor
+ if (ctor === unwrapped.constructor) {
var name = ctor._ShadowDOMPolyfill$cacheTag_;
if (!name) {
name = Object.prototype.toString.call(unwrapped);
« no previous file with comments | « pkg/pkg.status ('k') | pkg/shadow_dom/lib/shadow_dom.min.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698