|
/* wax - 6.0.1 - 1.0.4-557-g1eb9e9e */ |
|
|
|
|
|
!function (name, context, definition) { |
|
if (typeof module !== 'undefined') module.exports = definition(name, context); |
|
else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); |
|
else context[name] = definition(name, context); |
|
}('bean', this, function (name, context) { |
|
var win = window |
|
, old = context[name] |
|
, overOut = /over|out/ |
|
, namespaceRegex = /[^\.]*(?=\..*)\.|.*/ |
|
, nameRegex = /\..*/ |
|
, addEvent = 'addEventListener' |
|
, attachEvent = 'attachEvent' |
|
, removeEvent = 'removeEventListener' |
|
, detachEvent = 'detachEvent' |
|
, doc = document || {} |
|
, root = doc.documentElement || {} |
|
, W3C_MODEL = root[addEvent] |
|
, eventSupport = W3C_MODEL ? addEvent : attachEvent |
|
, slice = Array.prototype.slice |
|
, mouseTypeRegex = /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i |
|
, mouseWheelTypeRegex = /mouse.*(wheel|scroll)/i |
|
, textTypeRegex = /^text/i |
|
, touchTypeRegex = /^touch|^gesture/i |
|
, ONE = { one: 1 } // singleton for quick matching making add() do one() |
|
|
|
, nativeEvents = (function (hash, events, i) { |
|
for (i = 0; i < events.length; i++) |
|
hash[events[i]] = 1 |
|
return hash |
|
})({}, ( |
|
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons |
|
'mousewheel mousemultiwheel DOMMouseScroll ' + // mouse wheel |
|
'mouseover mouseout mousemove selectstart selectend ' + // mouse movement |
|
'keydown keypress keyup ' + // keyboard |
|
'orientationchange ' + // mobile |
|
'focus blur change reset select submit ' + // form elements |
|
'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window |
|
'error abort scroll ' + // misc |
|
(W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event |
|
// that doesn't actually exist, so make sure we only do these on newer browsers |
|
'show ' + // mouse buttons |
|
'input invalid ' + // form elements |
|
'touchstart touchmove touchend touchcancel ' + // touch |
|
'gesturestart gesturechange gestureend ' + // gesture |
|
'message readystatechange pageshow pagehide popstate ' + // window |
|
'hashchange offline online ' + // window |
|
'afterprint beforeprint ' + // printing |
|
'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd |
|
'loadstart progress suspend emptied stalled loadmetadata ' + // media |
|
'loadeddata canplay canplaythrough playing waiting seeking ' + // media |
|
'seeked ended durationchange timeupdate play pause ratechange ' + // media |
|
'volumechange cuechange ' + // media |
|
'checking noupdate downloading cached updateready obsolete ' + // appcache |
|
'' : '') |
|
).split(' ') |
|
) |
|
|
|
, customEvents = (function () { |
|
function isDescendant(parent, node) { |
|
while ((node = node.parentNode) !== null) { |
|
if (node === parent) return true |
|
} |
|
return false |
|
} |
|
|
|
function check(event) { |
|
var related = event.relatedTarget |
|
if (!related) return related === null |
|
return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)) |
|
} |
|
|
|
return { |
|
mouseenter: { base: 'mouseover', condition: check } |
|
, mouseleave: { base: 'mouseout', condition: check } |
|
, mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } |
|
} |
|
})() |
|
|
|
, fixEvent = (function () { |
|
var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ') |
|
, mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' ')) |
|
, mouseWheelProps = mouseProps.concat('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ axis'.split(' ')) // 'axis' is FF specific |
|
, keyProps = commonProps.concat('char charCode key keyCode keyIdentifier keyLocation'.split(' ')) |
|
, textProps = commonProps.concat(['data']) |
|
, touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' ')) |
|
, preventDefault = 'preventDefault' |
|
, createPreventDefault = function (event) { |
|
return function () { |
|
if (event[preventDefault]) |
|
event[preventDefault]() |
|
else |
|
event.returnValue = false |
|
} |
|
} |
|
, stopPropagation = 'stopPropagation' |
|
, createStopPropagation = function (event) { |
|
return function () { |
|
if (event[stopPropagation]) |
|
event[stopPropagation]() |
|
else |
|
event.cancelBubble = true |
|
} |
|
} |
|
, createStop = function (synEvent) { |
|
return function () { |
|
synEvent[preventDefault]() |
|
synEvent[stopPropagation]() |
|
synEvent.stopped = true |
|
} |
|
} |
|
, copyProps = function (event, result, props) { |
|
var i, p |
|
for (i = props.length; i--;) { |
|
p = props[i] |
|
if (!(p in result) && p in event) result[p] = event[p] |
|
} |
|
} |
|
|
|
return function (event, isNative) { |
|
var result = { originalEvent: event, isNative: isNative } |
|
if (!event) |
|
return result |
|
|
|
var props |
|
, type = event.type |
|
, target = event.target || event.srcElement |
|
|
|
result[preventDefault] = createPreventDefault(event) |
|
result[stopPropagation] = createStopPropagation(event) |
|
result.stop = createStop(result) |
|
result.target = target && target.nodeType === 3 ? target.parentNode : target |
|
|
|
if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive |
|
if (type.indexOf('key') !== -1) { |
|
props = keyProps |
|
result.keyCode = event.which || event.keyCode |
|
} else if (mouseTypeRegex.test(type)) { |
|
props = mouseProps |
|
result.rightClick = event.which === 3 || event.button === 2 |
|
result.pos = { x: 0, y: 0 } |
|
if (event.pageX || event.pageY) { |
|
result.clientX = event.pageX |
|
result.clientY = event.pageY |
|
} else if (event.clientX || event.clientY) { |
|
result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft |
|
result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop |
|
} |
|
if (overOut.test(type)) |
|
result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element'] |
|
} else if (touchTypeRegex.test(type)) { |
|
props = touchProps |
|
} else if (mouseWheelTypeRegex.test(type)) { |
|
props = mouseWheelProps |
|
} else if (textTypeRegex.test(type)) { |
|
props = textProps |
|
} |
|
copyProps(event, result, props || commonProps) |
|
} |
|
return result |
|
} |
|
})() |
|
|
|
// if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both |
|
, targetElement = function (element, isNative) { |
|
return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element |
|
} |
|
|
|
// we use one of these per listener, of any type |
|
, RegEntry = (function () { |
|
function entry(element, type, handler, original, namespaces) { |
|
this.element = element |
|
this.type = type |
|
this.handler = handler |
|
this.original = original |
|
this.namespaces = namespaces |
|
this.custom = customEvents[type] |
|
this.isNative = nativeEvents[type] && element[eventSupport] |
|
this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange' |
|
this.customType = !W3C_MODEL && !this.isNative && type |
|
this.target = targetElement(element, this.isNative) |
|
this.eventSupport = this.target[eventSupport] |
|
} |
|
|
|
entry.prototype = { |
|
// given a list of namespaces, is our entry in any of them? |
|
inNamespaces: function (checkNamespaces) { |
|
var i, j |
|
if (!checkNamespaces) |
|
return true |
|
if (!this.namespaces) |
|
return false |
|
for (i = checkNamespaces.length; i--;) { |
|
for (j = this.namespaces.length; j--;) { |
|
if (checkNamespaces[i] === this.namespaces[j]) |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
// match by element, original fn (opt), handler fn (opt) |
|
, matches: function (checkElement, checkOriginal, checkHandler) { |
|
return this.element === checkElement && |
|
(!checkOriginal || this.original === checkOriginal) && |
|
(!checkHandler || this.handler === checkHandler) |
|
} |
|
} |
|
|
|
return entry |
|
})() |
|
|
|
, registry = (function () { |
|
// our map stores arrays by event type, just because it's better than storing |
|
// everything in a single array. uses '$' as a prefix for the keys for safety |
|
var map = {} |
|
|
|
// generic functional search of our registry for matching listeners, |
|
// `fn` returns false to break out of the loop |
|
, forAll = function (element, type, original, handler, fn) { |
|
if (!type || type === '*') { |
|
// search the whole registry |
|
for (var t in map) { |
|
if (t.charAt(0) === '$') |
|
forAll(element, t.substr(1), original, handler, fn) |
|
} |
|
} else { |
|
var i = 0, l, list = map['$' + type], all = element === '*' |
|
if (!list) |
|
return |
|
for (l = list.length; i < l; i++) { |
|
if (all || list[i].matches(element, original, handler)) |
|
if (!fn(list[i], list, i, type)) |
|
return |
|
} |
|
} |
|
} |
|
|
|
, has = function (element, type, original) { |
|
// we're not using forAll here simply because it's a bit slower and this |
|
// needs to be fast |
|
var i, list = map['$' + type] |
|
if (list) { |
|
for (i = list.length; i--;) { |
|
if (list[i].matches(element, original, null)) |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
, get = function (element, type, original) { |
|
var entries = [] |
|
forAll(element, type, original, null, function (entry) { return entries.push(entry) }) |
|
return entries |
|
} |
|
|
|
, put = function (entry) { |
|
(map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry) |
|
return entry |
|
} |
|
|
|
, del = function (entry) { |
|
forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) { |
|
list.splice(i, 1) |
|
if (list.length === 0) |
|
delete map['$' + entry.type] |
|
return false |
|
}) |
|
} |
|
|
|
// dump all entries, used for onunload |
|
, entries = function () { |
|
var t, entries = [] |
|
for (t in map) { |
|
if (t.charAt(0) === '$') |
|
entries = entries.concat(map[t]) |
|
} |
|
return entries |
|
} |
|
|
|
return { has: has, get: get, put: put, del: del, entries: entries } |
|
})() |
|
|
|
// add and remove listeners to DOM elements |
|
, listener = W3C_MODEL ? function (element, type, fn, add) { |
|
element[add ? addEvent : removeEvent](type, fn, false) |
|
} : function (element, type, fn, add, custom) { |
|
if (custom && add && element['_on' + custom] === null) |
|
element['_on' + custom] = 0 |
|
element[add ? attachEvent : detachEvent]('on' + type, fn) |
|
} |
|
|
|
, nativeHandler = function (element, fn, args) { |
|
return function (event) { |
|
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true) |
|
return fn.apply(element, [event].concat(args)) |
|
} |
|
} |
|
|
|
, customHandler = function (element, fn, type, condition, args, isNative) { |
|
return function (event) { |
|
if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) { |
|
if (event) |
|
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative) |
|
fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args)) |
|
} |
|
} |
|
} |
|
|
|
, once = function (rm, element, type, fn, originalFn) { |
|
// wrap the handler in a handler that does a remove as well |
|
return function () { |
|
rm(element, type, originalFn) |
|
fn.apply(this, arguments) |
|
} |
|
} |
|
|
|
, removeListener = function (element, orgType, handler, namespaces) { |
|
var i, l, entry |
|
, type = (orgType && orgType.replace(nameRegex, '')) |
|
, handlers = registry.get(element, type, handler) |
|
|
|
for (i = 0, l = handlers.length; i < l; i++) { |
|
if (handlers[i].inNamespaces(namespaces)) { |
|
if ((entry = handlers[i]).eventSupport) |
|
listener(entry.target, entry.eventType, entry.handler, false, entry.type) |
|
// TODO: this is problematic, we have a registry.get() and registry.del() that |
|
// both do registry searches so we waste cycles doing this. Needs to be rolled into |
|
// a single registry.forAll(fn) that removes while finding, but the catch is that |
|
// we'll be splicing the arrays that we're iterating over. Needs extra tests to |
|
// make sure we don't screw it up. @rvagg |
|
registry.del(entry) |
|
} |
|
} |
|
} |
|
|
|
, addListener = function (element, orgType, fn, originalFn, args) { |
|
var entry |
|
, type = orgType.replace(nameRegex, '') |
|
, namespaces = orgType.replace(namespaceRegex, '').split('.') |
|
|
|
if (registry.has(element, type, fn)) |
|
return element // no dupe |
|
if (type === 'unload') |
|
fn = once(removeListener, element, type, fn, originalFn) // self clean-up |
|
if (customEvents[type]) { |
|
if (customEvents[type].condition) |
|
fn = customHandler(element, fn, type, customEvents[type].condition, true) |
|
type = customEvents[type].base || type |
|
} |
|
entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces)) |
|
entry.handler = entry.isNative ? |
|
nativeHandler(element, entry.handler, args) : |
|
customHandler(element, entry.handler, type, false, args, false) |
|
if (entry.eventSupport) |
|
listener(entry.target, entry.eventType, entry.handler, true, entry.customType) |
|
} |
|
|
|
, del = function (selector, fn, $) { |
|
return function (e) { |
|
var target, i, array = typeof selector === 'string' ? $(selector, this) : selector |
|
for (target = e.target; target && target !== this; target = target.parentNode) { |
|
for (i = array.length; i--;) { |
|
if (array[i] === target) { |
|
return fn.apply(target, arguments) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
, remove = function (element, typeSpec, fn) { |
|
var k, m, type, namespaces, i |
|
, rm = removeListener |
|
, isString = typeSpec && typeof typeSpec === 'string' |
|
|
|
if (isString && typeSpec.indexOf(' ') > 0) { |
|
// remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3') |
|
typeSpec = typeSpec.split(' ') |
|
for (i = typeSpec.length; i--;) |
|
remove(element, typeSpec[i], fn) |
|
return element |
|
} |
|
type = isString && typeSpec.replace(nameRegex, '') |
|
if (type && customEvents[type]) |
|
type = customEvents[type].type |
|
if (!typeSpec || isString) { |
|
// remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3) |
|
if (namespaces = isString && typeSpec.replace(namespaceRegex, '')) |
|
namespaces = namespaces.split('.') |
|
rm(element, type, fn, namespaces) |
|
} else if (typeof typeSpec === 'function') { |
|
// remove(el, fn) |
|
rm(element, null, typeSpec) |
|
} else { |
|
// remove(el, { t1: fn1, t2, fn2 }) |
|
for (k in typeSpec) { |
|
if (typeSpec.hasOwnProperty(k)) |
|
remove(element, k, typeSpec[k]) |
|
} |
|
} |
|
return element |
|
} |
|
|
|
, add = function (element, events, fn, delfn, $) { |
|
var type, types, i, args |
|
, originalFn = fn |
|
, isDel = fn && typeof fn === 'string' |
|
|
|
if (events && !fn && typeof events === 'object') { |
|
for (type in events) { |
|
if (events.hasOwnProperty(type)) |
|
add.apply(this, [ element, type, events[type] ]) |
|
} |
|
} else { |
|
args = arguments.length > 3 ? slice.call(arguments, 3) : [] |
|
types = (isDel ? fn : events).split(' ') |
|
isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1)) |
|
// special case for one() |
|
this === ONE && (fn = once(remove, element, events, fn, originalFn)) |
|
for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args) |
|
} |
|
return element |
|
} |
|
|
|
, one = function () { |
|
return add.apply(ONE, arguments) |
|
} |
|
|
|
, fireListener = W3C_MODEL ? function (isNative, type, element) { |
|
var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents') |
|
evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1) |
|
element.dispatchEvent(evt) |
|
} : function (isNative, type, element) { |
|
element = targetElement(element, isNative) |
|
// if not-native then we're using onpropertychange so we just increment a custom property |
|
isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++ |
|
} |
|
|
|
, fire = function (element, type, args) { |
|
var i, j, l, names, handlers |
|
, types = type.split(' ') |
|
|
|
for (i = types.length; i--;) { |
|
type = types[i].replace(nameRegex, '') |
|
if (names = types[i].replace(namespaceRegex, '')) |
|
names = names.split('.') |
|
if (!names && !args && element[eventSupport]) { |
|
fireListener(nativeEvents[type], type, element) |
|
} else { |
|
// non-native event, either because of a namespace, arguments or a non DOM element |
|
// iterate over all listeners and manually 'fire' |
|
handlers = registry.get(element, type) |
|
args = [false].concat(args) |
|
for (j = 0, l = handlers.length; j < l; j++) { |
|
if (handlers[j].inNamespaces(names)) |
|
handlers[j].handler.apply(element, args) |
|
} |
|
} |
|
} |
|
return element |
|
} |
|
|
|
, clone = function (element, from, type) { |
|
var i = 0 |
|
, handlers = registry.get(from, type) |
|
, l = handlers.length |
|
|
|
for (;i < l; i++) |
|
handlers[i].original && add(element, handlers[i].type, handlers[i].original) |
|
return element |
|
} |
|
|
|
, bean = { |
|
add: add |
|
, one: one |
|
, remove: remove |
|
, clone: clone |
|
, fire: fire |
|
, noConflict: function () { |
|
context[name] = old |
|
return this |
|
} |
|
} |
|
|
|
if (win[attachEvent]) { |
|
// for IE, clean up on unload to avoid leaks |
|
var cleanup = function () { |
|
var i, entries = registry.entries() |
|
for (i in entries) { |
|
if (entries[i].type && entries[i].type !== 'unload') |
|
remove(entries[i].element, entries[i].type) |
|
} |
|
win[detachEvent]('onunload', cleanup) |
|
win.CollectGarbage && win.CollectGarbage() |
|
} |
|
win[attachEvent]('onunload', cleanup) |
|
} |
|
|
|
return bean |
|
}) |
|
// Copyright Google Inc. |
|
// Licensed under the Apache Licence Version 2.0 |
|
// Autogenerated at Tue Oct 11 13:36:46 EDT 2011 |
|
// @provides html4 |
|
var html4 = {}; |
|
html4.atype = { |
|
NONE: 0, |
|
URI: 1, |
|
URI_FRAGMENT: 11, |
|
SCRIPT: 2, |
|
STYLE: 3, |
|
ID: 4, |
|
IDREF: 5, |
|
IDREFS: 6, |
|
GLOBAL_NAME: 7, |
|
LOCAL_NAME: 8, |
|
CLASSES: 9, |
|
FRAME_TARGET: 10 |
|
}; |
|
html4.ATTRIBS = { |
|
'*::class': 9, |
|
'*::dir': 0, |
|
'*::id': 4, |
|
'*::lang': 0, |
|
'*::onclick': 2, |
|
'*::ondblclick': 2, |
|
'*::onkeydown': 2, |
|
'*::onkeypress': 2, |
|
'*::onkeyup': 2, |
|
'*::onload': 2, |
|
'*::onmousedown': 2, |
|
'*::onmousemove': 2, |
|
'*::onmouseout': 2, |
|
'*::onmouseover': 2, |
|
'*::onmouseup': 2, |
|
'*::style': 3, |
|
'*::title': 0, |
|
'a::accesskey': 0, |
|
'a::coords': 0, |
|
'a::href': 1, |
|
'a::hreflang': 0, |
|
'a::name': 7, |
|
'a::onblur': 2, |
|
'a::onfocus': 2, |
|
'a::rel': 0, |
|
'a::rev': 0, |
|
'a::shape': 0, |
|
'a::tabindex': 0, |
|
'a::target': 10, |
|
'a::type': 0, |
|
'area::accesskey': 0, |
|
'area::alt': 0, |
|
'area::coords': 0, |
|
'area::href': 1, |
|
'area::nohref': 0, |
|
'area::onblur': 2, |
|
'area::onfocus': 2, |
|
'area::shape': 0, |
|
'area::tabindex': 0, |
|
'area::target': 10, |
|
'bdo::dir': 0, |
|
'blockquote::cite': 1, |
|
'br::clear': 0, |
|
'button::accesskey': 0, |
|
'button::disabled': 0, |
|
'button::name': 8, |
|
'button::onblur': 2, |
|
'button::onfocus': 2, |
|
'button::tabindex': 0, |
|
'button::type': 0, |
|
'button::value': 0, |
|
'canvas::height': 0, |
|
'canvas::width': 0, |
|
'caption::align': 0, |
|
'col::align': 0, |
|
'col::char': 0, |
|
'col::charoff': 0, |
|
'col::span': 0, |
|
'col::valign': 0, |
|
'col::width': 0, |
|
'colgroup::align': 0, |
|
'colgroup::char': 0, |
|
'colgroup::charoff': 0, |
|
'colgroup::span': 0, |
|
'colgroup::valign': 0, |
|
'colgroup::width': 0, |
|
'del::cite': 1, |
|
'del::datetime': 0, |
|
'dir::compact': 0, |
|
'div::align': 0, |
|
'dl::compact': 0, |
|
'font::color': 0, |
|
'font::face': 0, |
|
'font::size': 0, |
|
'form::accept': 0, |
|
'form::action': 1, |
|
'form::autocomplete': 0, |
|
'form::enctype': 0, |
|
'form::method': 0, |
|
'form::name': 7, |
|
'form::onreset': 2, |
|
'form::onsubmit': 2, |
|
'form::target': 10, |
|
'h1::align': 0, |
|
'h2::align': 0, |
|
'h3::align': 0, |
|
'h4::align': 0, |
|
'h5::align': 0, |
|
'h6::align': 0, |
|
'hr::align': 0, |
|
'hr::noshade': 0, |
|
'hr::size': 0, |
|
'hr::width': 0, |
|
'iframe::align': 0, |
|
'iframe::frameborder': 0, |
|
'iframe::height': 0, |
|
'iframe::marginheight': 0, |
|
'iframe::marginwidth': 0, |
|
'iframe::width': 0, |
|
'img::align': 0, |
|
'img::alt': 0, |
|
'img::border': 0, |
|
'img::height': 0, |
|
'img::hspace': 0, |
|
'img::ismap': 0, |
|
'img::name': 7, |
|
'img::src': 1, |
|
'img::usemap': 11, |
|
'img::vspace': 0, |
|
'img::width': 0, |
|
'input::accept': 0, |
|
'input::accesskey': 0, |
|
'input::align': 0, |
|
'input::alt': 0, |
|
'input::autocomplete': 0, |
|
'input::checked': 0, |
|
'input::disabled': 0, |
|
'input::ismap': 0, |
|
'input::maxlength': 0, |
|
'input::name': 8, |
|
'input::onblur': 2, |
|
'input::onchange': 2, |
|
'input::onfocus': 2, |
|
'input::onselect': 2, |
|
'input::readonly': 0, |
|
'input::size': 0, |
|
'input::src': 1, |
|
'input::tabindex': 0, |
|
'input::type': 0, |
|
'input::usemap': 11, |
|
'input::value': 0, |
|
'ins::cite': 1, |
|
'ins::datetime': 0, |
|
'label::accesskey': 0, |
|
'label::for': 5, |
|
'label::onblur': 2, |
|
'label::onfocus': 2, |
|
'legend::accesskey': 0, |
|
'legend::align': 0, |
|
'li::type': 0, |
|
'li::value': 0, |
|
'map::name': 7, |
|
'menu::compact': 0, |
|
'ol::compact': 0, |
|
'ol::start': 0, |
|
'ol::type': 0, |
|
'optgroup::disabled': 0, |
|
'optgroup::label': 0, |
|
'option::disabled': 0, |
|
'option::label': 0, |
|
'option::selected': 0, |
|
'option::value': 0, |
|
'p::align': 0, |
|
'pre::width': 0, |
|
'q::cite': 1, |
|
'select::disabled': 0, |
|
'select::multiple': 0, |
|
'select::name': 8, |
|
'select::onblur': 2, |
|
'select::onchange': 2, |
|
'select::onfocus': 2, |
|
'select::size': 0, |
|
'select::tabindex': 0, |
|
'table::align': 0, |
|
'table::bgcolor': 0, |
|
'table::border': 0, |
|
'table::cellpadding': 0, |
|
'table::cellspacing': 0, |
|
'table::frame': 0, |
|
'table::rules': 0, |
|
'table::summary': 0, |
|
'table::width': 0, |
|
'tbody::align': 0, |
|
'tbody::char': 0, |
|
'tbody::charoff': 0, |
|
'tbody::valign': 0, |
|
'td::abbr': 0, |
|
'td::align': 0, |
|
'td::axis': 0, |
|
'td::bgcolor': 0, |
|
'td::char': 0, |
|
'td::charoff': 0, |
|
'td::colspan': 0, |
|
'td::headers': 6, |
|
'td::height': 0, |
|
'td::nowrap': 0, |
|
'td::rowspan': 0, |
|
'td::scope': 0, |
|
'td::valign': 0, |
|
'td::width': 0, |
|
'textarea::accesskey': 0, |
|
'textarea::cols': 0, |
|
'textarea::disabled': 0, |
|
'textarea::name': 8, |
|
'textarea::onblur': 2, |
|
'textarea::onchange': 2, |
|
'textarea::onfocus': 2, |
|
'textarea::onselect': 2, |
|
'textarea::readonly': 0, |
|
'textarea::rows': 0, |
|
'textarea::tabindex': 0, |
|
'tfoot::align': 0, |
|
'tfoot::char': 0, |
|
'tfoot::charoff': 0, |
|
'tfoot::valign': 0, |
|
'th::abbr': 0, |
|
'th::align': 0, |
|
'th::axis': 0, |
|
'th::bgcolor': 0, |
|
'th::char': 0, |
|
'th::charoff': 0, |
|
'th::colspan': 0, |
|
'th::headers': 6, |
|
'th::height': 0, |
|
'th::nowrap': 0, |
|
'th::rowspan': 0, |
|
'th::scope': 0, |
|
'th::valign': 0, |
|
'th::width': 0, |
|
'thead::align': 0, |
|
'thead::char': 0, |
|
'thead::charoff': 0, |
|
'thead::valign': 0, |
|
'tr::align': 0, |
|
'tr::bgcolor': 0, |
|
'tr::char': 0, |
|
'tr::charoff': 0, |
|
'tr::valign': 0, |
|
'ul::compact': 0, |
|
'ul::type': 0 |
|
}; |
|
html4.eflags = { |
|
OPTIONAL_ENDTAG: 1, |
|
EMPTY: 2, |
|
CDATA: 4, |
|
RCDATA: 8, |
|
UNSAFE: 16, |
|
FOLDABLE: 32, |
|
SCRIPT: 64, |
|
STYLE: 128 |
|
}; |
|
html4.ELEMENTS = { |
|
'a': 0, |
|
'abbr': 0, |
|
'acronym': 0, |
|
'address': 0, |
|
'applet': 16, |
|
'area': 2, |
|
'b': 0, |
|
'base': 18, |
|
'basefont': 18, |
|
'bdo': 0, |
|
'big': 0, |
|
'blockquote': 0, |
|
'body': 49, |
|
'br': 2, |
|
'button': 0, |
|
'canvas': 0, |
|
'caption': 0, |
|
'center': 0, |
|
'cite': 0, |
|
'code': 0, |
|
'col': 2, |
|
'colgroup': 1, |
|
'dd': 1, |
|
'del': 0, |
|
'dfn': 0, |
|
'dir': 0, |
|
'div': 0, |
|
'dl': 0, |
|
'dt': 1, |
|
'em': 0, |
|
'fieldset': 0, |
|
'font': 0, |
|
'form': 0, |
|
'frame': 18, |
|
'frameset': 16, |
|
'h1': 0, |
|
'h2': 0, |
|
'h3': 0, |
|
'h4': 0, |
|
'h5': 0, |
|
'h6': 0, |
|
'head': 49, |
|
'hr': 2, |
|
'html': 49, |
|
'i': 0, |
|
'iframe': 4, |
|
'img': 2, |
|
'input': 2, |
|
'ins': 0, |
|
'isindex': 18, |
|
'kbd': 0, |
|
'label': 0, |
|
'legend': 0, |
|
'li': 1, |
|
'link': 18, |
|
'map': 0, |
|
'menu': 0, |
|
'meta': 18, |
|
'nobr': 0, |
|
'noembed': 4, |
|
'noframes': 20, |
|
'noscript': 20, |
|
'object': 16, |
|
'ol': 0, |
|
'optgroup': 0, |
|
'option': 1, |
|
'p': 1, |
|
'param': 18, |
|
'pre': 0, |
|
'q': 0, |
|
's': 0, |
|
'samp': 0, |
|
'script': 84, |
|
'select': 0, |
|
'small': 0, |
|
'span': 0, |
|
'strike': 0, |
|
'strong': 0, |
|
'style': 148, |
|
'sub': 0, |
|
'sup': 0, |
|
'table': 0, |
|
'tbody': 1, |
|
'td': 1, |
|
'textarea': 8, |
|
'tfoot': 1, |
|
'th': 1, |
|
'thead': 1, |
|
'title': 24, |
|
'tr': 1, |
|
'tt': 0, |
|
'u': 0, |
|
'ul': 0, |
|
'var': 0 |
|
}; |
|
html4.ueffects = { |
|
NOT_LOADED: 0, |
|
SAME_DOCUMENT: 1, |
|
NEW_DOCUMENT: 2 |
|
}; |
|
html4.URIEFFECTS = { |
|
'a::href': 2, |
|
'area::href': 2, |
|
'blockquote::cite': 0, |
|
'body::background': 1, |
|
'del::cite': 0, |
|
'form::action': 2, |
|
'img::src': 1, |
|
'input::src': 1, |
|
'ins::cite': 0, |
|
'q::cite': 0 |
|
}; |
|
html4.ltypes = { |
|
UNSANDBOXED: 2, |
|
SANDBOXED: 1, |
|
DATA: 0 |
|
}; |
|
html4.LOADERTYPES = { |
|
'a::href': 2, |
|
'area::href': 2, |
|
'blockquote::cite': 2, |
|
'body::background': 1, |
|
'del::cite': 2, |
|
'form::action': 2, |
|
'img::src': 1, |
|
'input::src': 1, |
|
'ins::cite': 2, |
|
'q::cite': 2 |
|
};; |
|
// Copyright (C) 2006 Google Inc. |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
|
|
/** |
|
* @fileoverview |
|
* An HTML sanitizer that can satisfy a variety of security policies. |
|
* |
|
* <p> |
|
* The HTML sanitizer is built around a SAX parser and HTML element and |
|
* attributes schemas. |
|
* |
|
* @author mikesamuel@gmail.com |
|
* @requires html4 |
|
* @overrides window |
|
* @provides html, html_sanitize |
|
*/ |
|
|
|
/** |
|
* @namespace |
|
*/ |
|
var html = (function (html4) { |
|
var lcase; |
|
// The below may not be true on browsers in the Turkish locale. |
|
if ('script' === 'SCRIPT'.toLowerCase()) { |
|
lcase = function (s) { return s.toLowerCase(); }; |
|
} else { |
|
/** |
|
* {@updoc |
|
* $ lcase('SCRIPT') |
|
* # 'script' |
|
* $ lcase('script') |
|
* # 'script' |
|
* } |
|
*/ |
|
lcase = function (s) { |
|
return s.replace( |
|
/[A-Z]/g, |
|
function (ch) { |
|
return String.fromCharCode(ch.charCodeAt(0) | 32); |
|
}); |
|
}; |
|
} |
|
|
|
var ENTITIES = { |
|
lt : '<', |
|
gt : '>', |
|
amp : '&', |
|
nbsp : '\240', |
|
quot : '"', |
|
apos : '\'' |
|
}; |
|
|
|
// Schemes on which to defer to uripolicy. Urls with other schemes are denied |
|
var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i; |
|
|
|
var decimalEscapeRe = /^#(\d+)$/; |
|
var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/; |
|
/** |
|
* Decodes an HTML entity. |
|
* |
|
* {@updoc |
|
* $ lookupEntity('lt') |
|
* # '<' |
|
* $ lookupEntity('GT') |
|
* # '>' |
|
* $ lookupEntity('amp') |
|
* # '&' |
|
* $ lookupEntity('nbsp') |
|
* # '\xA0' |
|
* $ lookupEntity('apos') |
|
* # "'" |
|
* $ lookupEntity('quot') |
|
* # '"' |
|
* $ lookupEntity('#xa') |
|
* # '\n' |
|
* $ lookupEntity('#10') |
|
* # '\n' |
|
* $ lookupEntity('#x0a') |
|
* # '\n' |
|
* $ lookupEntity('#010') |
|
* # '\n' |
|
* $ lookupEntity('#x00A') |
|
* # '\n' |
|
* $ lookupEntity('Pi') // Known failure |
|
* # '\u03A0' |
|
* $ lookupEntity('pi') // Known failure |
|
* # '\u03C0' |
|
* } |
|
* |
|
* @param name the content between the '&' and the ';'. |
|
* @return a single unicode code-point as a string. |
|
*/ |
|
function lookupEntity(name) { |
|
name = lcase(name); // TODO: π is different from Π |
|
if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; } |
|
var m = name.match(decimalEscapeRe); |
|
if (m) { |
|
return String.fromCharCode(parseInt(m[1], 10)); |
|
} else if (!!(m = name.match(hexEscapeRe))) { |
|
return String.fromCharCode(parseInt(m[1], 16)); |
|
} |
|
return ''; |
|
} |
|
|
|
function decodeOneEntity(_, name) { |
|
return lookupEntity(name); |
|
} |
|
|
|
var nulRe = /\0/g; |
|
function stripNULs(s) { |
|
return s.replace(nulRe, ''); |
|
} |
|
|
|
var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g; |
|
/** |
|
* The plain text of a chunk of HTML CDATA which possibly containing. |
|
* |
|
* {@updoc |
|
* $ unescapeEntities('') |
|
* # '' |
|
* $ unescapeEntities('hello World!') |
|
* # 'hello World!' |
|
* $ unescapeEntities('1 < 2 && 4 > 3 ') |
|
* # '1 < 2 && 4 > 3\n' |
|
* $ unescapeEntities('<< <- unfinished entity>') |
|
* # '<< <- unfinished entity>' |
|
* $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS |
|
* # '/foo?bar=baz©=true' |
|
* $ unescapeEntities('pi=ππ, Pi=Π\u03A0') // FIXME: known failure |
|
* # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0' |
|
* } |
|
* |
|
* @param s a chunk of HTML CDATA. It must not start or end inside an HTML |
|
* entity. |
|
*/ |
|
function unescapeEntities(s) { |
|
return s.replace(entityRe, decodeOneEntity); |
|
} |
|
|
|
var ampRe = /&/g; |
|
var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi; |
|
var ltRe = /</g; |
|
var gtRe = />/g; |
|
var quotRe = /\"/g; |
|
var eqRe = /\=/g; // Backslash required on JScript.net |
|
|
|
/** |
|
* Escapes HTML special characters in attribute values as HTML entities. |
|
* |
|
* {@updoc |
|
* $ escapeAttrib('') |
|
* # '' |
|
* $ escapeAttrib('"<<&==&>>"') // Do not just escape the first occurrence. |
|
* # '"<<&==&>>"' |
|
* $ escapeAttrib('Hello <World>!') |
|
* # 'Hello <World>!' |
|
* } |
|
*/ |
|
function escapeAttrib(s) { |
|
// Escaping '=' defangs many UTF-7 and SGML short-tag attacks. |
|
return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>') |
|
.replace(quotRe, '"').replace(eqRe, '='); |
|
} |
|
|
|
/** |
|
* Escape entities in RCDATA that can be escaped without changing the meaning. |
|
* {@updoc |
|
* $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8') |
|
* # '1 < 2 && 3 > 4 && 5 < 7&8' |
|
* } |
|
*/ |
|
function normalizeRCData(rcdata) { |
|
return rcdata |
|
.replace(looseAmpRe, '&$1') |
|
.replace(ltRe, '<') |
|
.replace(gtRe, '>'); |
|
} |
|
|
|
|
|
// TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html |
|
|
|
/** token definitions. */ |
|
var INSIDE_TAG_TOKEN = new RegExp( |
|
// Don't capture space. |
|
'^\\s*(?:' |
|
// Capture an attribute name in group 1, and value in group 3. |
|
// We capture the fact that there was an attribute in group 2, since |
|
// interpreters are inconsistent in whether a group that matches nothing |
|
// is null, undefined, or the empty string. |
|
+ ('(?:' |
|
+ '([a-z][a-z-]*)' // attribute name |
|
+ ('(' // optionally followed |
|
+ '\\s*=\\s*' |
|
+ ('(' |
|
// A double quoted string. |
|
+ '\"[^\"]*\"' |
|
// A single quoted string. |
|
+ '|\'[^\']*\'' |
|
// The positive lookahead is used to make sure that in |
|
// <foo bar= baz=boo>, the value for bar is blank, not "baz=boo". |
|
+ '|(?=[a-z][a-z-]*\\s*=)' |
|
// An unquoted value that is not an attribute name. |
|
// We know it is not an attribute name because the previous |
|
// zero-width match would've eliminated that possibility. |
|
+ '|[^>\"\'\\s]*' |
|
+ ')' |
|
) |
|
+ ')' |
|
) + '?' |
|
+ ')' |
|
) |
|
// End of tag captured in group 3. |
|
+ '|(\/?>)' |
|
// Don't capture cruft |
|
+ '|[\\s\\S][^a-z\\s>]*)', |
|
'i'); |
|
|
|
var OUTSIDE_TAG_TOKEN = new RegExp( |
|
'^(?:' |
|
// Entity captured in group 1. |
|
+ '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);' |
|
// Comment, doctypes, and processing instructions not captured. |
|
+ '|<\!--[\\s\\S]*?--\>|<!\\w[^>]*>|<\\?[^>*]*>' |
|
// '/' captured in group 2 for close tags, and name captured in group 3. |
|
+ '|<(\/)?([a-z][a-z0-9]*)' |
|
// Text captured in group 4. |
|
+ '|([^<&>]+)' |
|
// Cruft captured in group 5. |
|
+ '|([<&>]))', |
|
'i'); |
|
|
|
/** |
|
* Given a SAX-like event handler, produce a function that feeds those |
|
* events and a parameter to the event handler. |
|
* |
|
* The event handler has the form:{@code |
|
* { |
|
* // Name is an upper-case HTML tag name. Attribs is an array of |
|
* // alternating upper-case attribute names, and attribute values. The |
|
* // attribs array is reused by the parser. Param is the value passed to |
|
* // the saxParser. |
|
* startTag: function (name, attribs, param) { ... }, |
|
* endTag: function (name, param) { ... }, |
|
* pcdata: function (text, param) { ... }, |
|
* rcdata: function (text, param) { ... }, |
|
* cdata: function (text, param) { ... }, |
|
* startDoc: function (param) { ... }, |
|
* endDoc: function (param) { ... } |
|
* }} |
|
* |
|
* @param {Object} handler a record containing event handlers. |
|
* @return {Function} that takes a chunk of html and a parameter. |
|
* The parameter is passed on to the handler methods. |
|
*/ |
|
function makeSaxParser(handler) { |
|
return function parse(htmlText, param) { |
|
htmlText = String(htmlText); |
|
var htmlLower = null; |
|
|
|
var inTag = false; // True iff we're currently processing a tag. |
|
var attribs = []; // Accumulates attribute names and values. |
|
var tagName = void 0; // The name of the tag currently being processed. |
|
var eflags = void 0; // The element flags for the current tag. |
|
var openTag = void 0; // True if the current tag is an open tag. |
|
|
|
if (handler.startDoc) { handler.startDoc(param); } |
|
|
|
while (htmlText) { |
|
var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN); |
|
htmlText = htmlText.substring(m[0].length); |
|
|
|
if (inTag) { |
|
if (m[1]) { // attribute |
|
// setAttribute with uppercase names doesn't work on IE6. |
|
var attribName = lcase(m[1]); |
|
var decodedValue; |
|
if (m[2]) { |
|
var encodedValue = m[3]; |
|
switch (encodedValue.charCodeAt(0)) { // Strip quotes |
|
case 34: case 39: |
|
encodedValue = encodedValue.substring( |
|
1, encodedValue.length - 1); |
|
break; |
|
} |
|
decodedValue = unescapeEntities(stripNULs(encodedValue)); |
|
} else { |
|
// Use name as value for valueless attribs, so |
|
// <input type=checkbox checked> |
|
// gets attributes ['type', 'checkbox', 'checked', 'checked'] |
|
decodedValue = attribName; |
|
} |
|
attribs.push(attribName, decodedValue); |
|
} else if (m[4]) { |
|
if (eflags !== void 0) { // False if not in whitelist. |
|
if (openTag) { |
|
if (handler.startTag) { |
|
handler.startTag(tagName, attribs, param); |
|
} |
|
} else { |
|
if (handler.endTag) { |
|
handler.endTag(tagName, param); |
|
} |
|
} |
|
} |
|
|
|
if (openTag |
|
&& (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) { |
|
if (htmlLower === null) { |
|
htmlLower = lcase(htmlText); |
|
} else { |
|
htmlLower = htmlLower.substring( |
|
htmlLower.length - htmlText.length); |
|
} |
|
var dataEnd = htmlLower.indexOf('</' + tagName); |
|
if (dataEnd < 0) { dataEnd = htmlText.length; } |
|
if (dataEnd) { |
|
if (eflags & html4.eflags.CDATA) { |
|
if (handler.cdata) { |
|
handler.cdata(htmlText.substring(0, dataEnd), param); |
|
} |
|
} else if (handler.rcdata) { |
|
handler.rcdata( |
|
normalizeRCData(htmlText.substring(0, dataEnd)), param); |
|
} |
|
htmlText = htmlText.substring(dataEnd); |
|
} |
|
} |
|
|
|
tagName = eflags = openTag = void 0; |
|
attribs.length = 0; |
|
inTag = false; |
|
} |
|
} else { |
|
if (m[1]) { // Entity |
|
if (handler.pcdata) { handler.pcdata(m[0], param); } |
|
} else if (m[3]) { // Tag |
|
openTag = !m[2]; |
|
inTag = true; |
|
tagName = lcase(m[3]); |
|
eflags = html4.ELEMENTS.hasOwnProperty(tagName) |
|
? html4.ELEMENTS[tagName] : void 0; |
|
} else if (m[4]) { // Text |
|
if (handler.pcdata) { handler.pcdata(m[4], param); } |
|
} else if (m[5]) { // Cruft |
|
if (handler.pcdata) { |
|
var ch = m[5]; |
|
handler.pcdata( |
|
ch === '<' ? '<' : ch === '>' ? '>' : '&', |
|
param); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (handler.endDoc) { handler.endDoc(param); } |
|
}; |
|
} |
|
|
|
/** |
|
* Returns a function that strips unsafe tags and attributes from html. |
|
* @param {Function} sanitizeAttributes |
|
* maps from (tagName, attribs[]) to null or a sanitized attribute array. |
|
* The attribs array can be arbitrarily modified, but the same array |
|
* instance is reused, so should not be held. |
|
* @return {Function} from html to sanitized html |
|
*/ |
|
function makeHtmlSanitizer(sanitizeAttributes) { |
|
var stack; |
|
var ignoring; |
|
return makeSaxParser({ |
|
startDoc: function (_) { |
|
stack = []; |
|
ignoring = false; |
|
}, |
|
startTag: function (tagName, attribs, out) { |
|
if (ignoring) { return; } |
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; } |
|
var eflags = html4.ELEMENTS[tagName]; |
|
if (eflags & html4.eflags.FOLDABLE) { |
|
return; |
|
} else if (eflags & html4.eflags.UNSAFE) { |
|
ignoring = !(eflags & html4.eflags.EMPTY); |
|
return; |
|
} |
|
attribs = sanitizeAttributes(tagName, attribs); |
|
// TODO(mikesamuel): relying on sanitizeAttributes not to |
|
// insert unsafe attribute names. |
|
if (attribs) { |
|
if (!(eflags & html4.eflags.EMPTY)) { |
|
stack.push(tagName); |
|
} |
|
|
|
out.push('<', tagName); |
|
for (var i = 0, n = attribs.length; i < n; i += 2) { |
|
var attribName = attribs[i], |
|
value = attribs[i + 1]; |
|
if (value !== null && value !== void 0) { |
|
out.push(' ', attribName, '="', escapeAttrib(value), '"'); |
|
} |
|
} |
|
out.push('>'); |
|
} |
|
}, |
|
endTag: function (tagName, out) { |
|
if (ignoring) { |
|
ignoring = false; |
|
return; |
|
} |
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; } |
|
var eflags = html4.ELEMENTS[tagName]; |
|
if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY |
|
| html4.eflags.FOLDABLE))) { |
|
var index; |
|
if (eflags & html4.eflags.OPTIONAL_ENDTAG) { |
|
for (index = stack.length; --index >= 0;) { |
|
var stackEl = stack[index]; |
|
if (stackEl === tagName) { break; } |
|
if (!(html4.ELEMENTS[stackEl] |
|
& html4.eflags.OPTIONAL_ENDTAG)) { |
|
// Don't pop non optional end tags looking for a match. |
|
return; |
|
} |
|
} |
|
} else { |
|
for (index = stack.length; --index >= 0;) { |
|
if (stack[index] === tagName) { break; } |
|
} |
|
} |
|
if (index < 0) { return; } // Not opened. |
|
for (var i = stack.length; --i > index;) { |
|
var stackEl = stack[i]; |
|
if (!(html4.ELEMENTS[stackEl] |
|
& html4.eflags.OPTIONAL_ENDTAG)) { |
|
out.push('</', stackEl, '>'); |
|
} |
|
} |
|
stack.length = index; |
|
out.push('</', tagName, '>'); |
|
} |
|
}, |
|
pcdata: function (text, out) { |
|
if (!ignoring) { out.push(text); } |
|
}, |
|
rcdata: function (text, out) { |
|
if (!ignoring) { out.push(text); } |
|
}, |
|
cdata: function (text, out) { |
|
if (!ignoring) { out.push(text); } |
|
}, |
|
endDoc: function (out) { |
|
for (var i = stack.length; --i >= 0;) { |
|
out.push('</', stack[i], '>'); |
|
} |
|
stack.length = 0; |
|
} |
|
}); |
|
} |
|
|
|
// From RFC3986 |
|
var URI_SCHEME_RE = new RegExp( |
|
"^" + |
|
"(?:" + |
|
"([^:\/?#]+)" + // scheme |
|
":)?" |
|
); |
|
|
|
/** |
|
* Strips unsafe tags and attributes from html. |
|
* @param {string} htmlText to sanitize |
|
* @param {Function} opt_uriPolicy -- a transform to apply to uri/url |
|
* attribute values. If no opt_uriPolicy is provided, no uris |
|
* are allowed ie. the default uriPolicy rewrites all uris to null |
|
* @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to |
|
* apply to names, ids, and classes. If no opt_nmTokenPolicy is provided, |
|
* all names, ids and classes are passed through ie. the default |
|
* nmTokenPolicy is an identity transform |
|
* @return {string} html |
|
*/ |
|
function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) { |
|
var out = []; |
|
makeHtmlSanitizer( |
|
function sanitizeAttribs(tagName, attribs) { |
|
for (var i = 0; i < attribs.length; i += 2) { |
|
var attribName = attribs[i]; |
|
var value = attribs[i + 1]; |
|
var atype = null, attribKey; |
|
if ((attribKey = tagName + '::' + attribName, |
|
html4.ATTRIBS.hasOwnProperty(attribKey)) |
|
|| (attribKey = '*::' + attribName, |
|
html4.ATTRIBS.hasOwnProperty(attribKey))) { |
|
atype = html4.ATTRIBS[attribKey]; |
|
} |
|
if (atype !== null) { |
|
switch (atype) { |
|
case html4.atype.NONE: break; |
|
case html4.atype.SCRIPT: |
|
case html4.atype.STYLE: |
|
value = null; |
|
break; |
|
case html4.atype.ID: |
|
case html4.atype.IDREF: |
|
case html4.atype.IDREFS: |
|
case html4.atype.GLOBAL_NAME: |
|
case html4.atype.LOCAL_NAME: |
|
case html4.atype.CLASSES: |
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; |
|
break; |
|
case html4.atype.URI: |
|
var parsedUri = ('' + value).match(URI_SCHEME_RE); |
|
if (!parsedUri) { |
|
value = null; |
|
} else if (!parsedUri[1] || |
|
WHITELISTED_SCHEMES.test(parsedUri[1])) { |
|
value = opt_uriPolicy && opt_uriPolicy(value); |
|
} else { |
|
value = null; |
|
} |
|
break; |
|
case html4.atype.URI_FRAGMENT: |
|
if (value && '#' === value.charAt(0)) { |
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; |
|
if (value) { value = '#' + value; } |
|
} else { |
|
value = null; |
|
} |
|
break; |
|
default: |
|
value = null; |
|
break; |
|
} |
|
} else { |
|
value = null; |
|
} |
|
attribs[i + 1] = value; |
|
} |
|
return attribs; |
|
})(htmlText, out); |
|
return out.join(''); |
|
} |
|
|
|
return { |
|
escapeAttrib: escapeAttrib, |
|
makeHtmlSanitizer: makeHtmlSanitizer, |
|
makeSaxParser: makeSaxParser, |
|
normalizeRCData: normalizeRCData, |
|
sanitize: sanitize, |
|
unescapeEntities: unescapeEntities |
|
}; |
|
})(html4); |
|
|
|
var html_sanitize = html.sanitize; |
|
|
|
// Exports for closure compiler. Note this file is also cajoled |
|
// for domado and run in an environment without 'window' |
|
if (typeof window !== 'undefined') { |
|
window['html'] = html; |
|
window['html_sanitize'] = html_sanitize; |
|
} |
|
// Loosen restrictions of Caja's |
|
// html-sanitizer to allow for styling |
|
html4.ATTRIBS['*::style'] = 0; |
|
html4.ELEMENTS['style'] = 0; |
|
/* |
|
mustache.js — Logic-less templates in JavaScript |
|
|
|
See http://mustache.github.com/ for more info. |
|
*/ |
|
|
|
var Mustache = function() { |
|
var regexCache = {}; |
|
var Renderer = function() {}; |
|
|
|
Renderer.prototype = { |
|
otag: "{{", |
|
ctag: "}}", |
|
pragmas: {}, |
|
buffer: [], |
|
pragmas_implemented: { |
|
"IMPLICIT-ITERATOR": true |
|
}, |
|
context: {}, |
|
|
|
render: function(template, context, partials, in_recursion) { |
|
// reset buffer & set context |
|
if(!in_recursion) { |
|
this.context = context; |
|
this.buffer = []; // TODO: make this non-lazy |
|
} |
|
|
|
// fail fast |
|
if(!this.includes("", template)) { |
|
if(in_recursion) { |
|
return template; |
|
} else { |
|
this.send(template); |
|
return; |
|
} |
|
} |
|
|
|
// get the pragmas together |
|
template = this.render_pragmas(template); |
|
|
|
// render the template |
|
var html = this.render_section(template, context, partials); |
|
|
|
// render_section did not find any sections, we still need to render the tags |
|
if (html === false) { |
|
html = this.render_tags(template, context, partials, in_recursion); |
|
} |
|
|
|
if (in_recursion) { |
|
return html; |
|
} else { |
|
this.sendLines(html); |
|
} |
|
}, |
|
|
|
/* |
|
Sends parsed lines |
|
*/ |
|
send: function(line) { |
|
if(line !== "") { |
|
this.buffer.push(line); |
|
} |
|
}, |
|
|
|
sendLines: function(text) { |
|
if (text) { |
|
var lines = text.split("\n"); |
|
for (var i = 0; i < lines.length; i++) { |
|
this.send(lines[i]); |
|
} |
|
} |
|
}, |
|
|
|
/* |
|
Looks for %PRAGMAS |
|
*/ |
|
render_pragmas: function(template) { |
|
// no pragmas |
|
if(!this.includes("%", template)) { |
|
return template; |
|
} |
|
|
|
var that = this; |
|
var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) { |
|
return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); |
|
}); |
|
|
|
return template.replace(regex, function(match, pragma, options) { |
|
if(!that.pragmas_implemented[pragma]) { |
|
throw({message: |
|
"This implementation of mustache doesn't understand the '" + |
|
pragma + "' pragma"}); |
|
} |
|
that.pragmas[pragma] = {}; |
|
if(options) { |
|
var opts = options.split("="); |
|
that.pragmas[pragma][opts[0]] = opts[1]; |
|
} |
|
return ""; |
|
// ignore unknown pragmas silently |
|
}); |
|
}, |
|
|
|
/* |
|
Tries to find a partial in the curent scope and render it |
|
*/ |
|
render_partial: function(name, context, partials) { |
|
name = this.trim(name); |
|
if(!partials || partials[name] === undefined) { |
|
throw({message: "unknown_partial '" + name + "'"}); |
|
} |
|
if(typeof(context[name]) != "object") { |
|
return this.render(partials[name], context, partials, true); |
|
} |
|
return this.render(partials[name], context[name], partials, true); |
|
}, |
|
|
|
/* |
|
Renders inverted (^) and normal (#) sections |
|
*/ |
|
render_section: function(template, context, partials) { |
|
if(!this.includes("#", template) && !this.includes("^", template)) { |
|
// did not render anything, there were no sections |
|
return false; |
|
} |
|
|
|
var that = this; |
|
|
|
var regex = this.getCachedRegex("render_section", function(otag, ctag) { |
|
// This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder |
|
return new RegExp( |
|
"^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) |
|
|
|
otag + // {{ |
|
"(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) |
|
ctag + // }} |
|
|
|
"\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped |
|
|
|
otag + // {{ |
|
"\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). |
|
ctag + // }} |
|
|
|
"\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. |
|
|
|
"g"); |
|
}); |
|
|
|
|
|
// for each {{#foo}}{{/foo}} section do... |
|
return template.replace(regex, function(match, before, type, name, content, after) { |
|
// before contains only tags, no sections |
|
var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", |
|
|
|
// after may contain both sections and tags, so use full rendering function |
|
renderedAfter = after ? that.render(after, context, partials, true) : "", |
|
|
|
// will be computed below |
|
renderedContent, |
|
|
|
value = that.find(name, context); |
|
|
|
if (type === "^") { // inverted section |
|
if (!value || that.is_array(value) && value.length === 0) { |
|
// false or empty list, render it |
|
renderedContent = that.render(content, context, partials, true); |
|
} else { |
|
renderedContent = ""; |
|
} |
|
} else if (type === "#") { // normal section |
|
if (that.is_array(value)) { // Enumerable, Let's loop! |
|
renderedContent = that.map(value, function(row) { |
|
return that.render(content, that.create_context(row), partials, true); |
|
}).join(""); |
|
} else if (that.is_object(value)) { // Object, Use it as subcontext! |
|
renderedContent = that.render(content, that.create_context(value), |
|
partials, true); |
|
} else if (typeof value === "function") { |
|
// higher order section |
|
renderedContent = value.call(context, content, function(text) { |
|
return that.render(text, context, partials, true); |
|
}); |
|
} else if (value) { // boolean section |
|
renderedContent = that.render(content, context, partials, true); |
|
} else { |
|
renderedContent = ""; |
|
} |
|
} |
|
|
|
return renderedBefore + renderedContent + renderedAfter; |
|
}); |
|
}, |
|
|
|
/* |
|
Replace {{foo}} and friends with values from our view |
|
*/ |
|
render_tags: function(template, context, partials, in_recursion) { |
|
// tit for tat |
|
var that = this; |
|
|
|
|
|
|
|
var new_regex = function() { |
|
return that.getCachedRegex("render_tags", function(otag, ctag) { |
|
return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g"); |
|
}); |
|
}; |
|
|
|
var regex = new_regex(); |
|
var tag_replace_callback = function(match, operator, name) { |
|
switch(operator) { |
|
case "!": // ignore comments |
|
return ""; |
|
case "=": // set new delimiters, rebuild the replace regexp |
|
that.set_delimiters(name); |
|
regex = new_regex(); |
|
return ""; |
|
case ">": // render partial |
|
return that.render_partial(name, context, partials); |
|
case "{": // the triple mustache is unescaped |
|
return that.find(name, context); |
|
default: // escape the value |
|
return that.escape(that.find(name, context)); |
|
} |
|
}; |
|
var lines = template.split("\n"); |
|
for(var i = 0; i < lines.length; i++) { |
|
lines[i] = lines[i].replace(regex, tag_replace_callback, this); |
|
if(!in_recursion) { |
|
this.send(lines[i]); |
|
} |
|
} |
|
|
|
if(in_recursion) { |
|
return lines.join("\n"); |
|
} |
|
}, |
|
|
|
set_delimiters: function(delimiters) { |
|
var dels = delimiters.split(" "); |
|
this.otag = this.escape_regex(dels[0]); |
|
this.ctag = this.escape_regex(dels[1]); |
|
}, |
|
|
|
escape_regex: function(text) { |
|
// thank you Simon Willison |
|
if(!arguments.callee.sRE) { |
|
var specials = [ |
|
'/', '.', '*', '+', '?', '|', |
|
'(', ')', '[', ']', '{', '}', '\\' |
|
]; |
|
arguments.callee.sRE = new RegExp( |
|
'(\\' + specials.join('|\\') + ')', 'g' |
|
); |
|
} |
|
return text.replace(arguments.callee.sRE, '\\$1'); |
|
}, |
|
|
|
/* |
|
find `name` in current `context`. That is find me a value |
|
from the view object |
|
*/ |
|
find: function(name, context) { |
|
name = this.trim(name); |
|
|
|
// Checks whether a value is thruthy or false or 0 |
|
function is_kinda_truthy(bool) { |
|
return bool === false || bool === 0 || bool; |
|
} |
|
|
|
var value; |
|
if(is_kinda_truthy(context[name])) { |
|
value = context[name]; |
|
} else if(is_kinda_truthy(this.context[name])) { |
|
value = this.context[name]; |
|
} |
|
|
|
if(typeof value === "function") { |
|
return value.apply(context); |
|
} |
|
if(value !== undefined) { |
|
return value; |
|
} |
|
// silently ignore unkown variables |
|
return ""; |
|
}, |
|
|
|
// Utility methods |
|
|
|
/* includes tag */ |
|
includes: function(needle, haystack) { |
|
return haystack.indexOf(this.otag + needle) != -1; |
|
}, |
|
|
|
/* |
|
Does away with nasty characters |
|
*/ |
|
escape: function(s) { |
|
s = String(s === null ? "" : s); |
|
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) { |
|
switch(s) { |
|
case "&": return "&"; |
|
case '"': return '"'; |
|
case "'": return '''; |
|
case "<": return "<"; |
|
case ">": return ">"; |
|
default: return s; |
|
} |
|
}); |
|
}, |
|
|
|
// by @langalex, support for arrays of strings |
|
create_context: function(_context) { |
|
if(this.is_object(_context)) { |
|
return _context; |
|
} else { |
|
var iterator = "."; |
|
if(this.pragmas["IMPLICIT-ITERATOR"]) { |
|
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; |
|
} |
|
var ctx = {}; |
|
ctx[iterator] = _context; |
|
return ctx; |
|
} |
|
}, |
|
|
|
is_object: function(a) { |
|
return a && typeof a == "object"; |
|
}, |
|
|
|
is_array: function(a) { |
|
return Object.prototype.toString.call(a) === '[object Array]'; |
|
}, |
|
|
|
/* |
|
Gets rid of leading and trailing whitespace |
|
*/ |
|
trim: function(s) { |
|
return s.replace(/^\s*|\s*$/g, ""); |
|
}, |
|
|
|
/* |
|
Why, why, why? Because IE. Cry, cry cry. |
|
*/ |
|
map: function(array, fn) { |
|
if (typeof array.map == "function") { |
|
return array.map(fn); |
|
} else { |
|
var r = []; |
|
var l = array.length; |
|
for(var i = 0; i < l; i++) { |
|
r.push(fn(array[i])); |
|
} |
|
return r; |
|
} |
|
}, |
|
|
|
getCachedRegex: function(name, generator) { |
|
var byOtag = regexCache[this.otag]; |
|
if (!byOtag) { |
|
byOtag = regexCache[this.otag] = {}; |
|
} |
|
|
|
var byCtag = byOtag[this.ctag]; |
|
if (!byCtag) { |
|
byCtag = byOtag[this.ctag] = {}; |
|
} |
|
|
|
var regex = byCtag[name]; |
|
if (!regex) { |
|
regex = byCtag[name] = generator(this.otag, this.ctag); |
|
} |
|
|
|
return regex; |
|
} |
|
}; |
|
|
|
return({ |
|
name: "mustache.js", |
|
version: "0.4.0-dev", |
|
|
|
/* |
|
Turns a template and view into HTML |
|
*/ |
|
to_html: function(template, view, partials, send_fun) { |
|
var renderer = new Renderer(); |
|
if(send_fun) { |
|
renderer.send = send_fun; |
|
} |
|
renderer.render(template, view || {}, partials); |
|
if(!send_fun) { |
|
return renderer.buffer.join("\n"); |
|
} |
|
} |
|
}); |
|
}(); |
|
/*! |
|
* Reqwest! A general purpose XHR connection manager |
|
* copyright Dustin Diaz 2011 |
|
* https://github.com/ded/reqwest |
|
* license MIT |
|
*/ |
|
!function(context,win){function serial(a){var b=a.name;if(a.disabled||!b)return"";b=enc(b);switch(a.tagName.toLowerCase()){case"input":switch(a.type){case"reset":case"button":case"image":case"file":return"";case"checkbox":case"radio":return a.checked?b+"="+(a.value?enc(a.value):!0)+"&":"";default:return b+"="+(a.value?enc(a.value):"")+"&"}break;case"textarea":return b+"="+enc(a.value)+"&";case"select":return b+"="+enc(a.options[a.selectedIndex].value)+"&"}return""}function enc(a){return encodeURIComponent(a)}function reqwest(a,b){return new Reqwest(a,b)}function init(o,fn){function error(a){o.error&&o.error(a),complete(a)}function success(resp){o.timeout&&clearTimeout(self.timeout)&&(self.timeout=null);var r=resp.responseText;if(r)switch(type){case"json":resp=win.JSON?win.JSON.parse(r):eval("("+r+")");break;case"js":resp=eval(r);break;case"html":resp=r}fn(resp),o.success&&o.success(resp),complete(resp)}function complete(a){o.complete&&o.complete(a)}this.url=typeof o=="string"?o:o.url,this.timeout=null;var type=o.type||setType(this.url),self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){self.abort(),error()},o.timeout)),this.request=getRequest(o,success,error)}function setType(a){if(/\.json$/.test(a))return"json";if(/\.jsonp$/.test(a))return"jsonp";if(/\.js$/.test(a))return"js";if(/\.html?$/.test(a))return"html";if(/\.xml$/.test(a))return"xml";return"js"}function Reqwest(a,b){this.o=a,this.fn=b,init.apply(this,arguments)}function getRequest(a,b,c){if(a.type!="jsonp"){var f=xhr();f.open(a.method||"GET",typeof a=="string"?a:a.url,!0),setHeaders(f,a),f.onreadystatechange=handleReadyState(f,b,c),a.before&&a.before(f),f.send(a.data||null);return f}var d=doc.createElement("script"),e=0;win[getCallbackName(a)]=generalCallback,d.type="text/javascript",d.src=a.url,d.async=!0,d.onload=d.onreadystatechange=function(){if(d[readyState]&&d[readyState]!=="complete"&&d[readyState]!=="loaded"||e)return!1;d.onload=d.onreadystatechange=null,a.success&&a.success(lastValue),lastValue=undefined,head.removeChild(d),e=1},head.appendChild(d)}function generalCallback(a){lastValue=a}function getCallbackName(a){var b=a.jsonpCallback||"callback";if(a.url.slice(-(b.length+2))==b+"=?"){var c="reqwest_"+uniqid++;a.url=a.url.substr(0,a.url.length-1)+c;return c}var d=new RegExp(b+"=([\\w]+)");return a.url.match(d)[1]}function setHeaders(a,b){var c=b.headers||{};c.Accept=c.Accept||"text/javascript, text/html, application/xml, text/xml, */*",b.crossOrigin||(c["X-Requested-With"]=c["X-Requested-With"]||"XMLHttpRequest"),c[contentType]=c[contentType]||"application/x-www-form-urlencoded";for(var d in c)c.hasOwnProperty(d)&&a.setRequestHeader(d,c[d],!1)}function handleReadyState(a,b,c){return function(){a&&a[readyState]==4&&(twoHundo.test(a.status)?b(a):c(a))}}var twoHundo=/^20\d$/,doc=document,byTag="getElementsByTagName",readyState="readyState",contentType="Content-Type",head=doc[byTag]("head")[0],uniqid=0,lastValue,xhr="XMLHttpRequest"in win?function(){return new XMLHttpRequest}:function(){return new ActiveXObject("Microsoft.XMLHTTP")};Reqwest.prototype={abort:function(){this.request.abort()},retry:function(){init.call(this,this.o,this.fn)}},reqwest.serialize=function(a){var b=[a[byTag]("input"),a[byTag]("select"),a[byTag]("textarea")],c=[],d,e;for(d=0,l=b.length;d<l;++d)for(e=0,l2=b[d].length;e<l2;++e)c.push(serial(b[d][e]));return c.join("").replace(/&$/,"")},reqwest.serializeArray=function(a){for(var b=this.serialize(a).split("&"),c=0,d=b.length,e=[],f;c<d;c++)b[c]&&(f=b[c].split("="))&&e.push({name:f[0],value:f[1]});return e};var old=context.reqwest;reqwest.noConflict=function(){context.reqwest=old;return this},typeof module!="undefined"?module.exports=reqwest:context.reqwest=reqwest}(this,window);wax = wax || {}; |
|
|
|
// Attribution |
|
// ----------- |
|
wax.attribution = function() { |
|
var container, |
|
a = {}; |
|
|
|
function urlX(url) { |
|
// Data URIs are subject to a bug in Firefox |
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 |
|
// which let them be a vector. But WebKit does 'the right thing' |
|
// or at least 'something' about this situation, so we'll tolerate |
|
// them. |
|
if (/^(https?:\/\/|data:image)/.test(url)) { |
|
return url; |
|
} |
|
} |
|
|
|
function idX(id) { |
|
return id; |
|
} |
|
|
|
a.content = function(x) { |
|
if (typeof x === 'undefined') return container.innerHTML; |
|
container.innerHTML = html_sanitize(x, urlX, idX); |
|
return this; |
|
}; |
|
|
|
a.element = function() { |
|
return container; |
|
}; |
|
|
|
a.init = function() { |
|
container = document.createElement('div'); |
|
container.className = 'wax-attribution'; |
|
return this; |
|
}; |
|
|
|
return a.init(); |
|
}; |
|
wax = wax || {}; |
|
|
|
// Attribution |
|
// ----------- |
|
wax.bwdetect = function(options, callback) { |
|
var detector = {}, |
|
threshold = options.threshold || 400, |
|
// test image: 30.29KB |
|
testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()), |
|
// High-bandwidth assumed |
|
// 1: high bandwidth (.png, .jpg) |
|
// 0: low bandwidth (.png128, .jpg70) |
|
bw = 1, |
|
// Alternative versions |
|
auto = options.auto === undefined ? true : options.auto; |
|
|
|
function bwTest() { |
|
wax.bw = -1; |
|
var im = new Image(); |
|
im.src = testImage; |
|
var first = true; |
|
var timeout = setTimeout(function() { |
|
if (first && wax.bw == -1) { |
|
detector.bw(0); |
|
first = false; |
|
} |
|
}, threshold); |
|
im.onload = function() { |
|
if (first && wax.bw == -1) { |
|
clearTimeout(timeout); |
|
detector.bw(1); |
|
first = false; |
|
} |
|
}; |
|
} |
|
|
|
detector.bw = function(x) { |
|
if (!arguments.length) return bw; |
|
var oldBw = bw; |
|
if (wax.bwlisteners && wax.bwlisteners.length) (function () { |
|
listeners = wax.bwlisteners; |
|
wax.bwlisteners = []; |
|
for (i = 0; i < listeners; i++) { |
|
listeners[i](x); |
|
} |
|
})(); |
|
wax.bw = x; |
|
|
|
if (bw != (bw = x)) callback(x); |
|
}; |
|
|
|
detector.add = function() { |
|
if (auto) bwTest(); |
|
return this; |
|
}; |
|
|
|
if (wax.bw == -1) { |
|
wax.bwlisteners = wax.bwlisteners || []; |
|
wax.bwlisteners.push(detector.bw); |
|
} else if (wax.bw !== undefined) { |
|
detector.bw(wax.bw); |
|
} else { |
|
detector.add(); |
|
} |
|
return detector; |
|
}; |
|
// Formatter |
|
// --------- |
|
// |
|
// This code is no longer the recommended code path for Wax - |
|
// see `template.js`, a safe implementation of Mustache templates. |
|
wax.formatter = function(x) { |
|
var formatter = {}, |
|
f; |
|
|
|
// Prevent against just any input being used. |
|
if (x && typeof x === 'string') { |
|
try { |
|
// Ugly, dangerous use of eval. |
|
eval('f = ' + x); |
|
} catch (e) { |
|
if (console) console.log(e); |
|
} |
|
} else if (x && typeof x === 'function') { |
|
f = x; |
|
} else { |
|
f = function() {}; |
|
} |
|
|
|
function urlX(url) { |
|
if (/^(https?:\/\/|data:image)/.test(url)) { |
|
return url; |
|
} |
|
} |
|
|
|
function idX(id) { |
|
return id; |
|
} |
|
|
|
// Wrap the given formatter function in order to |
|
// catch exceptions that it may throw. |
|
formatter.format = function(options, data) { |
|
try { |
|
return html_sanitize(f(options, data), urlX, idX); |
|
} catch (e) { |
|
if (console) console.log(e); |
|
} |
|
}; |
|
|
|
return formatter; |
|
}; |
|
// GridInstance |
|
// ------------ |
|
// GridInstances are queryable, fully-formed |
|
// objects for acquiring features from events. |
|
// |
|
// This code ignores format of 1.1-1.2 |
|
wax.gi = function(grid_tile, options) { |
|
options = options || {}; |
|
// resolution is the grid-elements-per-pixel ratio of gridded data. |
|
// The size of a tile element. For now we expect tiles to be squares. |
|
var instance = {}, |
|
resolution = options.resolution || 4, |
|
tileSize = options.tileSize || 256; |
|
|
|
// Resolve the UTF-8 encoding stored in grids to simple |
|
// number values. |
|
// See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec) |
|
// for details. |
|
function resolveCode(key) { |
|
if (key >= 93) key--; |
|
if (key >= 35) key--; |
|
key -= 32; |
|
return key; |
|
} |
|
|
|
instance.grid_tile = function() { |
|
return grid_tile; |
|
}; |
|
|
|
instance.getKey = function(x, y) { |
|
if (!(grid_tile && grid_tile.grid)) return; |
|
if ((y < 0) || (x < 0)) return; |
|
if ((Math.floor(y) >= tileSize) || |
|
(Math.floor(x) >= tileSize)) return; |
|
// Find the key in the grid. The above calls should ensure that |
|
// the grid's array is large enough to make this work. |
|
return resolveCode(grid_tile.grid[ |
|
Math.floor((y) / resolution) |
|
].charCodeAt( |
|
Math.floor((x) / resolution) |
|
)); |
|
}; |
|
|
|
// Lower-level than tileFeature - has nothing to do |
|
// with the DOM. Takes a px offset from 0, 0 of a grid. |
|
instance.gridFeature = function(x, y) { |
|
// Find the key in the grid. The above calls should ensure that |
|
// the grid's array is large enough to make this work. |
|
var key = this.getKey(x, y), |
|
keys = grid_tile.keys; |
|
|
|
if (keys && |
|
keys[key] && |
|
grid_tile.data[keys[key]]) { |
|
return grid_tile.data[keys[key]]; |
|
} |
|
}; |
|
|
|
// Get a feature: |
|
// * `x` and `y`: the screen coordinates of an event |
|
// * `tile_element`: a DOM element of a tile, from which we can get an offset. |
|
instance.tileFeature = function(x, y, tile_element) { |
|
if (!grid_tile) return; |
|
// IE problem here - though recoverable, for whatever reason |
|
var offset = wax.u.offset(tile_element); |
|
feature = this.gridFeature(x - offset.left, y - offset.top); |
|
return feature; |
|
}; |
|
|
|
return instance; |
|
}; |
|
// GridManager |
|
// ----------- |
|
// Generally one GridManager will be used per map. |
|
// |
|
// It takes one options object, which current accepts a single option: |
|
// `resolution` determines the number of pixels per grid element in the grid. |
|
// The default is 4. |
|
wax.gm = function() { |
|
|
|
var resolution = 4, |
|
grid_tiles = {}, |
|
manager = {}, |
|
tilejson, |
|
formatter; |
|
|
|
var gridUrl = function(url) { |
|
return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json'); |
|
}; |
|
|
|
function templatedGridUrl(template) { |
|
if (typeof template === 'string') template = [template]; |
|
return function templatedGridFinder(url) { |
|
if (!url) return; |
|
var rx = new RegExp('/(\\d+)\\/(\\d+)\\/(\\d+)\\.[\\w\\._]+'); |
|
var xyz = rx.exec(url); |
|
if (!xyz) return; |
|
return template[parseInt(xyz[2], 10) % template.length] |
|
.replace('{z}', xyz[1]) |
|
.replace('{x}', xyz[2]) |
|
.replace('{y}', xyz[3]); |
|
}; |
|
} |
|
|
|
manager.formatter = function(x) { |
|
if (!arguments.length) return formatter; |
|
formatter = wax.formatter(x); |
|
return manager; |
|
}; |
|
|
|
manager.template = function(x) { |
|
if (!arguments.length) return formatter; |
|
formatter = wax.template(x); |
|
return manager; |
|
}; |
|
|
|
manager.gridUrl = function(x) { |
|
if (!arguments.length) return gridUrl; |
|
gridUrl = typeof x === 'function' ? |
|
x : templatedGridUrl(x); |
|
return manager; |
|
}; |
|
|
|
manager.getGrid = function(url, callback) { |
|
var gurl = gridUrl(url); |
|
if (!formatter || !gurl) return callback(null, null); |
|
|
|
wax.request.get(gurl, function(err, t) { |
|
if (err) return callback(err, null); |
|
callback(null, wax.gi(t, { |
|
formatter: formatter, |
|
resolution: resolution |
|
})); |
|
}); |
|
return manager; |
|
}; |
|
|
|
manager.tilejson = function(x) { |
|
if (!arguments.length) return tilejson; |
|
// prefer templates over formatters |
|
if (x.template) { |
|
manager.template(x.template); |
|
} else if (x.formatter) { |
|
manager.formatter(x.formatter); |
|
} |
|
if (x.grids) manager.gridUrl(x.grids); |
|
if (x.resolution) resolution = x.resolution; |
|
tilejson = x; |
|
return manager; |
|
}; |
|
|
|
return manager; |
|
}; |
|
wax = wax || {}; |
|
|
|
// Hash |
|
// ---- |
|
wax.hash = function(options) { |
|
options = options || {}; |
|
|
|
function getState() { |
|
return location.hash.substring(1); |
|
} |
|
|
|
function pushState(state) { |
|
var l = window.location; |
|
l.replace(l.toString().replace(l.hash, '#' + state)); |
|
} |
|
|
|
var s0, // old hash |
|
hash = {}, |
|
lat = 90 - 1e-8; // allowable latitude range |
|
|
|
function parseHash(s) { |
|
var args = s.split('/'); |
|
for (var i = 0; i < args.length; i++) { |
|
args[i] = Number(args[i]); |
|
if (isNaN(args[i])) return true; |
|
} |
|
if (args.length < 3) { |
|
// replace bogus hash |
|
return true; |
|
} else if (args.length == 3) { |
|
options.setCenterZoom(args); |
|
} |
|
} |
|
|
|
function move() { |
|
var s1 = options.getCenterZoom(); |
|
if (s0 !== s1) { |
|
s0 = s1; |
|
// don't recenter the map! |
|
pushState(s0); |
|
} |
|
} |
|
|
|
function stateChange(state) { |
|
// ignore spurious hashchange events |
|
if (state === s0) return; |
|
if (parseHash(s0 = state)) { |
|
// replace bogus hash |
|
move(); |
|
} |
|
} |
|
|
|
var _move = wax.u.throttle(move, 500); |
|
|
|
hash.add = function() { |
|
stateChange(getState()); |
|
options.bindChange(_move); |
|
return this; |
|
}; |
|
|
|
hash.remove = function() { |
|
options.unbindChange(_move); |
|
return this; |
|
}; |
|
|
|
return hash.add(); |
|
}; |
|
wax = wax || {}; |
|
|
|
wax.interaction = function() { |
|
var gm = wax.gm(), |
|
interaction = {}, |
|
_downLock = false, |
|
_clickTimeout = false, |
|
// Active feature |
|
// Down event |
|
_d, |
|
// Touch tolerance |
|
tol = 4, |
|
grid, |
|
attach, |
|
detach, |
|
parent, |
|
map, |
|
tileGrid; |
|
|
|
var defaultEvents = { |
|
mousemove: onMove, |
|
touchstart: onDown, |
|
mousedown: onDown |
|
}; |
|
|
|
var touchEnds = { |
|
touchend: onUp, |
|
touchmove: onUp, |
|
touchcancel: touchCancel |
|
}; |
|
|
|
// Abstract getTile method. Depends on a tilegrid with |
|
// grid[ [x, y, tile] ] structure. |
|
function getTile(e) { |
|
var g = grid(); |
|
for (var i = 0; i < g.length; i++) { |
|
if ((g[i][0] < e.y) && |
|
((g[i][0] + 256) > e.y) && |
|
(g[i][1] < e.x) && |
|
((g[i][1] + 256) > e.x)) return g[i][2]; |
|
} |
|
return false; |
|
} |
|
|
|
// Clear the double-click timeout to prevent double-clicks from |
|
// triggering popups. |
|
function killTimeout() { |
|
if (_clickTimeout) { |
|
window.clearTimeout(_clickTimeout); |
|
_clickTimeout = null; |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
function onMove(e) { |
|
// If the user is actually dragging the map, exit early |
|
// to avoid performance hits. |
|
if (_downLock) return; |
|
|
|
var pos = wax.u.eventoffset(e), |
|
tile = getTile(pos), |
|
feature; |
|
|
|
if (tile) gm.getGrid(tile.src, function(err, g) { |
|
if (err || !g) return; |
|
feature = g.tileFeature(pos.x, pos.y, tile); |
|
if (feature) { |
|
bean.fire(interaction, 'on', { |
|
parent: parent(), |
|
data: feature, |
|
formatter: gm.formatter().format, |
|
e: e |
|
}); |
|
} else { |
|
bean.fire(interaction, 'off'); |
|
} |
|
}); |
|
} |
|
|
|
// A handler for 'down' events - which means `mousedown` and `touchstart` |
|
function onDown(e) { |
|
// Ignore double-clicks by ignoring clicks within 300ms of |
|
// each other. |
|
if (killTimeout()) { return; } |
|
|
|
// Prevent interaction offset calculations happening while |
|
// the user is dragging the map. |
|
// |
|
// Store this event so that we can compare it to the |
|
// up event |
|
_downLock = true; |
|
_d = wax.u.eventoffset(e); |
|
if (e.type === 'mousedown') { |
|
bean.add(document.body, 'mouseup', onUp); |
|
|
|
// Only track single-touches. Double-touches will not affect this |
|
// control |
|
} else if (e.type === 'touchstart' && e.touches.length === 1) { |
|
// Don't make the user click close if they hit another tooltip |
|
bean.fire(interaction, 'off'); |
|
// Touch moves invalidate touches |
|
bean.add(parent(), touchEnds); |
|
} |
|
} |
|
|
|
function touchCancel() { |
|
bean.remove(parent(), touchEnds); |
|
_downLock = false; |
|
} |
|
|
|
function onUp(e) { |
|
var evt = {}, |
|
pos = wax.u.eventoffset(e); |
|
_downLock = false; |
|
|
|
// TODO: refine |
|
for (var key in e) { |
|
evt[key] = e[key]; |
|
} |
|
|
|
bean.remove(document.body, 'mouseup', onUp); |
|
bean.remove(parent(), touchEnds); |
|
|
|
if (e.type === 'touchend') { |
|
// If this was a touch and it survived, there's no need to avoid a double-tap |
|
// but also wax.u.eventoffset will have failed, since this touch |
|
// event doesn't have coordinates |
|
click(e, _d); |
|
} else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) && |
|
Math.round(pos.x / tol) === Math.round(_d.x / tol)) { |
|
// Contain the event data in a closure. |
|
_clickTimeout = window.setTimeout( |
|
function() { |
|
_clickTimeout = null; |
|
click(evt, pos); |
|
}, 300); |
|
} |
|
return onUp; |
|
} |
|
|
|
// Handle a click event. Takes a second |
|
function click(e, pos) { |
|
var tile = getTile(pos); |
|
if (tile) gm.getGrid(tile.src, function(err, g) { |
|
if (err || !g) return; |
|
var feature = g.tileFeature(pos.x, pos.y, tile); |
|
if (!feature) return; |
|
bean.fire(interaction, 'on', { |
|
parent: parent(), |
|
data: feature, |
|
formatter: gm.formatter().format, |
|
e: e |
|
}); |
|
}); |
|
} |
|
|
|
// set an attach function that should be |
|
// called when maps are set |
|
interaction.attach = function(x) { |
|
if (!arguments.length) return attach; |
|
attach = x; |
|
return interaction; |
|
}; |
|
|
|
interaction.detach = function(x) { |
|
if (!arguments.length) return detach; |
|
detach = x; |
|
return interaction; |
|
}; |
|
|
|
// Attach listeners to the map |
|
interaction.map = function(x) { |
|
if (!arguments.length) return map; |
|
map = x; |
|
if (attach) attach(map); |
|
bean.add(parent(), defaultEvents); |
|
bean.add(parent(), 'touchstart', onDown); |
|
return interaction; |
|
}; |
|
|
|
// set a grid getter for this control |
|
interaction.grid = function(x) { |
|
if (!arguments.length) return grid; |
|
grid = x; |
|
return interaction; |
|
}; |
|
|
|
// detach this and its events from the map cleanly |
|
interaction.remove = function(x) { |
|
if (detach) detach(map); |
|
bean.remove(parent(), defaultEvents); |
|
bean.fire(interaction, 'remove'); |
|
return interaction; |
|
}; |
|
|
|
// get or set a tilejson chunk of json |
|
interaction.tilejson = function(x) { |
|
if (!arguments.length) return gm.tilejson(); |
|
gm.tilejson(x); |
|
return interaction; |
|
}; |
|
|
|
// return the formatter, which has an exposed .format |
|
// function |
|
interaction.formatter = function() { |
|
return gm.formatter(); |
|
}; |
|
|
|
// ev can be 'on', 'off', fn is the handler |
|
interaction.on = function(ev, fn) { |
|
bean.add(interaction, ev, fn); |
|
return interaction; |
|
}; |
|
|
|
// ev can be 'on', 'off', fn is the handler |
|
interaction.off = function(ev, fn) { |
|
bean.remove(interaction, ev, fn); |
|
return interaction; |
|
}; |
|
|
|
// parent should be a function that returns |
|
// the parent element of the map |
|
interaction.parent = function(x) { |
|
parent = x; |
|
return interaction; |
|
}; |
|
|
|
return interaction; |
|
}; |
|
// Wax Legend |
|
// ---------- |
|
|
|
// Wax header |
|
var wax = wax || {}; |
|
|
|
wax.legend = function() { |
|
var element, |
|
legend = {}, |
|
container; |
|
|
|
function urlX(url) { |
|
// Data URIs are subject to a bug in Firefox |
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 |
|
// which let them be a vector. But WebKit does 'the right thing' |
|
// or at least 'something' about this situation, so we'll tolerate |
|
// them. |
|
if (/^(https?:\/\/|data:image)/.test(url)) { |
|
return url; |
|
} |
|
} |
|
|
|
function idX(id) { |
|
return id; |
|
} |
|
|
|
legend.element = function() { |
|
return container; |
|
}; |
|
|
|
legend.content = function(content) { |
|
if (!arguments.length) return element.innerHTML; |
|
if (content) { |
|
element.innerHTML = html_sanitize(content, urlX, idX); |
|
element.style.display = 'block'; |
|
} else { |
|
element.innerHTML = ''; |
|
element.style.display = 'none'; |
|
} |
|
return legend; |
|
}; |
|
|
|
legend.add = function() { |
|
container = document.createElement('div'); |
|
container.className = 'wax-legends'; |
|
|
|
element = container.appendChild(document.createElement('div')); |
|
element.className = 'wax-legend'; |
|
element.style.display = 'none'; |
|
return legend; |
|
}; |
|
|
|
return legend.add(); |
|
}; |
|
var wax = wax || {}; |
|
wax.movetip = {}; |
|
|
|
wax.movetip = function() { |
|
var popped = false, |
|
t = {}, |
|
_tooltipOffset, |
|
_contextOffset, |
|
tooltip, |
|
parent; |
|
|
|
function moveTooltip(e) { |
|
var eo = wax.u.eventoffset(e); |
|
// faux-positioning |
|
if ((_tooltipOffset.height + eo.y) > |
|
(_contextOffset.top + _contextOffset.height) && |
|
(_contextOffset.height > _tooltipOffset.height)) { |
|
eo.y -= _tooltipOffset.height; |
|
tooltip.className += ' flip-y'; |
|
} |
|
|
|
// faux-positioning |
|
if ((_tooltipOffset.width + eo.x) > |
|
(_contextOffset.left + _contextOffset.width)) { |
|
eo.x -= _tooltipOffset.width; |
|
tooltip.className += ' flip-x'; |
|
} |
|
|
|
tooltip.style.left = eo.x + 'px'; |
|
tooltip.style.top = eo.y + 'px'; |
|
} |
|
|
|
// Get the active tooltip for a layer or create a new one if no tooltip exists. |
|
// Hide any tooltips on layers underneath this one. |
|
function getTooltip(feature) { |
|
var tooltip = document.createElement('div'); |
|
tooltip.className = 'wax-tooltip wax-tooltip-0'; |
|
tooltip.innerHTML = feature; |
|
return tooltip; |
|
} |
|
|
|
// Hide a given tooltip. |
|
function hide() { |
|
if (tooltip) { |
|
tooltip.parentNode.removeChild(tooltip); |
|
tooltip = null; |
|
} |
|
} |
|
|
|
function on(o) { |
|
var content; |
|
if (popped) return; |
|
if ((o.e.type === 'mousemove' || !o.e.type)) { |
|
content = o.formatter({ format: 'teaser' }, o.data); |
|
if (!content) return; |
|
hide(); |
|
parent.style.cursor = 'pointer'; |
|
tooltip = document.body.appendChild(getTooltip(content)); |
|
} else { |
|
content = o.formatter({ format: 'teaser' }, o.data); |
|
if (!content) return; |
|
hide(); |
|
var tt = document.body.appendChild(getTooltip(content)); |
|
tt.className += ' wax-popup'; |
|
|
|
var close = tt.appendChild(document.createElement('a')); |
|
close.href = '#close'; |
|
close.className = 'close'; |
|
close.innerHTML = 'Close'; |
|
|
|
popped = true; |
|
|
|
tooltip = tt; |
|
|
|
_tooltipOffset = wax.u.offset(tooltip); |
|
_contextOffset = wax.u.offset(parent); |
|
moveTooltip(o.e); |
|
|
|
bean.add(close, 'click touchend', function closeClick(e) { |
|
e.stop(); |
|
hide(); |
|
popped = false; |
|
}); |
|
} |
|
if (tooltip) { |
|
_tooltipOffset = wax.u.offset(tooltip); |
|
_contextOffset = wax.u.offset(parent); |
|
moveTooltip(o.e); |
|
} |
|
|
|
} |
|
|
|
function off() { |
|
parent.style.cursor = 'default'; |
|
if (!popped) hide(); |
|
} |
|
|
|
t.parent = function(x) { |
|
if (!arguments.length) return parent; |
|
parent = x; |
|
return t; |
|
}; |
|
|
|
t.events = function() { |
|
return { |
|
on: on, |
|
off: off |
|
}; |
|
}; |
|
|
|
return t; |
|
}; |
|
|
|
// Wax GridUtil |
|
// ------------ |
|
|
|
// Wax header |
|
var wax = wax || {}; |
|
|
|
// Request |
|
// ------- |
|
// Request data cache. `callback(data)` where `data` is the response data. |
|
wax.request = { |
|
cache: {}, |
|
locks: {}, |
|
promises: {}, |
|
get: function(url, callback) { |
|
// Cache hit. |
|
if (this.cache[url]) { |
|
return callback(this.cache[url][0], this.cache[url][1]); |
|
// Cache miss. |
|
} else { |
|
this.promises[url] = this.promises[url] || []; |
|
this.promises[url].push(callback); |
|
// Lock hit. |
|
if (this.locks[url]) return; |
|
// Request. |
|
var that = this; |
|
this.locks[url] = true; |
|
reqwest({ |
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid', |
|
type: 'jsonp', |
|
jsonpCallback: 'callback', |
|
success: function(data) { |
|
that.locks[url] = false; |
|
that.cache[url] = [null, data]; |
|
for (var i = 0; i < that.promises[url].length; i++) { |
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]); |
|
} |
|
}, |
|
error: function(err) { |
|
that.locks[url] = false; |
|
that.cache[url] = [err, null]; |
|
for (var i = 0; i < that.promises[url].length; i++) { |
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]); |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
// Templating |
|
// --------- |
|
wax.template = function(x) { |
|
var template = {}; |
|
|
|
function urlX(url) { |
|
// Data URIs are subject to a bug in Firefox |
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 |
|
// which let them be a vector. But WebKit does 'the right thing' |
|
// or at least 'something' about this situation, so we'll tolerate |
|
// them. |
|
if (/^(https?:\/\/|data:image)/.test(url)) { |
|
return url; |
|
} |
|
} |
|
|
|
function idX(id) { |
|
return id; |
|
} |
|
|
|
// Clone the data object such that the '__[format]__' key is only |
|
// set for this instance of templating. |
|
template.format = function(options, data) { |
|
var clone = {}; |
|
for (var key in data) { |
|
clone[key] = data[key]; |
|
} |
|
if (options.format) { |
|
clone['__' + options.format + '__'] = true; |
|
} |
|
return html_sanitize(Mustache.to_html(x, clone), urlX, idX); |
|
}; |
|
|
|
return template; |
|
}; |
|
if (!wax) var wax = {}; |
|
|
|
// A wrapper for reqwest jsonp to easily load TileJSON from a URL. |
|
wax.tilejson = function(url, callback) { |
|
reqwest({ |
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid', |
|
type: 'jsonp', |
|
jsonpCallback: 'callback', |
|
success: callback, |
|
error: callback |
|
}); |
|
}; |
|
var wax = wax || {}; |
|
wax.tooltip = {}; |
|
|
|
wax.tooltip = function() { |
|
var popped = false, |
|
animate = false, |
|
t = {}, |
|
tooltips = [], |
|
_currentContent, |
|
transitionEvent, |
|
parent; |
|
|
|
if (document.body.style['-webkit-transition'] !== undefined) { |
|
transitionEvent = 'webkitTransitionEnd'; |
|
} else if (document.body.style.MozTransition !== undefined) { |
|
transitionEvent = 'transitionend'; |
|
} |
|
|
|
// Get the active tooltip for a layer or create a new one if no tooltip exists. |
|
// Hide any tooltips on layers underneath this one. |
|
function getTooltip(feature) { |
|
var tooltip = document.createElement('div'); |
|
tooltip.className = 'wax-tooltip wax-tooltip-0'; |
|
tooltip.innerHTML = feature; |
|
return tooltip; |
|
} |
|
|
|
|
|
function remove() { |
|
if (this.parentNode) this.parentNode.removeChild(this); |
|
} |
|
|
|
// Hide a given tooltip. |
|
function hide() { |
|
var _ct; |
|
while (_ct = tooltips.pop()) { |
|
if (animate && transitionEvent) { |
|
// This code assumes that transform-supporting browsers |
|
// also support proper events. IE9 does both. |
|
bean.add(_ct, transitionEvent, remove); |
|
_ct.className += ' wax-fade'; |
|
} else { |
|
if (_ct.parentNode) _ct.parentNode.removeChild(_ct); |
|
} |
|
} |
|
} |
|
|
|
function on(o) { |
|
var content; |
|
if ((o.e.type === 'mousemove' || !o.e.type) && !popped) { |
|
content = o.content || o.formatter({ format: 'teaser' }, o.data); |
|
if (!content || content == _currentContent) return; |
|
hide(); |
|
parent.style.cursor = 'pointer'; |
|
tooltips.push(parent.appendChild(getTooltip(content))); |
|
_currentContent = content; |
|
} else { |
|
content = o.content || o.formatter({ format: 'full' }, o.data); |
|
if (!content) { |
|
if (o.e.type && o.e.type.match(/touch/)) { |
|
// fallback possible |
|
content = o.content || o.formatter({ format: 'teaser' }, o.data); |
|
} |
|
// but if that fails, return just the same. |
|
if (!content) return; |
|
} |
|
hide(); |
|
parent.style.cursor = 'pointer'; |
|
var tt = parent.appendChild(getTooltip(content)); |
|
tt.className += ' wax-popup'; |
|
|
|
var close = tt.appendChild(document.createElement('a')); |
|
close.href = '#close'; |
|
close.className = 'close'; |
|
close.innerHTML = 'Close'; |
|
popped = true; |
|
|
|
tooltips.push(tt); |
|
|
|
bean.add(close, 'click touchend', function closeClick(e) { |
|
e.stop(); |
|
hide(); |
|
popped = false; |
|
}); |
|
} |
|
} |
|
|
|
function off() { |
|
parent.style.cursor = 'default'; |
|
_currentContent = null; |
|
if (!popped) hide(); |
|
} |
|
|
|
t.parent = function(x) { |
|
if (!arguments.length) return parent; |
|
parent = x; |
|
return t; |
|
}; |
|
|
|
t.animate = function(x) { |
|
if (!arguments.length) return animate; |
|
animate = x; |
|
return t; |
|
}; |
|
|
|
t.events = function() { |
|
return { |
|
on: on, |
|
off: off |
|
}; |
|
}; |
|
|
|
return t; |
|
}; |
|
var wax = wax || {}; |
|
|
|
// Utils are extracted from other libraries or |
|
// written from scratch to plug holes in browser compatibility. |
|
wax.u = { |
|
// From Bonzo |
|
offset: function(el) { |
|
// TODO: window margins |
|
// |
|
// Okay, so fall back to styles if offsetWidth and height are botched |
|
// by Firefox. |
|
var width = el.offsetWidth || parseInt(el.style.width, 10), |
|
height = el.offsetHeight || parseInt(el.style.height, 10), |
|
doc_body = document.body, |
|
top = 0, |
|
left = 0; |
|
|
|
var calculateOffset = function(el) { |
|
if (el === doc_body || el === document.documentElement) return; |
|
top += el.offsetTop; |
|
left += el.offsetLeft; |
|
|
|
var style = el.style.transform || |
|
el.style.WebkitTransform || |
|
el.style.OTransform || |
|
el.style.MozTransform || |
|
el.style.msTransform; |
|
|
|
if (style) { |
|
if (match = style.match(/translate\((.+)px, (.+)px\)/)) { |
|
top += parseInt(match[2], 10); |
|
left += parseInt(match[1], 10); |
|
} else if (match = style.match(/translate3d\((.+)px, (.+)px, (.+)px\)/)) { |
|
top += parseInt(match[2], 10); |
|
left += parseInt(match[1], 10); |
|
} else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) { |
|
var pts = match[1].split(','); |
|
top += parseInt(pts[13], 10); |
|
left += parseInt(pts[12], 10); |
|
} else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) { |
|
top += parseInt(match[2], 10); |
|
left += parseInt(match[1], 10); |
|
} |
|
} |
|
}; |
|
|
|
calculateOffset(el); |
|
|
|
try { |
|
while (el = el.offsetParent) { calculateOffset(el); } |
|
} catch(e) { |
|
// Hello, internet explorer. |
|
} |
|
|
|
// Offsets from the body |
|
top += doc_body.offsetTop; |
|
left += doc_body.offsetLeft; |
|
// Offsets from the HTML element |
|
top += doc_body.parentNode.offsetTop; |
|
left += doc_body.parentNode.offsetLeft; |
|
|
|
// Firefox and other weirdos. Similar technique to jQuery's |
|
// `doesNotIncludeMarginInBodyOffset`. |
|
var htmlComputed = document.defaultView ? |
|
window.getComputedStyle(doc_body.parentNode, null) : |
|
doc_body.parentNode.currentStyle; |
|
if (doc_body.parentNode.offsetTop !== |
|
parseInt(htmlComputed.marginTop, 10) && |
|
!isNaN(parseInt(htmlComputed.marginTop, 10))) { |
|
top += parseInt(htmlComputed.marginTop, 10); |
|
left += parseInt(htmlComputed.marginLeft, 10); |
|
} |
|
|
|
return { |
|
top: top, |
|
left: left, |
|
height: height, |
|
width: width |
|
}; |
|
}, |
|
|
|
'$': function(x) { |
|
return (typeof x === 'string') ? |
|
document.getElementById(x) : |
|
x; |
|
}, |
|
// IE doesn't have indexOf |
|
indexOf: function(array, item) { |
|
var nativeIndexOf = Array.prototype.indexOf; |
|
if (array === null) return -1; |
|
var i, l; |
|
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); |
|
for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; |
|
return -1; |
|
}, |
|
// From underscore: reimplement the ECMA5 `Object.keys()` method |
|
keys: Object.keys || function(obj) { |
|
var ho = Object.prototype.hasOwnProperty; |
|
if (obj !== Object(obj)) throw new TypeError('Invalid object'); |
|
var keys = []; |
|
for (var key in obj) if (ho.call(obj, key)) keys[keys.length] = key; |
|
return keys; |
|
}, |
|
// From quirksmode: normalize the offset of an event from the top-left |
|
// of the page. |
|
eventoffset: function(e) { |
|
var posx = 0; |
|
var posy = 0; |
|
if (!e) { e = window.event; } |
|
if (e.pageX || e.pageY) { |
|
// Good browsers |
|
return { |
|
x: e.pageX, |
|
y: e.pageY |
|
}; |
|
} else if (e.clientX || e.clientY) { |
|
// Internet Explorer |
|
var doc = document.documentElement, body = document.body; |
|
var htmlComputed = document.body.parentNode.currentStyle; |
|
var topMargin = parseInt(htmlComputed.marginTop, 10) || 0; |
|
var leftMargin = parseInt(htmlComputed.marginLeft, 10) || 0; |
|
return { |
|
x: e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - |
|
(doc && doc.clientLeft || body && body.clientLeft || 0) + leftMargin, |
|
y: e.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - |
|
(doc && doc.clientTop || body && body.clientTop || 0) + topMargin |
|
}; |
|
} else if (e.touches && e.touches.length === 1) { |
|
// Touch browsers |
|
return { |
|
x: e.touches[0].pageX, |
|
y: e.touches[0].pageY |
|
}; |
|
} |
|
}, |
|
|
|
// Ripped from underscore.js |
|
// Internal function used to implement `_.throttle` and `_.debounce`. |
|
limit: function(func, wait, debounce) { |
|
var timeout; |
|
return function() { |
|
var context = this, args = arguments; |
|
var throttler = function() { |
|
timeout = null; |
|
func.apply(context, args); |
|
}; |
|
if (debounce) clearTimeout(timeout); |
|
if (debounce || !timeout) timeout = setTimeout(throttler, wait); |
|
}; |
|
}, |
|
|
|
// Returns a function, that, when invoked, will only be triggered at most once |
|
// during a given window of time. |
|
throttle: function(func, wait) { |
|
return this.limit(func, wait, false); |
|
} |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Attribution |
|
// ----------- |
|
// Attribution wrapper for Modest Maps. |
|
wax.mm.attribution = function(map, tilejson) { |
|
tilejson = tilejson || {}; |
|
var a, // internal attribution control |
|
attribution = {}; |
|
|
|
attribution.element = function() { |
|
return a.element(); |
|
}; |
|
|
|
attribution.appendTo = function(elem) { |
|
wax.u.$(elem).appendChild(a.element()); |
|
return this; |
|
}; |
|
|
|
attribution.init = function() { |
|
a = wax.attribution(); |
|
a.content(tilejson.attribution); |
|
a.element().className = 'wax-attribution wax-mm'; |
|
return this; |
|
}; |
|
|
|
return attribution.init(); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Box Selector |
|
// ------------ |
|
wax.mm.boxselector = function(map, tilejson, opts) { |
|
var corner = null, |
|
nearCorner = null, |
|
callback = ((typeof opts === 'function') ? |
|
opts : |
|
opts.callback), |
|
boxDiv, |
|
style, |
|
borderWidth = 0, |
|
horizontal = false, // Whether the resize is horizontal |
|
vertical = false, |
|
edge = 5, // Distance from border sensitive to resizing |
|
addEvent = MM.addEvent, |
|
removeEvent = MM.removeEvent, |
|
box, |
|
boxselector = {}; |
|
|
|
function getMousePoint(e) { |
|
// start with just the mouse (x, y) |
|
var point = new MM.Point(e.clientX, e.clientY); |
|
// correct for scrolled document |
|
point.x += document.body.scrollLeft + document.documentElement.scrollLeft; |
|
point.y += document.body.scrollTop + document.documentElement.scrollTop; |
|
|
|
// correct for nested offsets in DOM |
|
for (var node = map.parent; node; node = node.offsetParent) { |
|
point.x -= node.offsetLeft; |
|
point.y -= node.offsetTop; |
|
} |
|
return point; |
|
} |
|
|
|
function mouseDown(e) { |
|
if (!e.shiftKey) return; |
|
|
|
corner = nearCorner = getMousePoint(e); |
|
horizontal = vertical = true; |
|
|
|
style.left = corner.x + 'px'; |
|
style.top = corner.y + 'px'; |
|
style.width = style.height = 0; |
|
|
|
addEvent(document, 'mousemove', mouseMove); |
|
addEvent(document, 'mouseup', mouseUp); |
|
|
|
map.parent.style.cursor = 'crosshair'; |
|
return MM.cancelEvent(e); |
|
} |
|
|
|
// Resize existing box |
|
function mouseDownResize(e) { |
|
var point = getMousePoint(e), |
|
TL = { |
|
x: parseInt(boxDiv.offsetLeft, 10), |
|
y: parseInt(boxDiv.offsetTop, 10) |
|
}, |
|
BR = { |
|
x: TL.x + parseInt(boxDiv.offsetWidth, 10), |
|
y: TL.y + parseInt(boxDiv.offsetHeight, 10) |
|
}; |
|
|
|
// Determine whether resize is horizontal, vertical or both |
|
horizontal = point.x - TL.x <= edge || BR.x - point.x <= edge; |
|
vertical = point.y - TL.y <= edge || BR.y - point.y <= edge; |
|
|
|
if (vertical || horizontal) { |
|
corner = { |
|
x: (point.x - TL.x < BR.x - point.x) ? BR.x : TL.x, |
|
y: (point.y - TL.y < BR.y - point.y) ? BR.y : TL.y |
|
}; |
|
nearCorner = { |
|
x: (point.x - TL.x < BR.x - point.x) ? TL.x : BR.x, |
|
y: (point.y - TL.y < BR.y - point.y) ? TL.y : BR.y |
|
}; |
|
addEvent(document, 'mousemove', mouseMove); |
|
addEvent(document, 'mouseup', mouseUp); |
|
return MM.cancelEvent(e); |
|
} |
|
} |
|
|
|
function mouseMove(e) { |
|
var point = getMousePoint(e); |
|
style.display = 'block'; |
|
if (horizontal) { |
|
style.left = (point.x < corner.x ? point.x : corner.x) + 'px'; |
|
style.width = Math.abs(point.x - corner.x) - 2 * borderWidth + 'px'; |
|
} |
|
if (vertical) { |
|
style.top = (point.y < corner.y ? point.y : corner.y) + 'px'; |
|
style.height = Math.abs(point.y - corner.y) - 2 * borderWidth + 'px'; |
|
} |
|
changeCursor(point, map.parent); |
|
return MM.cancelEvent(e); |
|
} |
|
|
|
function mouseUp(e) { |
|
var point = getMousePoint(e), |
|
l1 = map.pointLocation( new MM.Point( |
|
horizontal ? point.x : nearCorner.x, |
|
vertical? point.y : nearCorner.y |
|
)); |
|
l2 = map.pointLocation(corner); |
|
|
|
// Format coordinates like mm.map.getExtent(). |
|
boxselector.extent([ |
|
new MM.Location( |
|
Math.max(l1.lat, l2.lat), |
|
Math.min(l1.lon, l2.lon)), |
|
new MM.Location( |
|
Math.min(l1.lat, l2.lat), |
|
Math.max(l1.lon, l2.lon)) |
|
]); |
|
|
|
removeEvent(document, 'mousemove', mouseMove); |
|
removeEvent(document, 'mouseup', mouseUp); |
|
|
|
map.parent.style.cursor = 'auto'; |
|
} |
|
|
|
function mouseMoveCursor(e) { |
|
changeCursor(getMousePoint(e), boxDiv); |
|
} |
|
|
|
// Set resize cursor if mouse is on edge |
|
function changeCursor(point, elem) { |
|
var TL = { |
|
x: parseInt(boxDiv.offsetLeft, 10), |
|
y: parseInt(boxDiv.offsetTop, 10) |
|
}, |
|
BR = { |
|
x: TL.x + parseInt(boxDiv.offsetWidth, 10), |
|
y: TL.y + parseInt(boxDiv.offsetHeight, 10) |
|
}; |
|
// Build cursor style string |
|
var prefix = ''; |
|
if (point.y - TL.y <= edge) prefix = 'n'; |
|
else if (BR.y - point.y <= edge) prefix = 's'; |
|
if (point.x - TL.x <= edge) prefix += 'w'; |
|
else if (BR.x - point.x <= edge) prefix += 'e'; |
|
if (prefix !== '') prefix += '-resize'; |
|
elem.style.cursor = prefix; |
|
} |
|
|
|
function drawbox(map, e) { |
|
if (!boxDiv || !box) return; |
|
var br = map.locationPoint(box[1]), |
|
tl = map.locationPoint(box[0]), |
|
style = boxDiv.style; |
|
|
|
style.display = 'block'; |
|
style.height = 'auto'; |
|
style.width = 'auto'; |
|
style.left = Math.max(0, tl.x) + 'px'; |
|
style.top = Math.max(0, tl.y) + 'px'; |
|
style.right = Math.max(0, map.dimensions.x - br.x) + 'px'; |
|
style.bottom = Math.max(0, map.dimensions.y - br.y) + 'px'; |
|
} |
|
|
|
boxselector.extent = function(x, silent) { |
|
if (!x) return box; |
|
|
|
box = [ |
|
new MM.Location( |
|
Math.max(x[0].lat, x[1].lat), |
|
Math.min(x[0].lon, x[1].lon)), |
|
new MM.Location( |
|
Math.min(x[0].lat, x[1].lat), |
|
Math.max(x[0].lon, x[1].lon)) |
|
]; |
|
|
|
drawbox(map); |
|
|
|
if (!silent) callback(box); |
|
}; |
|
|
|
boxselector.add = function(map) { |
|
boxDiv = boxDiv || document.createElement('div'); |
|
boxDiv.id = map.parent.id + '-boxselector-box'; |
|
boxDiv.className = 'boxselector-box'; |
|
map.parent.appendChild(boxDiv); |
|
style = boxDiv.style; |
|
borderWidth = parseInt(window.getComputedStyle(boxDiv).borderWidth, 10); |
|
|
|
addEvent(map.parent, 'mousedown', mouseDown); |
|
addEvent(boxDiv, 'mousedown', mouseDownResize); |
|
addEvent(map.parent, 'mousemove', mouseMoveCursor); |
|
map.addCallback('drawn', drawbox); |
|
return this; |
|
}; |
|
|
|
boxselector.remove = function() { |
|
map.parent.removeChild(boxDiv); |
|
removeEvent(map.parent, 'mousedown', mouseDown); |
|
removeEvent(boxDiv, 'mousedown', mouseDownResize); |
|
removeEvent(map.parent, 'mousemove', mouseMoveCursor); |
|
map.removeCallback('drawn', drawbox); |
|
}; |
|
|
|
return boxselector.add(map); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
wax._ = {}; |
|
|
|
// Bandwidth Detection |
|
// ------------------ |
|
wax.mm.bwdetect = function(map, options) { |
|
options = options || {}; |
|
var lowpng = options.png || '.png128', |
|
lowjpg = options.jpg || '.jpg70', |
|
bw = false; |
|
|
|
wax._.bw_png = lowpng; |
|
wax._.bw_jpg = lowjpg; |
|
|
|
return wax.bwdetect(options, function(x) { |
|
wax._.bw = !x; |
|
for (var i = 0; i < map.layers.length; i++) { |
|
if (map.getLayerAt(i).provider instanceof wax.mm.connector) { |
|
map.getLayerAt(i).setProvider(map.getLayerAt(i).provider); |
|
} |
|
} |
|
}); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Fullscreen |
|
// ---------- |
|
// A simple fullscreen control for Modest Maps |
|
|
|
// Add zoom links, which can be styled as buttons, to a `modestmaps.Map` |
|
// control. This function can be used chaining-style with other |
|
// chaining-style controls. |
|
wax.mm.fullscreen = function(map) { |
|
// true: fullscreen |
|
// false: minimized |
|
var fullscreened = false, |
|
fullscreen = {}, |
|
a, |
|
body = document.body, |
|
smallSize; |
|
|
|
function click(e) { |
|
if (e) e.stop(); |
|
if (fullscreened) { |
|
fullscreen.original(); |
|
} else { |
|
fullscreen.full(); |
|
} |
|
} |
|
|
|
function ss(w, h) { |
|
map.dimensions = new MM.Point(w, h); |
|
map.parent.style.width = Math.round(map.dimensions.x) + 'px'; |
|
map.parent.style.height = Math.round(map.dimensions.y) + 'px'; |
|
map.dispatchCallback('resized', map.dimensions); |
|
} |
|
|
|
// Modest Maps demands an absolute height & width, and doesn't auto-correct |
|
// for changes, so here we save the original size of the element and |
|
// restore to that size on exit from fullscreen. |
|
fullscreen.add = function(map) { |
|
a = document.createElement('a'); |
|
a.className = 'wax-fullscreen'; |
|
a.href = '#fullscreen'; |
|
a.innerHTML = 'fullscreen'; |
|
bean.add(a, 'click', click); |
|
return this; |
|
}; |
|
fullscreen.full = function() { |
|
if (fullscreened) { return; } else { fullscreened = true; } |
|
smallSize = [map.parent.offsetWidth, map.parent.offsetHeight]; |
|
map.parent.className += ' wax-fullscreen-map'; |
|
body.className += ' wax-fullscreen-view'; |
|
ss(map.parent.offsetWidth, map.parent.offsetHeight); |
|
}; |
|
fullscreen.original = function() { |
|
if (!fullscreened) { return; } else { fullscreened = false; } |
|
map.parent.className = map.parent.className.replace(' wax-fullscreen-map', ''); |
|
body.className = body.className.replace(' wax-fullscreen-view', ''); |
|
ss(smallSize[0], smallSize[1]); |
|
}; |
|
fullscreen.appendTo = function(elem) { |
|
wax.u.$(elem).appendChild(a); |
|
return this; |
|
}; |
|
|
|
return fullscreen.add(map); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
wax.mm.hash = function(map) { |
|
return wax.hash({ |
|
getCenterZoom: function() { |
|
var center = map.getCenter(), |
|
zoom = map.getZoom(), |
|
precision = Math.max( |
|
0, |
|
Math.ceil(Math.log(zoom) / Math.LN2)); |
|
|
|
return [zoom.toFixed(2), |
|
center.lat.toFixed(precision), |
|
center.lon.toFixed(precision) |
|
].join('/'); |
|
}, |
|
setCenterZoom: function setCenterZoom(args) { |
|
map.setCenterZoom( |
|
new MM.Location(args[1], args[2]), |
|
args[0]); |
|
}, |
|
bindChange: function(fn) { |
|
map.addCallback('drawn', fn); |
|
}, |
|
unbindChange: function(fn) { |
|
map.removeCallback('drawn', fn); |
|
} |
|
}); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
wax.mm.interaction = function() { |
|
var dirty = false, |
|
_grid, |
|
map, |
|
clearingEvents = ['zoomed', 'panned', 'centered', |
|
'extentset', 'resized', 'drawn']; |
|
|
|
function grid() { |
|
var zoomLayer = map.getLayerAt(0) |
|
.levels[Math.round(map.getZoom())]; |
|
if (!dirty && _grid !== undefined && _grid.length) { |
|
return _grid; |
|
} else { |
|
_grid = (function(t) { |
|
var o = []; |
|
for (var key in t) { |
|
if (t[key].parentNode === zoomLayer) { |
|
var offset = wax.u.offset(t[key]); |
|
o.push([ |
|
offset.top, |
|
offset.left, |
|
t[key] |
|
]); |
|
} |
|
} |
|
return o; |
|
})(map.getLayerAt(0).tiles); |
|
return _grid; |
|
} |
|
} |
|
|
|
function setdirty() { dirty = true; } |
|
|
|
function attach(x) { |
|
if (!arguments.length) return map; |
|
map = x; |
|
for (var i = 0; i < clearingEvents.length; i++) { |
|
map.addCallback(clearingEvents[i], setdirty); |
|
} |
|
} |
|
|
|
function detach(x) { |
|
for (var i = 0; i < clearingEvents.length; i++) { |
|
map.removeCallback(clearingEvents[i], setdirty); |
|
} |
|
} |
|
|
|
return wax.interaction() |
|
.attach(attach) |
|
.detach(detach) |
|
.parent(function() { |
|
return map.parent; |
|
}) |
|
.grid(grid); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// LatLng |
|
// ------ |
|
// Show the current cursor position in |
|
// lat/long |
|
wax.mm.latlngtooltip = function(map) { |
|
var tt, // tooltip |
|
_down = false, |
|
latlng = {}; |
|
|
|
function getMousePoint(e) { |
|
// start with just the mouse (x, y) |
|
var point = new MM.Point(e.clientX, e.clientY); |
|
// correct for scrolled document |
|
point.x += document.body.scrollLeft + document.documentElement.scrollLeft; |
|
point.y += document.body.scrollTop + document.documentElement.scrollTop; |
|
|
|
// correct for nested offsets in DOM |
|
for (var node = map.parent; node; node = node.offsetParent) { |
|
point.x -= node.offsetLeft; |
|
point.y -= node.offsetTop; |
|
} |
|
return point; |
|
} |
|
|
|
function onDown(e) { |
|
console.log('here'); |
|
_down = true; |
|
} |
|
|
|
function onUp(e) { |
|
_down = false; |
|
} |
|
|
|
function onMove(e) { |
|
if (!e.shiftKey || _down) { |
|
if (tt.parentNode === map.parent) { |
|
map.parent.removeChild(tt); |
|
} |
|
return; |
|
} |
|
|
|
var pt = getMousePoint(e), |
|
ll = map.pointLocation(pt), |
|
fmt = ll.lat.toFixed(2) + ', ' + ll.lon.toFixed(2); |
|
|
|
tt.innerHTML = fmt; |
|
pt.scale = pt.width = pt.height = 1; |
|
pt.x += 10; |
|
MM.moveElement(tt, pt); |
|
map.parent.appendChild(tt); |
|
} |
|
|
|
latlng.add = function() { |
|
MM.addEvent(map.parent, 'mousemove', onMove); |
|
MM.addEvent(map.parent, 'mousedown', onDown); |
|
MM.addEvent(map.parent, 'mouseup', onUp); |
|
tt = document.createElement('div'); |
|
tt.className = 'wax-latlngtooltip'; |
|
return this; |
|
}; |
|
|
|
latlng.remove = function() { |
|
MM.removeEvent(map.parent, 'mousemove', onMove); |
|
MM.removeEvent(map.parent, 'mousedown', onDown); |
|
MM.removeEvent(map.parent, 'mouseup', onUp); |
|
return this; |
|
}; |
|
|
|
return latlng.add(); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Legend Control |
|
// -------------- |
|
// The Modest Maps version of this control is a very, very |
|
// light wrapper around the `/lib` code for legends. |
|
wax.mm.legend = function(map, tilejson) { |
|
tilejson = tilejson || {}; |
|
var l, // parent legend |
|
legend = {}; |
|
|
|
legend.add = function() { |
|
l = wax.legend() |
|
.content(tilejson.legend || ''); |
|
return this; |
|
}; |
|
|
|
legend.content = function(x) { |
|
if (x) l.content(x.legend || ''); |
|
}; |
|
|
|
legend.element = function() { |
|
return l.element(); |
|
}; |
|
|
|
legend.appendTo = function(elem) { |
|
wax.u.$(elem).appendChild(l.element()); |
|
return this; |
|
}; |
|
|
|
return legend.add(); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Point Selector |
|
// -------------- |
|
// |
|
// This takes an object of options: |
|
// |
|
// * `callback`: a function called with an array of `com.modestmaps.Location` |
|
// objects when the map is edited |
|
// |
|
// It also exposes a public API function: `addLocation`, which adds a point |
|
// to the map as if added by the user. |
|
wax.mm.pointselector = function(map, tilejson, opts) { |
|
var mouseDownPoint = null, |
|
mouseUpPoint = null, |
|
tolerance = 5, |
|
overlayDiv, |
|
pointselector = {}, |
|
locations = []; |
|
|
|
var callback = (typeof opts === 'function') ? |
|
opts : |
|
opts.callback; |
|
|
|
// Create a `com.modestmaps.Point` from a screen event, like a click. |
|
function makePoint(e) { |
|
var coords = wax.u.eventoffset(e); |
|
var point = new MM.Point(coords.x, coords.y); |
|
// correct for scrolled document |
|
|
|
// and for the document |
|
var body = { |
|
x: parseFloat(MM.getStyle(document.documentElement, 'margin-left')), |
|
y: parseFloat(MM.getStyle(document.documentElement, 'margin-top')) |
|
}; |
|
|
|
if (!isNaN(body.x)) point.x -= body.x; |
|
if (!isNaN(body.y)) point.y -= body.y; |
|
|
|
// TODO: use wax.util.offset |
|
// correct for nested offsets in DOM |
|
for (var node = map.parent; node; node = node.offsetParent) { |
|
point.x -= node.offsetLeft; |
|
point.y -= node.offsetTop; |
|
} |
|
return point; |
|
} |
|
|
|
// Currently locations in this control contain circular references to elements. |
|
// These can't be JSON encoded, so here's a utility to clean the data that's |
|
// spit back. |
|
function cleanLocations(locations) { |
|
var o = []; |
|
for (var i = 0; i < locations.length; i++) { |
|
o.push(new MM.Location(locations[i].lat, locations[i].lon)); |
|
} |
|
return o; |
|
} |
|
|
|
// Attach this control to a map by registering callbacks |
|
// and adding the overlay |
|
|
|
// Redraw the points when the map is moved, so that they stay in the |
|
// correct geographic locations. |
|
function drawPoints() { |
|
var offset = new MM.Point(0, 0); |
|
for (var i = 0; i < locations.length; i++) { |
|
var point = map.locationPoint(locations[i]); |
|
if (!locations[i].pointDiv) { |
|
locations[i].pointDiv = document.createElement('div'); |
|
locations[i].pointDiv.className = 'wax-point-div'; |
|
locations[i].pointDiv.style.position = 'absolute'; |
|
locations[i].pointDiv.style.display = 'block'; |
|
// TODO: avoid circular reference |
|
locations[i].pointDiv.location = locations[i]; |
|
// Create this closure once per point |
|
bean.add(locations[i].pointDiv, 'mouseup', |
|
(function selectPointWrap(e) { |
|
var l = locations[i]; |
|
return function(e) { |
|
MM.removeEvent(map.parent, 'mouseup', mouseUp); |
|
pointselector.deleteLocation(l, e); |
|
}; |
|
})()); |
|
map.parent.appendChild(locations[i].pointDiv); |
|
} |
|
locations[i].pointDiv.style.left = point.x + 'px'; |
|
locations[i].pointDiv.style.top = point.y + 'px'; |
|
} |
|
} |
|
|
|
function mouseDown(e) { |
|
mouseDownPoint = makePoint(e); |
|
bean.add(map.parent, 'mouseup', mouseUp); |
|
} |
|
|
|
// Remove the awful circular reference from locations. |
|
// TODO: This function should be made unnecessary by not having it. |
|
function mouseUp(e) { |
|
if (!mouseDownPoint) return; |
|
mouseUpPoint = makePoint(e); |
|
if (MM.Point.distance(mouseDownPoint, mouseUpPoint) < tolerance) { |
|
pointselector.addLocation(map.pointLocation(mouseDownPoint)); |
|
callback(cleanLocations(locations)); |
|
} |
|
mouseDownPoint = null; |
|
} |
|
|
|
// API for programmatically adding points to the map - this |
|
// calls the callback for ever point added, so it can be symmetrical. |
|
// Useful for initializing the map when it's a part of a form. |
|
pointselector.addLocation = function(location) { |
|
locations.push(location); |
|
drawPoints(); |
|
callback(cleanLocations(locations)); |
|
}; |
|
|
|
pointselector.locations = function(x) { |
|
return locations; |
|
}; |
|
|
|
pointselector.add = function(map) { |
|
bean.add(map.parent, 'mousedown', mouseDown); |
|
map.addCallback('drawn', drawPoints); |
|
return this; |
|
}; |
|
|
|
pointselector.remove = function(map) { |
|
bean.remove(map.parent, 'mousedown', mouseDown); |
|
map.removeCallback('drawn', drawPoints); |
|
for (var i = locations.length - 1; i > -1; i--) { |
|
pointselector.deleteLocation(locations[i]); |
|
} |
|
return this; |
|
}; |
|
|
|
pointselector.deleteLocation = function(location, e) { |
|
if (!e || confirm('Delete this point?')) { |
|
location.pointDiv.parentNode.removeChild(location.pointDiv); |
|
locations.splice(wax.u.indexOf(locations, location), 1); |
|
callback(cleanLocations(locations)); |
|
} |
|
}; |
|
|
|
return pointselector.add(map); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// ZoomBox |
|
// ------- |
|
// An OL-style ZoomBox control, from the Modest Maps example. |
|
wax.mm.zoombox = function(map) { |
|
// TODO: respond to resize |
|
var zoombox = {}, |
|
drawing = false, |
|
box, |
|
mouseDownPoint = null; |
|
|
|
function getMousePoint(e) { |
|
// start with just the mouse (x, y) |
|
var point = new MM.Point(e.clientX, e.clientY); |
|
// correct for scrolled document |
|
point.x += document.body.scrollLeft + document.documentElement.scrollLeft; |
|
point.y += document.body.scrollTop + document.documentElement.scrollTop; |
|
|
|
// correct for nested offsets in DOM |
|
for (var node = map.parent; node; node = node.offsetParent) { |
|
point.x -= node.offsetLeft; |
|
point.y -= node.offsetTop; |
|
} |
|
return point; |
|
} |
|
|
|
function mouseUp(e) { |
|
if (!drawing) return; |
|
|
|
drawing = false; |
|
var point = getMousePoint(e); |
|
|
|
var l1 = map.pointLocation(point), |
|
l2 = map.pointLocation(mouseDownPoint); |
|
|
|
map.setExtent([l1, l2]); |
|
|
|
box.style.display = 'none'; |
|
MM.removeEvent(map.parent, 'mousemove', mouseMove); |
|
MM.removeEvent(map.parent, 'mouseup', mouseUp); |
|
|
|
map.parent.style.cursor = 'auto'; |
|
} |
|
|
|
function mouseDown(e) { |
|
if (!(e.shiftKey && !this.drawing)) return; |
|
|
|
drawing = true; |
|
mouseDownPoint = getMousePoint(e); |
|
|
|
box.style.left = mouseDownPoint.x + 'px'; |
|
box.style.top = mouseDownPoint.y + 'px'; |
|
|
|
MM.addEvent(map.parent, 'mousemove', mouseMove); |
|
MM.addEvent(map.parent, 'mouseup', mouseUp); |
|
|
|
map.parent.style.cursor = 'crosshair'; |
|
return MM.cancelEvent(e); |
|
} |
|
|
|
function mouseMove(e) { |
|
if (!drawing) return; |
|
|
|
var point = getMousePoint(e); |
|
box.style.display = 'block'; |
|
if (point.x < mouseDownPoint.x) { |
|
box.style.left = point.x + 'px'; |
|
} else { |
|
box.style.left = mouseDownPoint.x + 'px'; |
|
} |
|
box.style.width = Math.abs(point.x - mouseDownPoint.x) + 'px'; |
|
if (point.y < mouseDownPoint.y) { |
|
box.style.top = point.y + 'px'; |
|
} else { |
|
box.style.top = mouseDownPoint.y + 'px'; |
|
} |
|
box.style.height = Math.abs(point.y - mouseDownPoint.y) + 'px'; |
|
return MM.cancelEvent(e); |
|
} |
|
|
|
zoombox.add = function(map) { |
|
// Use a flag to determine whether the zoombox is currently being |
|
// drawn. Necessary only for IE because `mousedown` is triggered |
|
// twice. |
|
box = box || document.createElement('div'); |
|
box.id = map.parent.id + '-zoombox-box'; |
|
box.className = 'zoombox-box'; |
|
map.parent.appendChild(box); |
|
MM.addEvent(map.parent, 'mousedown', mouseDown); |
|
return this; |
|
}; |
|
|
|
zoombox.remove = function() { |
|
map.parent.removeChild(box); |
|
MM.removeEvent(map.parent, 'mousedown', mouseDown); |
|
}; |
|
|
|
return zoombox.add(map); |
|
}; |
|
wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// Zoomer |
|
// ------ |
|
// Add zoom links, which can be styled as buttons, to a `modestmaps.Map` |
|
// control. This function can be used chaining-style with other |
|
// chaining-style controls. |
|
wax.mm.zoomer = function(map) { |
|
var zoomin = document.createElement('a'); |
|
zoomin.innerHTML = '+'; |
|
zoomin.href = '#'; |
|
zoomin.className = 'zoomer zoomin'; |
|
bean.add(zoomin, 'mousedown dblclick', function(e) { |
|
e.stop(); |
|
}); |
|
bean.add(zoomin, 'click', function(e) { |
|
e.stop(); |
|
map.zoomIn(); |
|
}, false); |
|
|
|
var zoomout = document.createElement('a'); |
|
zoomout.innerHTML = '-'; |
|
zoomout.href = '#'; |
|
zoomout.className = 'zoomer zoomout'; |
|
bean.add(zoomout, 'mousedown dblclick', function(e) { |
|
e.stop(); |
|
}); |
|
bean.add(zoomout, 'click', function(e) { |
|
e.stop(); |
|
map.zoomOut(); |
|
}); |
|
|
|
var zoomer = { |
|
add: function(map) { |
|
map.addCallback('drawn', function(map, e) { |
|
if (map.coordinate.zoom === map.coordLimits[0].zoom) { |
|
zoomout.className = 'zoomer zoomout zoomdisabled'; |
|
} else if (map.coordinate.zoom === map.coordLimits[1].zoom) { |
|
zoomin.className = 'zoomer zoomin zoomdisabled'; |
|
} else { |
|
zoomin.className = 'zoomer zoomin'; |
|
zoomout.className = 'zoomer zoomout'; |
|
} |
|
}); |
|
return this; |
|
}, |
|
appendTo: function(elem) { |
|
wax.u.$(elem).appendChild(zoomin); |
|
wax.u.$(elem).appendChild(zoomout); |
|
return this; |
|
} |
|
}; |
|
return zoomer.add(map); |
|
}; |
|
var wax = wax || {}; |
|
wax.mm = wax.mm || {}; |
|
|
|
// A layer connector for Modest Maps conformant to TileJSON |
|
// https://github.com/mapbox/tilejson |
|
wax.mm._provider = function(options) { |
|
this.options = { |
|
tiles: options.tiles, |
|
scheme: options.scheme || 'xyz', |
|
minzoom: options.minzoom || 0, |
|
maxzoom: options.maxzoom || 22, |
|
bounds: options.bounds || [-180, -90, 180, 90] |
|
}; |
|
}; |
|
|
|
wax.mm._provider.prototype = { |
|
outerLimits: function() { |
|
return [ |
|
this.locationCoordinate( |
|
new MM.Location( |
|
this.options.bounds[0], |
|
this.options.bounds[1])).zoomTo(this.options.minzoom), |
|
this.locationCoordinate( |
|
new MM.Location( |
|
this.options.bounds[2], |
|
this.options.bounds[3])).zoomTo(this.options.maxzoom) |
|
]; |
|
}, |
|
getTile: function(c) { |
|
if (!(coord = this.sourceCoordinate(c))) return null; |
|
if (coord.zoom < this.options.minzoom || coord.zoom > this.options.maxzoom) return null; |
|
|
|
coord.row = (this.options.scheme === 'tms') ? |
|
Math.pow(2, coord.zoom) - coord.row - 1 : |
|
coord.row; |
|
|
|
var u = this.options.tiles[parseInt(Math.pow(2, coord.zoom) * coord.row + coord.column, 10) % |
|
this.options.tiles.length] |
|
.replace('{z}', coord.zoom.toFixed(0)) |
|
.replace('{x}', coord.column.toFixed(0)) |
|
.replace('{y}', coord.row.toFixed(0)); |
|
|
|
if (wax._ && wax._.bw) { |
|
u = u.replace('.png', wax._.bw_png) |
|
.replace('.jpg', wax._.bw_jpg); |
|
} |
|
|
|
return u; |
|
} |
|
}; |
|
|
|
if (MM) { |
|
MM.extend(wax.mm._provider, MM.MapProvider); |
|
} |
|
|
|
wax.mm.connector = function(options) { |
|
var x = new wax.mm._provider(options); |
|
return new MM.Layer(x); |
|
}; |