Created
November 1, 2012 17:41
-
-
Save wboykinm/3995290 to your computer and use it in GitHub Desktop.
Wax for Google maps 6.0.4 updated w/ interaction.remove(); handler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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: π 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; | |
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 = { | |
"&": "&", | |
"<": "<", | |
">": ">", | |
'"': '"', | |
"'": ''', | |
"/": '/' | |
}; | |
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