Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| /* | |
| * 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); |