Last active
December 3, 2016 09:29
-
-
Save MiniDude22/f1a29687589de4d77b1db742b54f5053 to your computer and use it in GitHub Desktop.
Uncompressed and single file version of https://github.com/kapetan/jquery-observe
This file contains hidden or 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
(function($){ | |
$.Observe = {} | |
})(jQuery); | |
(function($, ns) { | |
var get = function(origin, target) { | |
if(!target) { | |
target = origin; | |
origin = window.document; | |
} | |
var result = []; | |
$(target).each(function() { | |
var selector = []; | |
var prev = $(this); | |
for(var current = prev.parent(); current.length && !prev.is(origin); current = current.parent()) { | |
var tag = prev.get(0).tagName.toLowerCase(); | |
selector.push(tag + ':eq(' + current.children(tag).index(prev) + ')'); | |
prev = current; | |
} | |
if(!current.length && !prev.is(origin)) { | |
return; | |
} | |
result.push('> ' + selector.reverse().join(' > ')); | |
}); | |
return result.join(', '); | |
}; | |
var capture = function(origin, target) { | |
if(!target) { | |
target = origin; | |
origin = window.document; | |
} | |
var result = []; | |
$(target).each(function() { | |
var textIndex = -1; | |
var realTarget = this; | |
if(this instanceof Text) { | |
realTarget = this.parentNode; | |
var children = realTarget.childNodes; | |
for(var i = 0; i < children.length; i++) { | |
if(children[i] === this) { | |
textIndex = i; | |
break; | |
} | |
} | |
} | |
var path = get(origin, realTarget); | |
var same = $(origin).is(realTarget); | |
result.push(function(origin) { | |
var target = same ? origin : $(origin).find(path); | |
return textIndex === -1 ? target : target.contents()[textIndex]; | |
}); | |
}); | |
return function(origin) { | |
origin = origin || window.document; | |
return result.reduce(function(acc, fn) { | |
return acc.add(fn(origin)); | |
}, $([])); | |
}; | |
}; | |
ns.path = { | |
get: get, | |
capture: capture | |
}; | |
}(jQuery, jQuery.Observe)); | |
(function($, ns) { | |
var Branch = function(root) { | |
this.original = $(root); | |
this.root = this.original.clone(false, true); | |
}; | |
Branch.prototype.find = function(selector) { | |
var path = ns.path.capture(this.original, selector); | |
return path(this.root); | |
}; | |
ns.Branch = Branch; | |
}(jQuery, jQuery.Observe)); | |
(function($, ns) { | |
var toObject = function(array, fn) { | |
var result = {}; | |
array.forEach(function(name) { | |
var pair = fn(name); | |
if(pair) { | |
result[pair[0]] = pair[1]; | |
} | |
}); | |
return result; | |
}; | |
var OBSERVER_OPTIONS = toObject([ | |
'childList', | |
'attributes', | |
'characterData', | |
'subtree', | |
'attributeOldValue', | |
'characterDataOldValue', | |
'attributeFilter' | |
], function(name) { | |
return [name.toLowerCase(), name]; | |
}); | |
var ALL = toObject(Object.keys(OBSERVER_OPTIONS), function(name) { | |
if(name !== 'attributefilter') { | |
return [OBSERVER_OPTIONS[name], true]; | |
} | |
}); | |
var EXTENDED_OPTIONS = toObject([ | |
'added', | |
'removed' | |
], function(name) { | |
return [name.toLowerCase(), name]; | |
}); | |
var EMPTY = $([]); | |
var parseOptions = function(options) { | |
if(typeof options === 'object') { | |
return options; | |
} | |
options = options.split(/\s+/); | |
var result = {}; | |
options.forEach(function(opt) { | |
opt = opt.toLowerCase(); | |
if(!OBSERVER_OPTIONS[opt] && !EXTENDED_OPTIONS[opt]) { | |
throw new Error('Unknown option ' + opt); | |
} | |
result[OBSERVER_OPTIONS[opt] || EXTENDED_OPTIONS[opt]] = true; | |
}); | |
return result; | |
}; | |
var objectToString = function(obj) { | |
return '[' + Object.keys(obj).sort().reduce(function(acc, key) { | |
var valueStr = (obj[key] && typeof obj[key] === 'object') ? objectToString(obj[key]) : obj[key]; | |
return acc + '[' + JSON.stringify(key) + ':' + valueStr + ']'; | |
}, '') + ']'; | |
}; | |
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; | |
var Pattern = function(target, options, selector, handler) { | |
this._originalOptions = $.extend({}, options); | |
options = $.extend({}, options); | |
this.attributeFilter = options.attributeFilter; | |
delete options.attributeFilter; | |
if(selector) { | |
options.subtree = true; | |
} | |
if(options.childList) { | |
options.added = true; | |
options.removed = true; | |
} | |
if(options.added || options.removed) { | |
options.childList = true; | |
} | |
this.target = $(target); | |
this.options = options; | |
this.selector = selector; | |
this.handler = handler; | |
}; | |
Pattern.prototype.is = function(options, selector, handler) { | |
return objectToString(this._originalOptions) === objectToString(options) && | |
this.selector === selector && | |
this.handler === handler; | |
}; | |
Pattern.prototype.match = function(record) { | |
var self = this; | |
var options = this.options; | |
var type = record.type; | |
if(!this.options[type]) { | |
return EMPTY; | |
} | |
if(this.selector) { | |
switch(type) { | |
case 'attributes': | |
if(!this._matchAttributeFilter(record)) { | |
break; | |
} | |
case 'characterData': | |
return this._matchAttributesAndCharacterData(record); | |
case 'childList': | |
if(record.addedNodes && record.addedNodes.length && options.added) { | |
var result = this._matchAddedNodes(record); | |
if(result.length) { | |
return result; | |
} | |
} | |
if(record.removedNodes && record.removedNodes.length && options.removed) { | |
return this._matchRemovedNodes(record); | |
} | |
} | |
} else { | |
var recordTarget = record.target instanceof Text ? | |
$(record.target).parent() : $(record.target); | |
if(!options.subtree && recordTarget.get(0) !== this.target.get(0)) { | |
return EMPTY; | |
} | |
switch(type) { | |
case 'attributes': | |
if(!this._matchAttributeFilter(record)) { | |
break; | |
} | |
case 'characterData': | |
return this.target; | |
case 'childList': | |
if((record.addedNodes && record.addedNodes.length && options.added) || | |
(record.removedNodes && record.removedNodes.length && options.removed)) { | |
return this.target; | |
} | |
} | |
} | |
return EMPTY; | |
}; | |
Pattern.prototype._matchAttributesAndCharacterData = function(record) { | |
return this._matchSelector(this.target, [record.target]); | |
}; | |
Pattern.prototype._matchAddedNodes = function(record) { | |
return this._matchSelector(this.target, record.addedNodes); | |
}; | |
Pattern.prototype._matchRemovedNodes = function(record) { | |
var branch = new ns.Branch(this.target); | |
var nodes = Array.prototype.slice.call(record.removedNodes).map(function(node) { | |
return node.cloneNode(true); | |
}); | |
if(record.previousSibling) { | |
branch.find(record.previousSibling).after(nodes); | |
} else if(record.nextSibling) { | |
branch.find(record.nextSibling).before(nodes); | |
} else { | |
branch.find(record.target).empty().append(nodes); | |
} | |
return this._matchSelector(branch.root, nodes).length ? $(record.target) : EMPTY; | |
}; | |
Pattern.prototype._matchSelector = function(origin, element) { | |
var match = origin.find(this.selector); | |
element = Array.prototype.slice.call(element); | |
match = match.filter(function() { | |
var self = this; | |
return element.some(function(node) { | |
if(node instanceof Text) return node.parentNode === self; | |
else return node === self || $(node).has(self).length; | |
}); | |
}); | |
return match; | |
}; | |
Pattern.prototype._matchAttributeFilter = function(record) { | |
if(this.attributeFilter && this.attributeFilter.length) { | |
return this.attributeFilter.indexOf(record.attributeName) >= 0; | |
} | |
return true; | |
}; | |
var Observer = function(target) { | |
this.patterns = []; | |
this._target = target; | |
this._observer = null; | |
}; | |
Observer.prototype.observe = function(options, selector, handler) { | |
var self = this; | |
if(!this._observer) { | |
this._observer = new MutationObserver(function(records) { | |
records.forEach(function(record) { | |
self.patterns.forEach(function(pattern) { | |
var match = pattern.match(record); | |
if(match.length) { | |
match.each(function() { | |
pattern.handler.call(this, record); | |
}); | |
} | |
}); | |
}); | |
}); | |
} else { | |
this._observer.disconnect(); | |
} | |
this.patterns.push(new Pattern(this._target, options, selector, handler)); | |
this._observer.observe(this._target, this._collapseOptions()); | |
}; | |
Observer.prototype.disconnect = function(options, selector, handler) { | |
var self = this; | |
if(this._observer) { | |
this.patterns.filter(function(pattern) { | |
return pattern.is(options, selector, handler); | |
}).forEach(function(pattern) { | |
var index = self.patterns.indexOf(pattern); | |
self.patterns.splice(index, 1); | |
}); | |
if(!this.patterns.length) { | |
this._observer.disconnect(); | |
} | |
} | |
}; | |
Observer.prototype.disconnectAll = function() { | |
if(this._observer) { | |
this.patterns = []; | |
this._observer.disconnect(); | |
} | |
}; | |
Observer.prototype.pause = function() { | |
if(this._observer) { | |
this._observer.disconnect(); | |
} | |
}; | |
Observer.prototype.resume = function() { | |
if(this._observer) { | |
this._observer.observe(this._target, this._collapseOptions()); | |
} | |
}; | |
Observer.prototype._collapseOptions = function() { | |
var result = {}; | |
this.patterns.forEach(function(pattern) { | |
var restrictiveFilter = result.attributes && result.attributeFilter; | |
if((restrictiveFilter || !result.attributes) && pattern.attributeFilter) { | |
var attributeFilter = (result.attributeFilter || []).concat(pattern.attributeFilter); | |
var existing = {}; | |
var unique = []; | |
attributeFilter.forEach(function(attr) { | |
if(!existing[attr]) { | |
unique.push(attr); | |
existing[attr] = 1; | |
} | |
}); | |
result.attributeFilter = unique; | |
} else if(restrictiveFilter && pattern.options.attributes && !pattern.attributeFilter) { | |
delete result.attributeFilter; | |
} | |
$.extend(result, pattern.options); | |
}); | |
Object.keys(EXTENDED_OPTIONS).forEach(function(name) { | |
delete result[EXTENDED_OPTIONS[name]]; | |
}); | |
return result; | |
}; | |
var DOMEventObserver = function(target) { | |
this.patterns = []; | |
this._paused = false; | |
this._target = target; | |
this._events = {}; | |
this._handler = this._handler.bind(this); | |
}; | |
DOMEventObserver.prototype.NS = '.jQueryObserve'; | |
DOMEventObserver.prototype.observe = function(options, selector, handler) { | |
var pattern = new Pattern(this._target, options, selector, handler); | |
var target = $(this._target); | |
if(pattern.options.childList) { | |
this._addEvent('DOMNodeInserted'); | |
this._addEvent('DOMNodeRemoved'); | |
} | |
if(pattern.options.attributes) { | |
this._addEvent('DOMAttrModified'); | |
} | |
if(pattern.options.characterData) { | |
this._addEvent('DOMCharacerDataModified'); | |
} | |
this.patterns.push(pattern); | |
}; | |
DOMEventObserver.prototype.disconnect = function(options, selector, handler) { | |
var target = $(this._target); | |
var self = this; | |
this.patterns.filter(function(pattern) { | |
return pattern.is(options, selector, handler); | |
}).forEach(function(pattern) { | |
var index = self.patterns.indexOf(pattern); | |
self.patterns.splice(index, 1); | |
}); | |
var eventsInUse = this.patterns.reduce(function(acc, pattern) { | |
if(pattern.options.childList) { | |
acc.DOMNodeInserted = true; | |
acc.DOMNodeRemoved = true; | |
} | |
if(pattern.options.attributes) { | |
acc.DOMAttrModified = true; | |
} | |
if(pattern.options.characterData) { | |
acc.DOMCharacerDataModified = true; | |
} | |
return acc; | |
}, {}); | |
Object.keys(this._events).forEach(function(type) { | |
if(eventsInUse[type]) { | |
return; | |
} | |
delete self._events[type]; | |
target.off(type + self.NS, self._handler); | |
}); | |
}; | |
DOMEventObserver.prototype.disconnectAll = function() { | |
var target = $(this._target); | |
for(var name in this._events) { | |
target.off(name + this.NS, this._handler); | |
} | |
this._events = {}; | |
this.patterns = []; | |
}; | |
DOMEventObserver.prototype.pause = function() { | |
this._paused = true; | |
}; | |
DOMEventObserver.prototype.resume = function() { | |
this._paused = false; | |
}; | |
DOMEventObserver.prototype._handler = function(e) { | |
if(this._paused) { | |
return; | |
} | |
var record = { | |
type: null, | |
target: null, | |
addedNodes: null, | |
removedNodes: null, | |
previousSibling: null, | |
nextSibling: null, | |
attributeName: null, | |
attributeNamespace: null, | |
oldValue: null | |
}; | |
switch(e.type) { | |
case 'DOMAttrModified': | |
record.type = 'attributes'; | |
record.target = e.target; | |
record.attributeName = e.attrName; | |
record.oldValue = e.prevValue; | |
break; | |
case 'DOMCharacerDataModified': | |
record.type = 'characterData'; | |
record.target = $(e.target).parent().get(0); | |
record.attributeName = e.attrName; | |
record.oldValue = e.prevValue; | |
break; | |
case 'DOMNodeInserted': | |
record.type = 'childList'; | |
record.target = e.relatedNode; | |
record.addedNodes = [e.target]; | |
record.removedNodes = []; | |
break; | |
case 'DOMNodeRemoved': | |
record.type = 'childList'; | |
record.target = e.relatedNode; | |
record.addedNodes = []; | |
record.removedNodes = [e.target]; | |
break; | |
} | |
for(var i = 0; i < this.patterns.length; i++) { | |
var pattern = this.patterns[i]; | |
var match = pattern.match(record); | |
if(match.length) { | |
match.each(function() { | |
pattern.handler.call(this, record); | |
}); | |
} | |
} | |
}; | |
DOMEventObserver.prototype._addEvent = function(type) { | |
if(!this._events[type]) { | |
$(this._target).on(type + this.NS, this._handler); | |
this._events[type] = true; | |
} | |
}; | |
ns.Pattern = Pattern; | |
ns.MutationObserver = Observer; | |
ns.DOMEventObserver = DOMEventObserver; | |
$.fn.observe = function(options, selector, handler) { | |
if(!selector) { | |
handler = options; | |
options = ALL; | |
} else if(!handler) { | |
handler = selector; | |
selector = null; | |
} | |
return this.each(function() { | |
var self = $(this); | |
var observer = self.data('observer'); | |
if(!observer) { | |
if(MutationObserver) { | |
observer = new Observer(this); | |
} else { | |
observer = new DOMEventObserver(this); | |
} | |
self.data('observer', observer); | |
} | |
options = parseOptions(options); | |
observer.observe(options, selector, handler); | |
}); | |
}; | |
$.fn.disconnect = function(options, selector, handler) { | |
if(!options) { | |
// No arguments | |
} | |
else if(!selector) { | |
handler = options; | |
options = ALL; | |
} else if(!handler) { | |
handler = selector; | |
selector = null; | |
} | |
return this.each(function() { | |
var self = $(this); | |
var observer = self.data('observer'); | |
if(!observer) { | |
return; | |
} | |
if(!options) { | |
observer.disconnectAll(); | |
self.removeData('observer'); | |
return; | |
} | |
options = parseOptions(options); | |
observer.disconnect(options, selector, handler); | |
}); | |
}; | |
}(jQuery, jQuery.Observe)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment