Create a gist now

Instantly share code, notes, and snippets.

Embed
Selector Observer, circa 2013
# Known issues
#
# In IE 9/10/11 replacing child via innerHTML will orphan all of the child
# elements. This prevents walking the descendants of removedNodes.
# https://connect.microsoft.com/IE/feedback/details/797844/ie9-10-11-dom-child-kill-bug
innerHTMLReplacementIsBuggy = do ->
a = document.createElement 'div'
b = document.createElement 'div'
c = document.createElement 'div'
a.appendChild b
b.appendChild c
a.innerHTML = ""
c.parentNode isnt b
# Observer uid counter
uid = 0
# Map of observer id to object
documentObservers = []
# Obscure property name to mark elements with.
expando = "observers#{Math.floor(Math.random()*1000000000)}"
initExpando = "#{expando}.init"
addExpando = "#{expando}.add"
# Match Element against selector.
matchesSelector = $.find.matchesSelector
# Initialize expando property on Element to an array.
#
# prop - String expando property name
# el - An Element
#
# Returns Array of ids.
initExpando = (prop, el) ->
if ids = el[prop]
ids
else
ids = []
Object.defineProperty el, prop,
enumerable: false
configurable: true
writable: false
value: ids
ids
# Run observer node "add" callback once.
# Call when observer selector matches node.
#
# el - An Element
# observer - An observer Object.
#
# Returns nothing.
addElement = (el, observer) ->
initIds = initExpando initExpando, el
addIds = initExpando addExpando, el
if initIds.indexOf(observer.id) is -1
observer.initialize?.call el
initIds.push observer.id
if addIds.indexOf(observer.id) is -1
observer.elements.push el
observer.add?.call el
addIds.push observer.id
return
# Run observer node "add" callback once on the any matching
# node and its subtree.
#
# nodes - An Array or NodeList of Nodes
# observers - An Array of observer Objects.
#
# Returns nothing.
addNodes = (nodes, observers) ->
for el in nodes when el.nodeType is Node.ELEMENT_NODE
for observer in observers
if matchesSelector el, observer.selector
addElement el, observer
for descendant in el.querySelectorAll observer.selector
addElement descendant, observer
return
# Runs all observer element "remove" callbacks.
# Call when element is completely removed from the DOM.
#
# el - An Element
# observer - Optional observer to check
#
# Returns nothing.
removeElement = (el, observer) ->
return unless addIds = el[addExpando]
if observer
index = observer.elements.indexOf el
if index isnt -1
observer.elements.splice index, 1
index = addIds.indexOf observer.id
if index isnt -1
observer.remove?.call el
addIds.splice index, 1
if addIds.length is 0
delete el[addExpando]
else
for id in addIds
observer = documentObservers[id]
index = observer.elements.indexOf el
if index isnt -1
observer.elements.splice index, 1
observer.remove?.call el
delete el[addExpando]
return
# Run all observer node "remove" callbacks on the node
# and its entire subtree.
#
# nodes - An Array or NodeList of Nodes
# observers - An Array of observer Objects.
#
# Returns nothing.
removeNodes = (nodes, observers) ->
for el in nodes when el.nodeType is Node.ELEMENT_NODE
removeElement el
for descendant in el.getElementsByTagName '*'
removeElement descendant
if innerHTMLReplacementIsBuggy
for observer in observers
for el in observer.elements when !el.parentNode
removeElement el
return
# Recheck all "add" observers to see if the selector still matches.
# If not, run the "remove" callback.
#
# node - A Node
# observers - An Array of observer Objects.
#
# Returns nothing.
revalidateObservers = (node, observers) ->
return unless node.nodeType is Node.ELEMENT_NODE
for observer in observers
if matchesSelector node, observer.selector
addElement node, observer
for id in (node[addExpando] ? []).slice(0)
observer = documentObservers[id]
unless matchesSelector node, observer.selector
removeElement node, observer
return
# Register a new observer.
#
# selector - String CSS selector.
# handlers - Initialize Function or Object with keys:
# initialize - Function to invoke once when Node is first matched
# add - Function to invoke when Node matches selector
# remove - Function to invoke when Node no longer matches selector
#
# Returns Observer object.
$.observe = (selector, handlers) ->
# If second arg is a function, default to initialize
if handlers.call?
handlers = {initialize: handlers}
observer = {
id: uid++
selector: selector
initialize: handlers.initialize or handlers.init
add: handlers.add
remove: handlers.remove
elements: []
}
documentObservers[observer.id] = observer
addNodes [document.documentElement], [observer]
observer
handleDocumentMutations = (mutations) ->
for mutation in mutations
if mutation.type is 'childList'
addNodes mutation.addedNodes, documentObservers
removeNodes mutation.removedNodes, documentObservers
else if mutation.type is 'attributes'
revalidateObservers mutation.target, documentObservers
return
documentObserver = new MutationObserver handleDocumentMutations
documentObserver.observe document, childList: true, attributes: true, subtree: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment