Skip to content

Instantly share code, notes, and snippets.

@MiniDude22
Last active December 3, 2016 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MiniDude22/f1a29687589de4d77b1db742b54f5053 to your computer and use it in GitHub Desktop.
Save MiniDude22/f1a29687589de4d77b1db742b54f5053 to your computer and use it in GitHub Desktop.
Uncompressed and single file version of https://github.com/kapetan/jquery-observe
(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