Skip to content

Instantly share code, notes, and snippets.

@wboykinm
Created November 1, 2012 17:41
Show Gist options
  • Save wboykinm/3995290 to your computer and use it in GitHub Desktop.
Save wboykinm/3995290 to your computer and use it in GitHub Desktop.
Wax for Google maps 6.0.4 updated w/ interaction.remove(); handler
/* wax - 7.0.0dev10 - v6.0.4-99-gbe8ba88 */
!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: &pi; is different from &Pi;
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 &lt; 2 &amp;&AMP; 4 &gt; 3&#10;')
* # '1 < 2 && 4 > 3\n'
* $ unescapeEntities('&lt;&lt <- unfinished entity&gt;')
* # '<&lt <- unfinished entity>'
* $ unescapeEntities('/foo?bar=baz&copy=true') // & often unescaped in URLS
* # '/foo?bar=baz&copy=true'
* $ unescapeEntities('pi=&pi;&#x3c0;, 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.
* # '&#34;&lt;&lt;&amp;&#61;&#61;&amp;&gt;&gt;&#34;'
* $ escapeAttrib('Hello <World>!')
* # 'Hello &lt;World&gt;!'
* }
*/
function escapeAttrib(s) {
// Escaping '=' defangs many UTF-7 and SGML short-tag attacks.
return s.replace(ampRe, '&amp;').replace(ltRe, '&lt;').replace(gtRe, '&gt;')
.replace(quotRe, '&#34;').replace(eqRe, '&#61;');
}
/**
* Escape entities in RCDATA that can be escaped without changing the meaning.
* {@updoc
* $ normalizeRCData('1 < 2 &&amp; 3 > 4 &amp;& 5 &lt; 7&8')
* # '1 &lt; 2 &amp;&amp; 3 &gt; 4 &amp;&amp; 5 &lt; 7&amp;8'
* }
*/
function normalizeRCData(rcdata) {
return rcdata
.replace(looseAmpRe, '&amp;$1')
.replace(ltRe, '&lt;')
.replace(gtRe, '&gt;');
}
// 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 === '<' ? '&lt;' : ch === '>' ? '&gt;' : '&amp;',
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;
html4.ATTRIBS['a::target'] = 0;
html4.ELEMENTS['video'] = 0;
html4.ATTRIBS['video::src'] = 0;
html4.ATTRIBS['video::poster'] = 0;
html4.ATTRIBS['video::controls'] = 0;
html4.ELEMENTS['audio'] = 0;
html4.ATTRIBS['audio::src'] = 0;
html4.ATTRIBS['video::autoplay'] = 0;
html4.ATTRIBS['video::controls'] = 0;
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
var Mustache = (typeof module !== "undefined" && module.exports) || {};
(function (exports) {
exports.name = "mustache.js";
exports.version = "0.5.0-dev";
exports.tags = ["{{", "}}"];
exports.parse = parse;
exports.compile = compile;
exports.render = render;
exports.clearCache = clearCache;
// This is here for backwards compatibility with 0.4.x.
exports.to_html = function (template, view, partials, send) {
var result = render(template, view, partials);
if (typeof send === "function") {
send(result);
} else {
return result;
}
};
var _toString = Object.prototype.toString;
var _isArray = Array.isArray;
var _forEach = Array.prototype.forEach;
var _trim = String.prototype.trim;
var isArray;
if (_isArray) {
isArray = _isArray;
} else {
isArray = function (obj) {
return _toString.call(obj) === "[object Array]";
};
}
var forEach;
if (_forEach) {
forEach = function (obj, callback, scope) {
return _forEach.call(obj, callback, scope);
};
} else {
forEach = function (obj, callback, scope) {
for (var i = 0, len = obj.length; i < len; ++i) {
callback.call(scope, obj[i], i, obj);
}
};
}
var spaceRe = /^\s*$/;
function isWhitespace(string) {
return spaceRe.test(string);
}
var trim;
if (_trim) {
trim = function (string) {
return string == null ? "" : _trim.call(string);
};
} else {
var trimLeft, trimRight;
if (isWhitespace("\xA0")) {
trimLeft = /^\s+/;
trimRight = /\s+$/;
} else {
// IE doesn't match non-breaking spaces with \s, thanks jQuery.
trimLeft = /^[\s\xA0]+/;
trimRight = /[\s\xA0]+$/;
}
trim = function (string) {
return string == null ? "" :
String(string).replace(trimLeft, "").replace(trimRight, "");
};
}
var escapeMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
function escapeHTML(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return escapeMap[s] || s;
});
}
/**
* Adds the `template`, `line`, and `file` properties to the given error
* object and alters the message to provide more useful debugging information.
*/
function debug(e, template, line, file) {
file = file || "<template>";
var lines = template.split("\n"),
start = Math.max(line - 3, 0),
end = Math.min(lines.length, line + 3),
context = lines.slice(start, end);
var c;
for (var i = 0, len = context.length; i < len; ++i) {
c = i + start + 1;
context[i] = (c === line ? " >> " : " ") + context[i];
}
e.template = template;
e.line = line;
e.file = file;
e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
return e;
}
/**
* Looks up the value of the given `name` in the given context `stack`.
*/
function lookup(name, stack, defaultValue) {
if (name === ".") {
return stack[stack.length - 1];
}
var names = name.split(".");
var lastIndex = names.length - 1;
var target = names[lastIndex];
var value, context, i = stack.length, j, localStack;
while (i) {
localStack = stack.slice(0);
context = stack[--i];
j = 0;
while (j < lastIndex) {
context = context[names[j++]];
if (context == null) {
break;
}
localStack.push(context);
}
if (context && typeof context === "object" && target in context) {
value = context[target];
break;
}
}
// If the value is a function, call it in the current context.
if (typeof value === "function") {
value = value.call(localStack[localStack.length - 1]);
}
if (value == null) {
return defaultValue;
}
return value;
}
function renderSection(name, stack, callback, inverted) {
var buffer = "";
var value = lookup(name, stack);
if (inverted) {
// From the spec: inverted sections may render text once based on the
// inverse value of the key. That is, they will be rendered if the key
// doesn't exist, is false, or is an empty list.
if (value == null || value === false || (isArray(value) && value.length === 0)) {
buffer += callback();
}
} else if (isArray(value)) {
forEach(value, function (value) {
stack.push(value);
buffer += callback();
stack.pop();
});
} else if (typeof value === "object") {
stack.push(value);
buffer += callback();
stack.pop();
} else if (typeof value === "function") {
var scope = stack[stack.length - 1];
var scopedRender = function (template) {
return render(template, scope);
};
buffer += value.call(scope, callback(), scopedRender) || "";
} else if (value) {
buffer += callback();
}
return buffer;
}
/**
* Parses the given `template` and returns the source of a function that,
* with the proper arguments, will render the template. Recognized options
* include the following:
*
* - file The name of the file the template comes from (displayed in
* error messages)
* - tags An array of open and close tags the `template` uses. Defaults
* to the value of Mustache.tags
* - debug Set `true` to log the body of the generated function to the
* console
* - space Set `true` to preserve whitespace from lines that otherwise
* contain only a {{tag}}. Defaults to `false`
*/
function parse(template, options) {
options = options || {};
var tags = options.tags || exports.tags,
openTag = tags[0],
closeTag = tags[tags.length - 1];
var code = [
'var buffer = "";', // output buffer
"\nvar line = 1;", // keep track of source line number
"\ntry {",
'\nbuffer += "'
];
var spaces = [], // indices of whitespace in code on the current line
hasTag = false, // is there a {{tag}} on the current line?
nonSpace = false; // is there a non-space char on the current line?
// Strips all space characters from the code array for the current line
// if there was a {{tag}} on it and otherwise only spaces.
var stripSpace = function () {
if (hasTag && !nonSpace && !options.space) {
while (spaces.length) {
code.splice(spaces.pop(), 1);
}
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
};
var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
var setTags = function (source) {
tags = trim(source).split(/\s+/);
nextOpenTag = tags[0];
nextCloseTag = tags[tags.length - 1];
};
var includePartial = function (source) {
code.push(
'";',
updateLine,
'\nvar partial = partials["' + trim(source) + '"];',
'\nif (partial) {',
'\n buffer += render(partial,stack[stack.length - 1],partials);',
'\n}',
'\nbuffer += "'
);
};
var openSection = function (source, inverted) {
var name = trim(source);
if (name === "") {
throw debug(new Error("Section name may not be empty"), template, line, options.file);
}
sectionStack.push({name: name, inverted: inverted});
code.push(
'";',
updateLine,
'\nvar name = "' + name + '";',
'\nvar callback = (function () {',
'\n return function () {',
'\n var buffer = "";',
'\nbuffer += "'
);
};
var openInvertedSection = function (source) {
openSection(source, true);
};
var closeSection = function (source) {
var name = trim(source);
var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
if (!openName || name != openName) {
throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
}
var section = sectionStack.pop();
code.push(
'";',
'\n return buffer;',
'\n };',
'\n})();'
);
if (section.inverted) {
code.push("\nbuffer += renderSection(name,stack,callback,true);");
} else {
code.push("\nbuffer += renderSection(name,stack,callback);");
}
code.push('\nbuffer += "');
};
var sendPlain = function (source) {
code.push(
'";',
updateLine,
'\nbuffer += lookup("' + trim(source) + '",stack,"");',
'\nbuffer += "'
);
};
var sendEscaped = function (source) {
code.push(
'";',
updateLine,
'\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
'\nbuffer += "'
);
};
var line = 1, c, callback;
for (var i = 0, len = template.length; i < len; ++i) {
if (template.slice(i, i + openTag.length) === openTag) {
i += openTag.length;
c = template.substr(i, 1);
updateLine = '\nline = ' + line + ';';
nextOpenTag = openTag;
nextCloseTag = closeTag;
hasTag = true;
switch (c) {
case "!": // comment
i++;
callback = null;
break;
case "=": // change open/close tags, e.g. {{=<% %>=}}
i++;
closeTag = "=" + closeTag;
callback = setTags;
break;
case ">": // include partial
i++;
callback = includePartial;
break;
case "#": // start section
i++;
callback = openSection;
break;
case "^": // start inverted section
i++;
callback = openInvertedSection;
break;
case "/": // end section
i++;
callback = closeSection;
break;
case "{": // plain variable
closeTag = "}" + closeTag;
// fall through
case "&": // plain variable
i++;
nonSpace = true;
callback = sendPlain;
break;
default: // escaped variable
nonSpace = true;
callback = sendEscaped;
}
var end = template.indexOf(closeTag, i);
if (end === -1) {
throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
}
var source = template.substring(i, end);
if (callback) {
callback(source);
}
// Maintain line count for \n in source.
var n = 0;
while (~(n = source.indexOf("\n", n))) {
line++;
n++;
}
i = end + closeTag.length - 1;
openTag = nextOpenTag;
closeTag = nextCloseTag;
} else {
c = template.substr(i, 1);
switch (c) {
case '"':
case "\\":
nonSpace = true;
code.push("\\" + c);
break;
case "\r":
// Ignore carriage returns.
break;
case "\n":
spaces.push(code.length);
code.push("\\n");
stripSpace(); // Check for whitespace on the current line.
line++;
break;
default:
if (isWhitespace(c)) {
spaces.push(code.length);
} else {
nonSpace = true;
}
code.push(c);
}
}
}
if (sectionStack.length != 0) {
throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
}
// Clean up any whitespace from a closing {{tag}} that was at the end
// of the template without a trailing \n.
stripSpace();
code.push(
'";',
"\nreturn buffer;",
"\n} catch (e) { throw {error: e, line: line}; }"
);
// Ignore `buffer += "";` statements.
var body = code.join("").replace(/buffer \+= "";\n/g, "");
if (options.debug) {
if (typeof console != "undefined" && console.log) {
console.log(body);
} else if (typeof print === "function") {
print(body);
}
}
return body;
}
/**
* Used by `compile` to generate a reusable function for the given `template`.
*/
function _compile(template, options) {
var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
var body = parse(template, options);
var fn = new Function(args, body);
// This anonymous function wraps the generated function so we can do
// argument coercion, setup some variables, and handle any errors
// encountered while executing it.
return function (view, partials) {
partials = partials || {};
var stack = [view]; // context stack
try {
return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
} catch (e) {
throw debug(e.error, template, e.line, options.file);
}
};
}
// Cache of pre-compiled templates.
var _cache = {};
/**
* Clear the cache of compiled templates.
*/
function clearCache() {
_cache = {};
}
/**
* Compiles the given `template` into a reusable function using the given
* `options`. In addition to the options accepted by Mustache.parse,
* recognized options include the following:
*
* - cache Set `false` to bypass any pre-compiled version of the given
* template. Otherwise, a given `template` string will be cached
* the first time it is parsed
*/
function compile(template, options) {
options = options || {};
// Use a pre-compiled version from the cache if we have one.
if (options.cache !== false) {
if (!_cache[template]) {
_cache[template] = _compile(template, options);
}
return _cache[template];
}
return _compile(template, options);
}
/**
* High-level function that renders the given `template` using the given
* `view` and `partials`. If you need to use any of the template options (see
* `compile` above), you must compile in a separate step, and then call that
* compiled function.
*/
function render(template, view, partials) {
return compile(template)(view, partials);
}
})(Mustache);
/*!
* Reqwest! A general purpose XHR connection manager
* (c) Dustin Diaz 2011
* https://github.com/ded/reqwest
* license MIT
*/
!function(a,b){typeof module!="undefined"?module.exports=b():typeof define=="function"&&define.amd?define(a,b):this[a]=b()}("reqwest",function(){function handleReadyState(a,b,c){return function(){a&&a[readyState]==4&&(twoHundo.test(a.status)?b(a):c(a))}}function setHeaders(a,b){var c=b.headers||{},d;c.Accept=c.Accept||defaultHeaders.accept[b.type]||defaultHeaders.accept["*"],!b.crossOrigin&&!c[requestedWith]&&(c[requestedWith]=defaultHeaders.requestedWith),c[contentType]||(c[contentType]=b.contentType||defaultHeaders.contentType);for(d in c)c.hasOwnProperty(d)&&a.setRequestHeader(d,c[d])}function generalCallback(a){lastValue=a}function urlappend(a,b){return a+(/\?/.test(a)?"&":"?")+b}function handleJsonp(a,b,c,d){var e=uniqid++,f=a.jsonpCallback||"callback",g=a.jsonpCallbackName||"reqwest_"+e,h=new RegExp("((^|\\?|&)"+f+")=([^&]+)"),i=d.match(h),j=doc.createElement("script"),k=0;i?i[3]==="?"?d=d.replace(h,"$1="+g):g=i[3]:d=urlappend(d,f+"="+g),win[g]=generalCallback,j.type="text/javascript",j.src=d,j.async=!0,typeof j.onreadystatechange!="undefined"&&(j.event="onclick",j.htmlFor=j.id="_reqwest_"+e),j.onload=j.onreadystatechange=function(){if(j[readyState]&&j[readyState]!=="complete"&&j[readyState]!=="loaded"||k)return!1;j.onload=j.onreadystatechange=null,j.onclick&&j.onclick(),a.success&&a.success(lastValue),lastValue=undefined,head.removeChild(j),k=1},head.appendChild(j)}function getRequest(a,b,c){var d=(a.method||"GET").toUpperCase(),e=typeof a=="string"?a:a.url,f=a.processData!==!1&&a.data&&typeof a.data!="string"?reqwest.toQueryString(a.data):a.data||null,g;return(a.type=="jsonp"||d=="GET")&&f&&(e=urlappend(e,f),f=null),a.type=="jsonp"?handleJsonp(a,b,c,e):(g=xhr(),g.open(d,e,!0),setHeaders(g,a),g.onreadystatechange=handleReadyState(g,b,c),a.before&&a.before(g),g.send(f),g)}function Reqwest(a,b){this.o=a,this.fn=b,init.apply(this,arguments)}function setType(a){var b=a.match(/\.(json|jsonp|html|xml)(\?|$)/);return b?b[1]:"js"}function init(o,fn){function complete(a){o.timeout&&clearTimeout(self.timeout),self.timeout=null,o.complete&&o.complete(a)}function success(resp){var r=resp.responseText;if(r)switch(type){case"json":try{resp=win.JSON?win.JSON.parse(r):eval("("+r+")")}catch(err){return error(resp,"Could not parse JSON in response",err)}break;case"js":resp=eval(r);break;case"html":resp=r}fn(resp),o.success&&o.success(resp),complete(resp)}function error(a,b,c){o.error&&o.error(a,b,c),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()},o.timeout)),this.request=getRequest(o,success,error)}function reqwest(a,b){return new Reqwest(a,b)}function normalize(a){return a?a.replace(/\r?\n/g,"\r\n"):""}function serial(a,b){var c=a.name,d=a.tagName.toLowerCase(),e=function(a){a&&!a.disabled&&b(c,normalize(a.attributes.value&&a.attributes.value.specified?a.value:a.text))};if(a.disabled||!c)return;switch(d){case"input":if(!/reset|button|image|file/i.test(a.type)){var f=/checkbox/i.test(a.type),g=/radio/i.test(a.type),h=a.value;(!f&&!g||a.checked)&&b(c,normalize(f&&h===""?"on":h))}break;case"textarea":b(c,normalize(a.value));break;case"select":if(a.type.toLowerCase()==="select-one")e(a.selectedIndex>=0?a.options[a.selectedIndex]:null);else for(var i=0;a.length&&i<a.length;i++)a.options[i].selected&&e(a.options[i])}}function eachFormElement(){var a=this,b,c,d,e=function(b,c){for(var e=0;e<c.length;e++){var f=b[byTag](c[e]);for(d=0;d<f.length;d++)serial(f[d],a)}};for(c=0;c<arguments.length;c++)b=arguments[c],/input|select|textarea/i.test(b.tagName)&&serial(b,a),e(b,["input","select","textarea"])}function serializeQueryString(){return reqwest.toQueryString(reqwest.serializeArray.apply(null,arguments))}function serializeHash(){var a={};return eachFormElement.apply(function(b,c){b in a?(a[b]&&!isArray(a[b])&&(a[b]=[a[b]]),a[b].push(c)):a[b]=c},arguments),a}var win=window,doc=document,twoHundo=/^20\d$/,byTag="getElementsByTagName",readyState="readyState",contentType="Content-Type",requestedWith="X-Requested-With",head=doc[byTag]("head")[0],uniqid=0,lastValue,xmlHttpRequest="XMLHttpRequest",isArray=typeof Array.isArray=="function"?Array.isArray:function(a){return a instanceof Array},defaultHeaders={contentType:"application/x-www-form-urlencoded",accept:{"*":"text/javascript, text/html, application/xml, text/xml, */*",xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript",js:"application/javascript, text/javascript"},requestedWith:xmlHttpRequest},xhr=win[xmlHttpRequest]?function(){return new XMLHttpRequest}:function(){return new ActiveXObject("Microsoft.XMLHTTP")};return Reqwest.prototype={abort:function(){this.request.abort()},retry:function(){init.call(this,this.o,this.fn)}},reqwest.serializeArray=function(){var a=[];return eachFormElement.apply(function(b,c){a.push({name:b,value:c})},arguments),a},reqwest.serialize=function(){if(arguments.length===0)return"";var a,b,c=Array.prototype.slice.call(arguments,0);return a=c.pop(),a&&a.nodeType&&c.push(a)&&(a=null),a&&(a=a.type),a=="map"?b=serializeHash:a=="array"?b=reqwest.serializeArray:b=serializeQueryString,b.apply(null,c)},reqwest.toQueryString=function(a){var b="",c,d=encodeURIComponent,e=function(a,c){b+=d(a)+"="+d(c)+"&"};if(isArray(a))for(c=0;a&&c<a.length;c++)e(a[c].name,a[c].value);else for(var f in a){if(!Object.hasOwnProperty.call(a,f))continue;var g=a[f];if(isArray(g))for(c=0;c<g.length;c++)e(f,g[c]);else e(f,a[f])}return b.replace(/&$/,"").replace(/%20/g,"+")},reqwest.compat=function(a,b){return a&&(a.type&&(a.method=a.type)&&delete a.type,a.dataType&&(a.type=a.dataType),a.jsonpCallback&&(a.jsonpCallbackName=a.jsonpCallback)&&delete a.jsonpCallback,a.jsonp&&(a.jsonpCallback=a.jsonp)),new Reqwest(a,b)},reqwest});wax = wax || {};
// Attribution
// -----------
wax.attribution = function() {
var a = {};
var container = document.createElement('div');
container.className = 'map-attribution';
a.content = function(x) {
if (typeof x === 'undefined') return container.innerHTML;
container.innerHTML = wax.u.sanitize(x);
return this;
};
a.element = function() {
return container;
};
a.init = function() {
return this;
};
return a;
};
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() {};
}
// Wrap the given formatter function in order to
// catch exceptions that it may throw.
formatter.format = function(options, data) {
try {
return wax.u.sanitize(f(options, data));
} 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) {
if (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\}/g, xyz[1])
.replace(/\{x\}/g, xyz[2])
.replace(/\{y\}/g, 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) {
// Getter-setter
if (!arguments.length) return gridUrl;
// Handle tilesets that don't support grids
if (!x) {
gridUrl = function() { return null; };
} else {
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);
} else {
// In this case, we cannot support grids
formatter = undefined;
}
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 || {};
var s0, // old hash
hash = {},
lat = 90 - 1e-8; // allowable latitude range
function getState() {
return location.hash.substring(1);
}
function pushState(state) {
var l = window.location;
l.replace(l.toString().replace((l.hash || /$/), '#' + state));
}
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 hash;
};
hash.remove = function() {
options.unbindChange(_move);
return hash;
};
return hash;
};
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);
interaction.screen_feature(pos, function(feature) {
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, 'click', onUp);
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
interaction.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;
interaction.click(evt, pos);
}, 300);
}
return onUp;
}
// Handle a click event. Takes a second
interaction.click = function(e, pos) {
interaction.screen_feature(pos, function(feature) {
if (feature) bean.fire(interaction, 'on', {
parent: parent(),
data: feature,
formatter: gm.formatter().format,
e: e
});
});
};
interaction.screen_feature = function(pos, callback) {
var tile = getTile(pos);
if (!tile) callback(null);
gm.getGrid(tile.src, function(err, g) {
if (err || !g) return callback(null);
var feature = g.tileFeature(pos.x, pos.y, tile);
callback(feature);
});
};
// 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;
};
// Return or set the gridmanager implementation
interaction.gridmanager = function(x) {
if (!arguments.length) return gm;
gm = x;
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;
legend.element = function() {
return container;
};
legend.content = function(content) {
if (!arguments.length) return element.innerHTML;
element.innerHTML = wax.u.sanitize(content);
element.style.display = 'block';
if (element.innerHTML === '') {
element.style.display = 'none';
}
return legend;
};
legend.add = function() {
container = document.createElement('div');
container.className = 'map-legends wax-legends';
element = container.appendChild(document.createElement('div'));
element.className = 'map-legend wax-legend';
element.style.display = 'none';
return legend;
};
return legend.add();
};
var wax = wax || {};
wax.location = function() {
var t = {};
function on(o) {
if ((o.e.type === 'mousemove' || !o.e.type)) {
return;
} else {
var loc = o.formatter({ format: 'location' }, o.data);
if (loc) {
window.location.href = loc;
}
}
}
t.events = function() {
return {
on: on
};
};
return t;
};
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 = 'map-tooltip map-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 += ' map-popup';
var close = tt.appendChild(document.createElement('a'));
close.href = '#close';
close.className = 'close';
close.innerHTML = ' ';
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 = {};
// 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 wax.u.sanitize(Mustache.to_html(x, clone));
};
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 = {};
var tooltip_buffer = []; //20120312-ALaing- Buffer to hold feature text from clicks on map.
//20120312-ALaing- Function to append unique tooltip to buffer
//tooltip_buffer_append = function(tooltip_buffer_item) {
// tooltip_buffer_item = tooltip_buffer_item || {};
//
// tooltip_buffer.append(tooltip_buffer_item);
//
//};
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 = 'map-tooltip map-tooltip-0 wax-tooltip';
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 += ' map-fade';
} else {
if (_ct.parentNode) _ct.parentNode.removeChild(_ct);
}
}
}
function on(o) {
var content;
if (o.e.type === 'mousemove' || !o.e.type) {
if (!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 += ' map-popup wax-popup';
var close = tt.appendChild(document.createElement('a'));
close.href = '#close';
close.className = 'close';
close.innerHTML = ' ';
popped = true;
tooltips.push(tt);
bean.add(close, 'touchstart mousedown', function(e) {
e.stop();
});
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) {
var match;
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;
},
// 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
return {
x: e.clientX,
y: e.clientY
};
} 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);
},
sanitize: function(content) {
if (!content) return '';
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; }
return html_sanitize(content, urlX, idX);
}
};
wax = wax || {};
wax.g = wax.g || {};
// Attribution
// -----------
// Attribution wrapper for Google Maps.
wax.g.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 = 'map-attribution map-g';
return this;
};
return attribution.init();
};
wax = wax || {};
wax.g = wax.g || {};
// Bandwidth Detection
// ------------------
wax.g.bwdetect = function(map, options) {
options = options || {};
var lowpng = options.png || '.png128',
lowjpg = options.jpg || '.jpg70';
// Create a low-bandwidth map type.
if (!map.mapTypes['mb-low']) {
var mb = map.mapTypes.mb;
var tilejson = {
tiles: [],
scheme: mb.options.scheme,
blankImage: mb.options.blankImage,
minzoom: mb.minZoom,
maxzoom: mb.maxZoom,
name: mb.name,
description: mb.description
};
for (var i = 0; i < mb.options.tiles.length; i++) {
tilejson.tiles.push(mb.options.tiles[i]
.replace('.png', lowpng)
.replace('.jpg', lowjpg));
}
m.mapTypes.set('mb-low', new wax.g.connector(tilejson));
}
return wax.bwdetect(options, function(bw) {
map.setMapTypeId(bw ? 'mb' : 'mb-low');
});
};
wax = wax || {};
wax.g = wax.g || {};
wax.g.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.lng().toFixed(precision)
].join('/');
},
setCenterZoom: function setCenterZoom(args) {
map.setCenter(new google.maps.LatLng(args[1], args[2]));
map.setZoom(args[0]);
},
bindChange: function(fn) {
google.maps.event.addListener(map, 'idle', fn);
},
unbindChange: function(fn) {
google.maps.event.removeListener(map, 'idle', fn);
}
});
};
wax = wax || {};
wax.g = wax.g || {};
wax.g.interaction = function() {
var dirty = false, _grid, map;
var tileloadListener = null,
idleListener = null;
function setdirty() { dirty = true; }
function grid() {
if (!dirty && _grid) {
return _grid;
} else {
_grid = [];
var zoom = map.getZoom();
var mapOffset = wax.u.offset(map.getDiv());
var get = function(mapType) {
if (!mapType.interactive) return;
for (var key in mapType.cache) {
if (key.split('/')[0] != zoom) continue;
var tileOffset = wax.u.offset(mapType.cache[key]);
_grid.push([
tileOffset.top,
tileOffset.left,
mapType.cache[key]
]);
}
};
// Iterate over base mapTypes and overlayMapTypes.
for (var i in map.mapTypes) get(map.mapTypes[i]);
map.overlayMapTypes.forEach(get);
}
return _grid;
}
function attach(x) {
if (!arguments.length) return map;
map = x;
tileloadListener = google.maps.event.addListener(map, 'tileloaded',
setdirty);
idleListener = google.maps.event.addListener(map, 'idle',
setdirty);
}
function detach(x) {
if(tileloadListener)
google.maps.event.removeListener(tileloadListener);
if(idleListener)
google.maps.event.removeListener(idleListener);
}
return wax.interaction()
.attach(attach)
.detach(detach)
.parent(function() {
return map.getDiv();
})
.grid(grid);
};
wax = wax || {};
wax.g = wax.g || {};
// Legend Control
// --------------
// Adds legends to a google Map object.
wax.g.legend = function(map, tilejson) {
tilejson = tilejson || {};
var l, // parent legend
legend = {};
legend.add = function() {
l = wax.legend()
.content(tilejson.legend || '');
return legend;
};
legend.element = function() {
return l.element();
};
legend.appendTo = function(elem) {
wax.u.$(elem).appendChild(l.element());
return legend;
};
return legend.add();
};
// Wax for Google Maps API v3
// --------------------------
// Wax header
var wax = wax || {};
wax.g = wax.g || {};
// Wax Google Maps MapType: takes an object of options in the form
//
// {
// name: '',
// filetype: '.png',
// layerName: 'world-light',
// alt: '',
// zoomRange: [0, 18],
// baseUrl: 'a url',
// }
wax.g.connector = function(options) {
options = options || {};
this.options = {
tiles: options.tiles,
scheme: options.scheme || 'xyz',
blankImage: options.blankImage || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
};
this.minZoom = options.minzoom || 0;
this.maxZoom = options.maxzoom || 22;
this.name = options.name || '';
this.description = options.description || '';
// non-configurable options
this.interactive = true;
this.tileSize = new google.maps.Size(256, 256);
// DOM element cache
this.cache = {};
};
// Get a tile element from a coordinate, zoom level, and an ownerDocument.
wax.g.connector.prototype.getTile = function(coord, zoom, ownerDocument) {
var key = zoom + '/' + coord.x + '/' + coord.y;
if (!this.cache[key]) {
var img = this.cache[key] = new Image(256, 256);
this.cache[key].src = this.getTileUrl(coord, zoom);
this.cache[key].setAttribute('gTileKey', key);
this.cache[key].onerror = function() { img.style.display = 'none'; };
}
return this.cache[key];
};
// Remove a tile that has fallen out of the map's viewport.
//
// TODO: expire cache data in the gridmanager.
wax.g.connector.prototype.releaseTile = function(tile) {
var key = tile.getAttribute('gTileKey');
if (this.cache[key]) delete this.cache[key];
if (tile.parentNode) tile.parentNode.removeChild(tile);
};
// Get a tile url, based on x, y coordinates and a z value.
wax.g.connector.prototype.getTileUrl = function(coord, z) {
// Y coordinate is flipped in Mapbox, compared to Google
var mod = Math.pow(2, z),
y = (this.options.scheme === 'tms') ?
(mod - 1) - coord.y :
coord.y,
x = (coord.x % mod);
x = (x < 0) ? (coord.x % mod) + mod : x;
if (y < 0) return this.options.blankImage;
return this.options.tiles
[parseInt(x + y, 10) %
this.options.tiles.length]
.replace('{z}', z)
.replace('{x}', x)
.replace('{y}', y);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment