mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-12 18:52:50 +00:00
606 lines
16 KiB
JavaScript
606 lines
16 KiB
JavaScript
var racer = require('racer')
|
|
, domShim = require('dom-shim')
|
|
, EventDispatcher = require('./EventDispatcher')
|
|
, viewPath = require('./viewPath')
|
|
, escapeHtml = require('html-util').escapeHtml
|
|
, merge = racer.util.merge
|
|
, win = window
|
|
, doc = win.document
|
|
, markers = {}
|
|
, elements = {
|
|
$_win: win
|
|
, $_doc: doc
|
|
}
|
|
, addListener, removeListener;
|
|
|
|
module.exports = Dom;
|
|
|
|
function Dom(model) {
|
|
var dom = this
|
|
, fns = this.fns
|
|
|
|
// Map dom event name -> true
|
|
, listenerAdded = {}
|
|
, captureListenerAdded = {};
|
|
|
|
// DOM listener capturing allows blur and focus to be delegated
|
|
// http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
|
|
var captureEvents = this._captureEvents = new EventDispatcher({
|
|
onTrigger: onCaptureTrigger
|
|
, onBind: onCaptureBind
|
|
});
|
|
function onCaptureTrigger(name, listener, e) {
|
|
var id = listener.id
|
|
, el = doc.getElementById(id);
|
|
|
|
// Remove listener if element isn't found
|
|
if (!el) return false;
|
|
|
|
if (el.tagName === 'HTML' || el.contains(e.target)) {
|
|
onDomTrigger(name, listener, id, e, el);
|
|
}
|
|
}
|
|
function onCaptureBind(name, listener) {
|
|
if (captureListenerAdded[name]) return;
|
|
addListener(doc, name, captureTrigger, true);
|
|
captureListenerAdded[name] = true;
|
|
}
|
|
|
|
var events = this._events = new EventDispatcher({
|
|
onTrigger: onDomTrigger
|
|
, onBind: onDomBind
|
|
});
|
|
function onDomTrigger(name, listener, id, e, el, next) {
|
|
var delay = listener.delay
|
|
, finish = listener.fn;
|
|
|
|
e.path = function(name) {
|
|
var path = model.__pathMap.paths[listener.pathId];
|
|
if (!name) return path;
|
|
viewPath.patchCtx(listener.ctx, path)
|
|
return viewPath.ctxPath(listener.view, listener.ctx, name);
|
|
};
|
|
e.get = function(name) {
|
|
var path = e.path(name);
|
|
return viewPath.dataValue(listener.view, listener.ctx, model, path);
|
|
};
|
|
e.at = function(name) {
|
|
return model.at(e.path(name));
|
|
};
|
|
|
|
if (!finish) {
|
|
// Update the model when the element's value changes
|
|
finish = function() {
|
|
var value = dom.getMethods[listener.method](el, listener.property)
|
|
, setValue = listener.setValue;
|
|
|
|
// Allow the listener to override the setting function
|
|
if (setValue) {
|
|
setValue(model, value);
|
|
return;
|
|
}
|
|
|
|
// Remove this listener if its path id is no longer registered
|
|
var path = model.__pathMap.paths[listener.pathId];
|
|
if (!path) return false;
|
|
|
|
// Set the value if changed
|
|
if (model.get(path) === value) return;
|
|
model.pass(e).set(path, value);
|
|
}
|
|
}
|
|
|
|
if (delay != null) {
|
|
setTimeout(finish, delay, e, el, next, dom);
|
|
} else {
|
|
finish(e, el, next, dom);
|
|
}
|
|
}
|
|
function onDomBind(name, listener, eventName) {
|
|
if (listenerAdded[eventName]) return;
|
|
addListener(doc, eventName, triggerDom, true);
|
|
listenerAdded[eventName] = true;
|
|
}
|
|
|
|
function triggerDom(e, el, noBubble, continued) {
|
|
if (!el) el = e.target;
|
|
var prefix = e.type + ':'
|
|
, id;
|
|
|
|
// Next can be called from a listener to continue bubbling
|
|
function next() {
|
|
triggerDom(e, el.parentNode, false, true);
|
|
}
|
|
next.firstTrigger = !continued;
|
|
if (noBubble && (id = el.id)) {
|
|
return events.trigger(prefix + id, id, e, el, next);
|
|
}
|
|
while (true) {
|
|
while (!(id = el.id)) {
|
|
if (!(el = el.parentNode)) return;
|
|
}
|
|
// Stop bubbling once the event is handled
|
|
if (events.trigger(prefix + id, id, e, el, next)) return;
|
|
if (!(el = el.parentNode)) return;
|
|
}
|
|
}
|
|
|
|
function captureTrigger(e) {
|
|
captureEvents.trigger(e.type, e);
|
|
}
|
|
|
|
this.trigger = triggerDom;
|
|
this.captureTrigger = captureTrigger;
|
|
|
|
this._listeners = [];
|
|
this._components = [];
|
|
this._pendingUpdates = [];
|
|
|
|
function componentCleanup() {
|
|
var components = dom._components
|
|
, map = getMarkers()
|
|
, i, component
|
|
for (i = components.length; i--;) {
|
|
component = components[i];
|
|
if (component && !getMarker(map, component.scope)) {
|
|
component.emit('destroy');
|
|
}
|
|
}
|
|
}
|
|
// This cleanup listeners is placed at the beginning so that component
|
|
// scopes are cleared before any ref cleanups are checked
|
|
model.listeners('cleanup').unshift(componentCleanup);
|
|
}
|
|
|
|
Dom.prototype = {
|
|
clear: domClear
|
|
, bind: domBind
|
|
, item: domItem
|
|
, marker: domMarker
|
|
, update: domUpdate
|
|
, nextUpdate: nextUpdate
|
|
, _emitUpdate: emitUpdate
|
|
, addListener: domAddListener
|
|
, removeListener: domRemoveListener
|
|
, addComponent: addComponent
|
|
|
|
, getMethods: {
|
|
attr: getAttr
|
|
, prop: getProp
|
|
, propPolite: getProp
|
|
, html: getHtml
|
|
// These methods return NaN, because it never equals anything else. Thus,
|
|
// when compared against the new value, the new value will always be set
|
|
, append: getNaN
|
|
, insert: getNaN
|
|
, remove: getNaN
|
|
, move: getNaN
|
|
}
|
|
|
|
, setMethods: {
|
|
attr: setAttr
|
|
, prop: setProp
|
|
, propPolite: setProp
|
|
, html: setHtml
|
|
, append: setAppend
|
|
, insert: setInsert
|
|
, remove: setRemove
|
|
, move: setMove
|
|
}
|
|
|
|
, fns: {
|
|
$forChildren: forChildren
|
|
, $forName: forName
|
|
}
|
|
}
|
|
|
|
function domClear() {
|
|
this._events.clear();
|
|
this._captureEvents.clear();
|
|
var components = this._components
|
|
, listeners = this._listeners
|
|
, i, component
|
|
for (i = listeners.length; i--;) {
|
|
removeListener.apply(null, listeners[i]);
|
|
}
|
|
this._listeners = [];
|
|
for (i = components.length; i--;) {
|
|
component = components[i];
|
|
component && component.emit('destroy');
|
|
}
|
|
this._components = [];
|
|
markers = {};
|
|
}
|
|
|
|
function domListenerHash() {
|
|
var out = {}
|
|
, key
|
|
for (key in this) {
|
|
if (key === 'view' || key === 'ctx' || key === 'pathId') continue;
|
|
out[key] = this[key];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function domBind(eventName, id, listener) {
|
|
listener.toJSON = domListenerHash;
|
|
if (listener.capture) {
|
|
listener.id = id;
|
|
this._captureEvents.bind(eventName, listener);
|
|
} else {
|
|
this._events.bind("" + eventName + ":" + id, listener, eventName);
|
|
}
|
|
}
|
|
|
|
function domItem(id) {
|
|
return doc.getElementById(id) || elements[id] || getRange(id);
|
|
}
|
|
|
|
function domUpdate(el, method, ignore, value, property, index) {
|
|
// Set to true during rendering
|
|
if (this._preventUpdates) return;
|
|
|
|
// Wrapped in a try / catch so that errors thrown on DOM updates don't
|
|
// stop subsequent code from running
|
|
try {
|
|
// Don't do anything if the element is already up to date
|
|
if (value === this.getMethods[method](el, property)) return;
|
|
this.setMethods[method](el, ignore, value, property, index);
|
|
this._emitUpdate();
|
|
} catch (err) {
|
|
setTimeout(function() {
|
|
throw err;
|
|
}, 0);
|
|
}
|
|
}
|
|
function nextUpdate(callback) {
|
|
this._pendingUpdates.push(callback);
|
|
}
|
|
function emitUpdate() {
|
|
var fns = this._pendingUpdates
|
|
, len = fns.length
|
|
, i;
|
|
if (!len) return;
|
|
this._pendingUpdates = [];
|
|
// Give the browser a chance to render the page before initializing
|
|
// components and other delayed updates
|
|
setTimeout(function() {
|
|
for (i = 0; i < len; i++) {
|
|
fns[i]();
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
function domAddListener(el, name, callback, captures) {
|
|
this._listeners.push([el, name, callback, captures]);
|
|
addListener(el, name, callback, captures);
|
|
}
|
|
function domRemoveListener(el, name, callback, captures) {
|
|
removeListener(el, name, callback, captures);
|
|
}
|
|
|
|
function addComponent(ctx, component) {
|
|
var components = this._components
|
|
, dom = component.dom = Object.create(this);
|
|
|
|
components.push(component);
|
|
component.on('destroy', function() {
|
|
var index = components.indexOf(component);
|
|
if (index === -1) return;
|
|
// The components array gets replaced on a dom.clear, so we allow
|
|
// it to get sparse as individual components are destroyed
|
|
delete components[index];
|
|
});
|
|
|
|
dom.addListener = function(el, name, callback, captures) {
|
|
component.on('destroy', function() {
|
|
removeListener(el, name, callback, captures);
|
|
});
|
|
addListener(el, name, callback, captures);
|
|
};
|
|
|
|
dom.element = function(name) {
|
|
var id = ctx.$elements[name];
|
|
return document.getElementById(id);
|
|
};
|
|
|
|
return dom;
|
|
}
|
|
|
|
|
|
function getAttr(el, attr) {
|
|
return el.getAttribute(attr);
|
|
}
|
|
function getProp(el, prop) {
|
|
return el[prop];
|
|
}
|
|
function getHtml(el) {
|
|
return el.innerHTML;
|
|
}
|
|
function getNaN() {
|
|
return NaN;
|
|
}
|
|
|
|
function setAttr(el, ignore, value, attr) {
|
|
if (ignore && el.id === ignore) return;
|
|
el.setAttribute(attr, value);
|
|
}
|
|
function setProp(el, ignore, value, prop) {
|
|
if (ignore && el.id === ignore) return;
|
|
el[prop] = value;
|
|
}
|
|
function propPolite(el, ignore, value, prop) {
|
|
if (ignore && el.id === ignore) return;
|
|
if (el !== doc.activeElement || !doc.hasFocus()) {
|
|
el[prop] = value;
|
|
}
|
|
}
|
|
|
|
function makeSVGFragment(fragment, svgElement) {
|
|
// TODO: Allow optional namespace declarations
|
|
var pre = '<svg xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink>'
|
|
, post = '</svg>'
|
|
, range = document.createRange()
|
|
range.selectNode(svgElement);
|
|
return range.createContextualFragment(pre + fragment + post);
|
|
}
|
|
function appendSVG(element, fragment, svgElement) {
|
|
var frag = makeSVGFragment(fragment, svgElement)
|
|
, children = frag.childNodes[0].childNodes
|
|
, i
|
|
for (i = children.length; i--;) {
|
|
element.appendChild(children[0]);
|
|
}
|
|
}
|
|
function insertBeforeSVG(element, fragment, svgElement) {
|
|
var frag = makeSVGFragment(fragment, svgElement)
|
|
, children = frag.childNodes[0].childNodes
|
|
, parent = element.parentNode
|
|
, i
|
|
for (i = children.length; i--;) {
|
|
parent.insertBefore(children[0], element);
|
|
}
|
|
}
|
|
function removeChildren(element) {
|
|
var children = element.childNodes
|
|
, i
|
|
for (i = children.length; i--;) {
|
|
element.removeChild(children[0]);
|
|
}
|
|
}
|
|
|
|
function isSVG(obj) {
|
|
return !!obj.ownerSVGElement || obj.tagName === "svg";
|
|
}
|
|
function svgRoot(obj) {
|
|
return obj.ownerSVGElement || obj;
|
|
}
|
|
function isRange(obj) {
|
|
return !!obj.cloneRange;
|
|
}
|
|
|
|
function setHtml(obj, ignore, value, escape) {
|
|
if (escape) value = escapeHtml(value);
|
|
if(isRange(obj)) {
|
|
if(isSVG(obj.startContainer)) {
|
|
// SVG Element
|
|
obj.deleteContents();
|
|
var svgElement = svgRoot(obj.startContainer);
|
|
obj.insertNode(makeSVGFragment(value, svgElement));
|
|
return;
|
|
} else {
|
|
// Range
|
|
obj.deleteContents();
|
|
obj.insertNode(obj.createContextualFragment(value));
|
|
return;
|
|
}
|
|
}
|
|
if (isSVG(obj)) {
|
|
// SVG Element
|
|
var svgElement = svgRoot(obj);
|
|
removeChildren(obj);
|
|
appendSVG(obj, value, svgElement);
|
|
return;
|
|
}
|
|
// HTML Element
|
|
if (ignore && obj.id === ignore) return;
|
|
obj.innerHTML = value;
|
|
}
|
|
function setAppend(obj, ignore, value, escape) {
|
|
if (escape) value = escapeHtml(value);
|
|
if (isSVG(obj)) {
|
|
// SVG Element
|
|
var svgElement = obj.ownerSVGElement || obj;
|
|
appendSVG(obj, value, svgElement);
|
|
return;
|
|
}
|
|
if (obj.nodeType) {
|
|
// HTML Element
|
|
obj.insertAdjacentHTML('beforeend', value);
|
|
} else {
|
|
// Range
|
|
if(isSVG(obj.startContainer)) {
|
|
var el = obj.endContainer
|
|
, ref = el.childNodes[obj.endOffset];
|
|
var svgElement = svgRoot(ref);
|
|
el.insertBefore(makeSVGFragment(value, svgElement), ref)
|
|
} else {
|
|
var el = obj.endContainer
|
|
, ref = el.childNodes[obj.endOffset];
|
|
el.insertBefore(obj.createContextualFragment(value), ref);
|
|
}
|
|
}
|
|
}
|
|
function setInsert(obj, ignore, value, escape, index) {
|
|
if (escape) value = escapeHtml(value);
|
|
if (obj.nodeType) {
|
|
// Element
|
|
if (ref = obj.childNodes[index]) {
|
|
if (isSVG(obj)) {
|
|
var svgElement = obj.ownerSVGElement || obj;
|
|
insertBeforeSVG(ref, value, svgElement);
|
|
return;
|
|
}
|
|
ref.insertAdjacentHTML('beforebegin', value);
|
|
} else {
|
|
if (isSVG(obj)) {
|
|
var svgElement = obj.ownerSVGElement || obj;
|
|
appendSVG(obj, value, svgElement);
|
|
return;
|
|
}
|
|
obj.insertAdjacentHTML('beforeend', value);
|
|
}
|
|
} else {
|
|
// Range
|
|
if(isSVG(obj.startContainer)) {
|
|
var el = obj.startContainer
|
|
, ref = el.childNodes[obj.startOffset + index];
|
|
var svgElement = svgRoot(ref);
|
|
el.insertBefore(makeSVGFragment(value, svgElement), ref)
|
|
} else {
|
|
var el = obj.startContainer
|
|
, ref = el.childNodes[obj.startOffset + index];
|
|
el.insertBefore(obj.createContextualFragment(value), ref);
|
|
}
|
|
}
|
|
}
|
|
function setRemove(el, ignore, index) {
|
|
if (!el.nodeType) {
|
|
// Range
|
|
index += el.startOffset;
|
|
el = el.startContainer;
|
|
}
|
|
var child = el.childNodes[index];
|
|
if (child) el.removeChild(child);
|
|
}
|
|
function setMove(el, ignore, from, to, howMany) {
|
|
var child, fragment, nextChild, offset, ref, toEl;
|
|
if (!el.nodeType) {
|
|
offset = el.startOffset;
|
|
from += offset;
|
|
to += offset;
|
|
el = el.startContainer;
|
|
}
|
|
child = el.childNodes[from];
|
|
|
|
// Don't move if the item at the destination is passed as the ignore
|
|
// option, since this indicates the intended item was already moved
|
|
// Also don't move if the child to move matches the ignore option
|
|
if (!child || ignore && (toEl = el.childNodes[to]) &&
|
|
toEl.id === ignore || child.id === ignore) return;
|
|
|
|
ref = el.childNodes[to > from ? to + howMany : to];
|
|
if (howMany > 1) {
|
|
fragment = document.createDocumentFragment();
|
|
while (howMany--) {
|
|
nextChild = child.nextSibling;
|
|
fragment.appendChild(child);
|
|
if (!(child = nextChild)) break;
|
|
}
|
|
el.insertBefore(fragment, ref);
|
|
return;
|
|
}
|
|
el.insertBefore(child, ref);
|
|
}
|
|
|
|
function forChildren(e, el, next, dom) {
|
|
// Prevent infinte emission
|
|
if (!next.firstTrigger) return;
|
|
|
|
// Re-trigger the event on all child elements
|
|
var children = el.childNodes;
|
|
for (var i = 0, len = children.length, child; i < len; i++) {
|
|
child = children[i];
|
|
if (child.nodeType !== 1) continue; // Node.ELEMENT_NODE
|
|
dom.trigger(e, child, true, true);
|
|
forChildren(e, child, next, dom);
|
|
}
|
|
}
|
|
|
|
function forName(e, el, next, dom) {
|
|
// Prevent infinte emission
|
|
if (!next.firstTrigger) return;
|
|
|
|
var name = el.getAttribute('name');
|
|
if (!name) return;
|
|
|
|
// Re-trigger the event on all other elements with
|
|
// the same 'name' attribute
|
|
var elements = doc.getElementsByName(name)
|
|
, len = elements.length;
|
|
if (!(len > 1)) return;
|
|
for (var i = 0, element; i < len; i++) {
|
|
element = elements[i];
|
|
if (element === el) continue;
|
|
dom.trigger(e, element, false, true);
|
|
}
|
|
}
|
|
|
|
function getMarkers() {
|
|
var map = {}
|
|
// NodeFilter.SHOW_COMMENT == 128
|
|
, commentIterator = doc.createTreeWalker(doc, 128, null, false)
|
|
, comment
|
|
while (comment = commentIterator.nextNode()) {
|
|
map[comment.data] = comment;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function getMarker(map, name) {
|
|
var marker = map[name];
|
|
if (!marker) return;
|
|
|
|
// Comment nodes may continue to exist even if they have been removed from
|
|
// the page. Thus, make sure they are still somewhere in the page body
|
|
if (!doc.contains(marker)) {
|
|
delete map[name];
|
|
return;
|
|
}
|
|
return marker;
|
|
}
|
|
|
|
function domMarker(name) {
|
|
var marker = getMarker(markers, name);
|
|
if (!marker) {
|
|
markers = getMarkers();
|
|
marker = getMarker(markers, name);
|
|
if (!marker) return;
|
|
}
|
|
return marker;
|
|
}
|
|
|
|
function getRange(name) {
|
|
var start = domMarker(name);
|
|
if (!start) return;
|
|
var end = domMarker('$' + name);
|
|
if (!end) return;
|
|
|
|
var range = doc.createRange();
|
|
range.setStartAfter(start);
|
|
range.setEndBefore(end);
|
|
return range;
|
|
}
|
|
|
|
if (doc.addEventListener) {
|
|
addListener = function(el, name, callback, captures) {
|
|
el.addEventListener(name, callback, captures || false);
|
|
};
|
|
removeListener = function(el, name, callback, captures) {
|
|
el.removeEventListener(name, callback, captures || false);
|
|
};
|
|
|
|
} else if (doc.attachEvent) {
|
|
addListener = function(el, name, callback) {
|
|
function listener() {
|
|
if (!event.target) event.target = event.srcElement;
|
|
callback(event);
|
|
}
|
|
callback.$derbyListener = listener;
|
|
el.attachEvent('on' + name, listener);
|
|
};
|
|
removeListener = function(el, name, callback) {
|
|
el.detachEvent('on' + name, callback.$derbyListener);
|
|
};
|
|
}
|