Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
740 lines (608 sloc)
20.4 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Watch Event Listener | |
| * | |
| * @author Darcy Clarke | |
| * | |
| * Copyright (c) 2014 Darcy Clarke | |
| * Dual licensed under the MIT and GPL licenses. | |
| * | |
| * Usage: | |
| * watch(element, 'width height', function(){ | |
| * console.log(this.style.width, this.style.height); | |
| * }); | |
| */ | |
| (function (window) { | |
| var _watch = function (elements, props, options, callback){ | |
| // Setup | |
| var self = this; | |
| var check; | |
| var toArray; | |
| // Object to Array | |
| toArray = function (obj) { | |
| var arr = []; | |
| for (var i = obj.length >>> 0; i--;) { | |
| arr[i] = obj[i]; | |
| } | |
| return arr; | |
| }; | |
| // Check if we should fire callback | |
| check = function (e) { | |
| var self = this; | |
| for (var i = 0; i < self.watching.length; i++) { | |
| var data = self.watching[i]; | |
| var changed = true; | |
| var temp; | |
| // Iterate through properties | |
| for (var j = 0; j < data.props.length; j++) { | |
| temp = self.attributes[data.props[j]] || self.style[data.props[j]]; | |
| if (data.vals[j] != temp) { | |
| data.vals[j] = temp; | |
| data.changed[j] = true; | |
| } | |
| } | |
| // Check changed attributes | |
| for (var k = 0; k < data.props.length; k++) { | |
| if (!data.changed[k]) { | |
| changed = false; | |
| break; | |
| } | |
| } | |
| // Run callback if property has changed | |
| if (changed && data.callback) { | |
| data.callback.apply(self, e); | |
| } | |
| }; | |
| }; | |
| // Elements from node list to array | |
| elements = toArray(elements); | |
| // Type check options | |
| if (typeof(options) == 'function') { | |
| callback = options; | |
| options = {}; | |
| } | |
| // Type check callback | |
| if (typeof(callback) != 'function') { | |
| callback = function(){}; | |
| } | |
| // Set throttle | |
| options.throttle = options.throttle || 10; | |
| // Iterate over elements | |
| for (var i = 0; i < elements.length; i++) { | |
| var element = elements[i]; | |
| var data = { | |
| props: props.split(' '), | |
| vals: [], | |
| changed: [], | |
| callback: callback | |
| }; | |
| // Grab each property's initial value | |
| for (var j = 0; j < data.props.length; j++) { | |
| data.vals[j] = element.attributes[data.props[j]] || element.style[data.props[j]]; | |
| data.changed[j] = false; | |
| } | |
| // Set watch array | |
| if (!element.watching) { | |
| element.watching = []; | |
| } | |
| // Store data in watch array | |
| element.watching.push(data); | |
| // Create new Mutation Observer | |
| var observer = new MutationObserver(function (mutations) { | |
| // console.log(mutations); | |
| for (var k = 0; k < mutations.length; k++) { | |
| check.call(mutations[k].target, mutations[k]); | |
| } | |
| }); | |
| // Start observing | |
| observer.observe(element, { subtree: false, attributes: true }); | |
| } | |
| // Return elements to enable chaining | |
| return self; | |
| }; | |
| // Expose watch to window | |
| window.watch = function () { | |
| return _watch.apply(arguments[0], arguments); | |
| }; | |
| // Expose watch to jQuery | |
| (function ($) { | |
| $.fn.watch = function () { | |
| Array.prototype.unshift.call(arguments, this); | |
| return _watch.apply(this, arguments); | |
| }; | |
| })(jQuery); | |
| })(window); | |
| /* | |
| * 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. | |
| */ | |
| if (typeof WeakMap === 'undefined') { | |
| (function() { | |
| var defineProperty = Object.defineProperty; | |
| var counter = Date.now() % 1e9; | |
| var WeakMap = function() { | |
| this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); | |
| }; | |
| WeakMap.prototype = { | |
| set: function(key, value) { | |
| var entry = key[this.name]; | |
| if (entry && entry[0] === key) | |
| entry[1] = value; | |
| else | |
| defineProperty(key, this.name, {value: [key, value], writable: true}); | |
| }, | |
| get: function(key) { | |
| var entry; | |
| return (entry = key[this.name]) && entry[0] === key ? | |
| entry[1] : undefined; | |
| }, | |
| delete: function(key) { | |
| var entry = key[this.name]; | |
| if (!entry) return false; | |
| var hasValue = entry[0] === key; | |
| entry[0] = entry[1] = undefined; | |
| return hasValue; | |
| }, | |
| has: function(key) { | |
| var entry = key[this.name]; | |
| if (!entry) return false; | |
| return entry[0] === key; | |
| } | |
| }; | |
| window.WeakMap = WeakMap; | |
| })(); | |
| } | |
| /* | |
| * 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 WeakMap(); | |
| // 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; | |
| if (!global.MutationObserver) | |
| global.MutationObserver = JsMutationObserver; | |
| })(this); |