Created
June 29, 2012 20:29
-
-
Save cwmanning/3020453 to your computer and use it in GitHub Desktop.
Wax and Reqwest
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
/*! | |
* Reqwest! A general purpose XHR connection manager | |
* copyright Dustin Diaz 2011 | |
* https://github.com/ded/reqwest | |
* license MIT | |
*/ | |
!function(context,win){function serial(a){var b=a.name;if(a.disabled||!b)return"";b=enc(b);switch(a.tagName.toLowerCase()){case"input":switch(a.type){case"reset":case"button":case"image":case"file":return"";case"checkbox":case"radio":return a.checked?b+"="+(a.value?enc(a.value):!0)+"&":"";default:return b+"="+(a.value?enc(a.value):"")+"&"}break;case"textarea":return b+"="+enc(a.value)+"&";case"select":return b+"="+enc(a.options[a.selectedIndex].value)+"&"}return""}function enc(a){return encodeURIComponent(a)}function reqwest(a,b){return new Reqwest(a,b)}function init(o,fn){function error(a){o.error&&o.error(a),complete(a)}function success(resp){o.timeout&&clearTimeout(self.timeout)&&(self.timeout=null);var r=resp.responseText;if(r)switch(type){case"json":resp=win.JSON?win.JSON.parse(r):eval("("+r+")");break;case"js":resp=eval(r);break;case"html":resp=r}fn(resp),o.success&&o.success(resp),complete(resp)}function complete(a){o.complete&&o.complete(a)}this.url=typeof o=="string"?o:o.url,this.timeout=null;var type=o.type||setType(this.url),self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){self.abort(),error()},o.timeout)),this.request=getRequest(o,success,error)}function setType(a){if(/\.json$/.test(a))return"json";if(/\.jsonp$/.test(a))return"jsonp";if(/\.js$/.test(a))return"js";if(/\.html?$/.test(a))return"html";if(/\.xml$/.test(a))return"xml";return"js"}function Reqwest(a,b){this.o=a,this.fn=b,init.apply(this,arguments)}function getRequest(a,b,c){if(a.type!="jsonp"){var f=xhr();f.open(a.method||"GET",typeof a=="string"?a:a.url,!0),setHeaders(f,a),f.onreadystatechange=handleReadyState(f,b,c),a.before&&a.before(f),f.send(a.data||null);return f}var d=doc.createElement("script"),e=0;win[getCallbackName(a)]=generalCallback,d.type="text/javascript",d.src=a.url,d.async=!0,d.onload=d.onreadystatechange=function(){if(d[readyState]&&d[readyState]!=="complete"&&d[readyState]!=="loaded"||e)return!1;d.onload=d.onreadystatechange=null,a.success&&a.success(lastValue),lastValue=undefined,head.removeChild(d),e=1},head.appendChild(d)}function generalCallback(a){lastValue=a}function getCallbackName(a){var b=a.jsonpCallback||"callback";if(a.url.slice(-(b.length+2))==b+"=?"){var c="reqwest_"+uniqid++;a.url=a.url.substr(0,a.url.length-1)+c;return c}var d=new RegExp(b+"=([\\w]+)");return a.url.match(d)[1]}function setHeaders(a,b){var c=b.headers||{};c.Accept=c.Accept||"text/javascript, text/html, application/xml, text/xml, */*",b.crossOrigin||(c["X-Requested-With"]=c["X-Requested-With"]||"XMLHttpRequest"),c[contentType]=c[contentType]||"application/x-www-form-urlencoded";for(var d in c)c.hasOwnProperty(d)&&a.setRequestHeader(d,c[d],!1)}function handleReadyState(a,b,c){return function(){a&&a[readyState]==4&&(twoHundo.test(a.status)?b(a):c(a))}}var twoHundo=/^20\d$/,doc=document,byTag="getElementsByTagName",readyState="readyState",contentType="Content-Type",head=doc[byTag]("head")[0],uniqid=0,lastValue,xhr="XMLHttpRequest"in win?function(){return new XMLHttpRequest}:function(){return new ActiveXObject("Microsoft.XMLHTTP")};Reqwest.prototype={abort:function(){this.request.abort()},retry:function(){init.call(this,this.o,this.fn)}},reqwest.serialize=function(a){var b=[a[byTag]("input"),a[byTag]("select"),a[byTag]("textarea")],c=[],d,e;for(d=0,l=b.length;d<l;++d)for(e=0,l2=b[d].length;e<l2;++e)c.push(serial(b[d][e]));return c.join("").replace(/&$/,"")},reqwest.serializeArray=function(a){for(var b=this.serialize(a).split("&"),c=0,d=b.length,e=[],f;c<d;c++)b[c]&&(f=b[c].split("="))&&e.push({name:f[0],value:f[1]});return e};var old=context.reqwest;reqwest.noConflict=function(){context.reqwest=old;return this},typeof module!="undefined"?module.exports=reqwest:context.reqwest=reqwest}(this,window)// Copyright Google Inc. | |
/* wax - 6.2.0 - 1.0.4-587-g1182501 */ | |
!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 templates in JavaScript | |
See http://mustache.github.com/ for more info. | |
*/ | |
var Mustache = function() { | |
var regexCache = {}; | |
var Renderer = function() {}; | |
Renderer.prototype = { | |
otag: "{{", | |
ctag: "}}", | |
pragmas: {}, | |
buffer: [], | |
pragmas_implemented: { | |
"IMPLICIT-ITERATOR": true | |
}, | |
context: {}, | |
render: function(template, context, partials, in_recursion) { | |
// reset buffer & set context | |
if(!in_recursion) { | |
this.context = context; | |
this.buffer = []; // TODO: make this non-lazy | |
} | |
// fail fast | |
if(!this.includes("", template)) { | |
if(in_recursion) { | |
return template; | |
} else { | |
this.send(template); | |
return; | |
} | |
} | |
// get the pragmas together | |
template = this.render_pragmas(template); | |
// render the template | |
var html = this.render_section(template, context, partials); | |
// render_section did not find any sections, we still need to render the tags | |
if (html === false) { | |
html = this.render_tags(template, context, partials, in_recursion); | |
} | |
if (in_recursion) { | |
return html; | |
} else { | |
this.sendLines(html); | |
} | |
}, | |
/* | |
Sends parsed lines | |
*/ | |
send: function(line) { | |
if(line !== "") { | |
this.buffer.push(line); | |
} | |
}, | |
sendLines: function(text) { | |
if (text) { | |
var lines = text.split("\n"); | |
for (var i = 0; i < lines.length; i++) { | |
this.send(lines[i]); | |
} | |
} | |
}, | |
/* | |
Looks for %PRAGMAS | |
*/ | |
render_pragmas: function(template) { | |
// no pragmas | |
if(!this.includes("%", template)) { | |
return template; | |
} | |
var that = this; | |
var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) { | |
return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); | |
}); | |
return template.replace(regex, function(match, pragma, options) { | |
if(!that.pragmas_implemented[pragma]) { | |
throw({message: | |
"This implementation of mustache doesn't understand the '" + | |
pragma + "' pragma"}); | |
} | |
that.pragmas[pragma] = {}; | |
if(options) { | |
var opts = options.split("="); | |
that.pragmas[pragma][opts[0]] = opts[1]; | |
} | |
return ""; | |
// ignore unknown pragmas silently | |
}); | |
}, | |
/* | |
Tries to find a partial in the curent scope and render it | |
*/ | |
render_partial: function(name, context, partials) { | |
name = this.trim(name); | |
if(!partials || partials[name] === undefined) { | |
throw({message: "unknown_partial '" + name + "'"}); | |
} | |
if(typeof(context[name]) != "object") { | |
return this.render(partials[name], context, partials, true); | |
} | |
return this.render(partials[name], context[name], partials, true); | |
}, | |
/* | |
Renders inverted (^) and normal (#) sections | |
*/ | |
render_section: function(template, context, partials) { | |
if(!this.includes("#", template) && !this.includes("^", template)) { | |
// did not render anything, there were no sections | |
return false; | |
} | |
var that = this; | |
var regex = this.getCachedRegex("render_section", function(otag, ctag) { | |
// This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder | |
return new RegExp( | |
"^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) | |
otag + // {{ | |
"(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) | |
ctag + // }} | |
"\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped | |
otag + // {{ | |
"\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). | |
ctag + // }} | |
"\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. | |
"g"); | |
}); | |
// for each {{#foo}}{{/foo}} section do... | |
return template.replace(regex, function(match, before, type, name, content, after) { | |
// before contains only tags, no sections | |
var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", | |
// after may contain both sections and tags, so use full rendering function | |
renderedAfter = after ? that.render(after, context, partials, true) : "", | |
// will be computed below | |
renderedContent, | |
value = that.find(name, context); | |
if (type === "^") { // inverted section | |
if (!value || that.is_array(value) && value.length === 0) { | |
// false or empty list, render it | |
renderedContent = that.render(content, context, partials, true); | |
} else { | |
renderedContent = ""; | |
} | |
} else if (type === "#") { // normal section | |
if (that.is_array(value)) { // Enumerable, Let's loop! | |
renderedContent = that.map(value, function(row) { | |
return that.render(content, that.create_context(row), partials, true); | |
}).join(""); | |
} else if (that.is_object(value)) { // Object, Use it as subcontext! | |
renderedContent = that.render(content, that.create_context(value), | |
partials, true); | |
} else if (typeof value === "function") { | |
// higher order section | |
renderedContent = value.call(context, content, function(text) { | |
return that.render(text, context, partials, true); | |
}); | |
} else if (value) { // boolean section | |
renderedContent = that.render(content, context, partials, true); | |
} else { | |
renderedContent = ""; | |
} | |
} | |
return renderedBefore + renderedContent + renderedAfter; | |
}); | |
}, | |
/* | |
Replace {{foo}} and friends with values from our view | |
*/ | |
render_tags: function(template, context, partials, in_recursion) { | |
// tit for tat | |
var that = this; | |
var new_regex = function() { | |
return that.getCachedRegex("render_tags", function(otag, ctag) { | |
return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g"); | |
}); | |
}; | |
var regex = new_regex(); | |
var tag_replace_callback = function(match, operator, name) { | |
switch(operator) { | |
case "!": // ignore comments | |
return ""; | |
case "=": // set new delimiters, rebuild the replace regexp | |
that.set_delimiters(name); | |
regex = new_regex(); | |
return ""; | |
case ">": // render partial | |
return that.render_partial(name, context, partials); | |
case "{": // the triple mustache is unescaped | |
return that.find(name, context); | |
default: // escape the value | |
return that.escape(that.find(name, context)); | |
} | |
}; | |
var lines = template.split("\n"); | |
for(var i = 0; i < lines.length; i++) { | |
lines[i] = lines[i].replace(regex, tag_replace_callback, this); | |
if(!in_recursion) { | |
this.send(lines[i]); | |
} | |
} | |
if(in_recursion) { | |
return lines.join("\n"); | |
} | |
}, | |
set_delimiters: function(delimiters) { | |
var dels = delimiters.split(" "); | |
this.otag = this.escape_regex(dels[0]); | |
this.ctag = this.escape_regex(dels[1]); | |
}, | |
escape_regex: function(text) { | |
// thank you Simon Willison | |
if(!arguments.callee.sRE) { | |
var specials = [ | |
'/', '.', '*', '+', '?', '|', | |
'(', ')', '[', ']', '{', '}', '\\' | |
]; | |
arguments.callee.sRE = new RegExp( | |
'(\\' + specials.join('|\\') + ')', 'g' | |
); | |
} | |
return text.replace(arguments.callee.sRE, '\\$1'); | |
}, | |
/* | |
find `name` in current `context`. That is find me a value | |
from the view object | |
*/ | |
find: function(name, context) { | |
name = this.trim(name); | |
// Checks whether a value is thruthy or false or 0 | |
function is_kinda_truthy(bool) { | |
return bool === false || bool === 0 || bool; | |
} | |
var value; | |
if(is_kinda_truthy(context[name])) { | |
value = context[name]; | |
} else if(is_kinda_truthy(this.context[name])) { | |
value = this.context[name]; | |
} | |
if(typeof value === "function") { | |
return value.apply(context); | |
} | |
if(value !== undefined) { | |
return value; | |
} | |
// silently ignore unkown variables | |
return ""; | |
}, | |
// Utility methods | |
/* includes tag */ | |
includes: function(needle, haystack) { | |
return haystack.indexOf(this.otag + needle) != -1; | |
}, | |
/* | |
Does away with nasty characters | |
*/ | |
escape: function(s) { | |
s = String(s === null ? "" : s); | |
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) { | |
switch(s) { | |
case "&": return "&"; | |
case '"': return '"'; | |
case "'": return '''; | |
case "<": return "<"; | |
case ">": return ">"; | |
default: return s; | |
} | |
}); | |
}, | |
// by @langalex, support for arrays of strings | |
create_context: function(_context) { | |
if(this.is_object(_context)) { | |
return _context; | |
} else { | |
var iterator = "."; | |
if(this.pragmas["IMPLICIT-ITERATOR"]) { | |
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; | |
} | |
var ctx = {}; | |
ctx[iterator] = _context; | |
return ctx; | |
} | |
}, | |
is_object: function(a) { | |
return a && typeof a == "object"; | |
}, | |
is_array: function(a) { | |
return Object.prototype.toString.call(a) === '[object Array]'; | |
}, | |
/* | |
Gets rid of leading and trailing whitespace | |
*/ | |
trim: function(s) { | |
return s.replace(/^\s*|\s*$/g, ""); | |
}, | |
/* | |
Why, why, why? Because IE. Cry, cry cry. | |
*/ | |
map: function(array, fn) { | |
if (typeof array.map == "function") { | |
return array.map(fn); | |
} else { | |
var r = []; | |
var l = array.length; | |
for(var i = 0; i < l; i++) { | |
r.push(fn(array[i])); | |
} | |
return r; | |
} | |
}, | |
getCachedRegex: function(name, generator) { | |
var byOtag = regexCache[this.otag]; | |
if (!byOtag) { | |
byOtag = regexCache[this.otag] = {}; | |
} | |
var byCtag = byOtag[this.ctag]; | |
if (!byCtag) { | |
byCtag = byOtag[this.ctag] = {}; | |
} | |
var regex = byCtag[name]; | |
if (!regex) { | |
regex = byCtag[name] = generator(this.otag, this.ctag); | |
} | |
return regex; | |
} | |
}; | |
return({ | |
name: "mustache.js", | |
version: "0.4.0-dev", | |
/* | |
Turns a template and view into HTML | |
*/ | |
to_html: function(template, view, partials, send_fun) { | |
var renderer = new Renderer(); | |
if(send_fun) { | |
renderer.send = send_fun; | |
} | |
renderer.render(template, view || {}, partials); | |
if(!send_fun) { | |
return renderer.buffer.join("\n"); | |
} | |
} | |
}); | |
}(); | |
wax = wax || {}; | |
// Attribution | |
// ----------- | |
wax.attribution = function() { | |
var container, | |
a = {}; | |
function urlX(url) { | |
// Data URIs are subject to a bug in Firefox | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 | |
// which let them be a vector. But WebKit does 'the right thing' | |
// or at least 'something' about this situation, so we'll tolerate | |
// them. | |
if (/^(https?:\/\/|data:image)/.test(url)) { | |
return url; | |
} | |
} | |
function idX(id) { | |
return id; | |
} | |
a.content = function(x) { | |
if (typeof x === 'undefined') return container.innerHTML; | |
container.innerHTML = html_sanitize(x, urlX, idX); | |
return this; | |
}; | |
a.element = function() { | |
return container; | |
}; | |
a.init = function() { | |
container = document.createElement('div'); | |
container.className = 'wax-attribution'; | |
return this; | |
}; | |
return a.init(); | |
}; | |
// Attribution | |
// ----------- | |
wax.bwdetect = function(options, callback) { | |
var detector = {}, | |
threshold = options.threshold || 400, | |
// test image: 30.29KB | |
testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()), | |
// High-bandwidth assumed | |
// 1: high bandwidth (.png, .jpg) | |
// 0: low bandwidth (.png128, .jpg70) | |
bw = 1, | |
// Alternative versions | |
auto = options.auto === undefined ? true : options.auto; | |
function bwTest() { | |
wax.bw = -1; | |
var im = new Image(); | |
im.src = testImage; | |
var first = true; | |
var timeout = setTimeout(function() { | |
if (first && wax.bw == -1) { | |
detector.bw(0); | |
first = false; | |
} | |
}, threshold); | |
im.onload = function() { | |
if (first && wax.bw == -1) { | |
clearTimeout(timeout); | |
detector.bw(1); | |
first = false; | |
} | |
}; | |
} | |
detector.bw = function(x) { | |
if (!arguments.length) return bw; | |
var oldBw = bw; | |
if (wax.bwlisteners && wax.bwlisteners.length) (function () { | |
listeners = wax.bwlisteners; | |
wax.bwlisteners = []; | |
for (i = 0; i < listeners; i++) { | |
listeners[i](x); | |
} | |
})(); | |
wax.bw = x; | |
if (bw != (bw = x)) callback(x); | |
}; | |
detector.add = function() { | |
if (auto) bwTest(); | |
return this; | |
}; | |
if (wax.bw == -1) { | |
wax.bwlisteners = wax.bwlisteners || []; | |
wax.bwlisteners.push(detector.bw); | |
} else if (wax.bw !== undefined) { | |
detector.bw(wax.bw); | |
} else { | |
detector.add(); | |
} | |
return detector; | |
}; | |
// Formatter | |
// --------- | |
// | |
// This code is no longer the recommended code path for Wax - | |
// see `template.js`, a safe implementation of Mustache templates. | |
wax.formatter = function(x) { | |
var formatter = {}, | |
f; | |
// Prevent against just any input being used. | |
if (x && typeof x === 'string') { | |
try { | |
// Ugly, dangerous use of eval. | |
eval('f = ' + x); | |
} catch (e) { | |
if (console) console.log(e); | |
} | |
} else if (x && typeof x === 'function') { | |
f = x; | |
} else { | |
f = function() {}; | |
} | |
function urlX(url) { | |
if (/^(https?:\/\/|data:image)/.test(url)) { | |
return url; | |
} | |
} | |
function idX(id) { | |
return id; | |
} | |
// Wrap the given formatter function in order to | |
// catch exceptions that it may throw. | |
formatter.format = function(options, data) { | |
try { | |
return html_sanitize(f(options, data), urlX, idX); | |
} catch (e) { | |
if (console) console.log(e); | |
} | |
}; | |
return formatter; | |
}; | |
// GridInstance | |
// ------------ | |
// GridInstances are queryable, fully-formed | |
// objects for acquiring features from events. | |
// | |
// This code ignores format of 1.1-1.2 | |
wax.gi = function(grid_tile, options) { | |
options = options || {}; | |
// resolution is the grid-elements-per-pixel ratio of gridded data. | |
// The size of a tile element. For now we expect tiles to be squares. | |
var instance = {}, | |
resolution = options.resolution || 4, | |
tileSize = options.tileSize || 256; | |
// Resolve the UTF-8 encoding stored in grids to simple | |
// number values. | |
// See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec) | |
// for details. | |
function resolveCode(key) { | |
if (key >= 93) key--; | |
if (key >= 35) key--; | |
key -= 32; | |
return key; | |
} | |
instance.grid_tile = function() { | |
return grid_tile; | |
}; | |
instance.getKey = function(x, y) { | |
if (!(grid_tile && grid_tile.grid)) return; | |
if ((y < 0) || (x < 0)) return; | |
if ((Math.floor(y) >= tileSize) || | |
(Math.floor(x) >= tileSize)) return; | |
// Find the key in the grid. The above calls should ensure that | |
// the grid's array is large enough to make this work. | |
return resolveCode(grid_tile.grid[ | |
Math.floor((y) / resolution) | |
].charCodeAt( | |
Math.floor((x) / resolution) | |
)); | |
}; | |
// Lower-level than tileFeature - has nothing to do | |
// with the DOM. Takes a px offset from 0, 0 of a grid. | |
instance.gridFeature = function(x, y) { | |
// Find the key in the grid. The above calls should ensure that | |
// the grid's array is large enough to make this work. | |
var key = this.getKey(x, y), | |
keys = grid_tile.keys; | |
if (keys && | |
keys[key] && | |
grid_tile.data[keys[key]]) { | |
return grid_tile.data[keys[key]]; | |
} | |
}; | |
// Get a feature: | |
// * `x` and `y`: the screen coordinates of an event | |
// * `tile_element`: a DOM element of a tile, from which we can get an offset. | |
instance.tileFeature = function(x, y, tile_element) { | |
if (!grid_tile) return; | |
// IE problem here - though recoverable, for whatever reason | |
var offset = wax.u.offset(tile_element); | |
feature = this.gridFeature(x - offset.left, y - offset.top); | |
return feature; | |
}; | |
return instance; | |
}; | |
// GridManager | |
// ----------- | |
// Generally one GridManager will be used per map. | |
// | |
// It takes one options object, which current accepts a single option: | |
// `resolution` determines the number of pixels per grid element in the grid. | |
// The default is 4. | |
wax.gm = function() { | |
var resolution = 4, | |
grid_tiles = {}, | |
manager = {}, | |
tilejson, | |
formatter; | |
var gridUrl = function(url) { | |
return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json'); | |
}; | |
function templatedGridUrl(template) { | |
if (typeof template === 'string') template = [template]; | |
return function templatedGridFinder(url) { | |
if (!url) return; | |
var rx = new RegExp('/(\\d+)\\/(\\d+)\\/(\\d+)\\.[\\w\\._]+'); | |
var xyz = rx.exec(url); | |
if (!xyz) return; | |
return template[parseInt(xyz[2], 10) % template.length] | |
.replace(/\{z\}/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) { | |
if (!arguments.length) return gridUrl; | |
gridUrl = typeof x === 'function' ? | |
x : templatedGridUrl(x); | |
return manager; | |
}; | |
manager.getGrid = function(url, callback) { | |
var gurl = gridUrl(url); | |
if (!formatter || !gurl) return callback(null, null); | |
wax.request.get(gurl, function(err, t) { | |
if (err) return callback(err, null); | |
callback(null, wax.gi(t, { | |
formatter: formatter, | |
resolution: resolution | |
})); | |
}); | |
return manager; | |
}; | |
manager.tilejson = function(x) { | |
if (!arguments.length) return tilejson; | |
// prefer templates over formatters | |
if (x.template) { | |
manager.template(x.template); | |
} else if (x.formatter) { | |
manager.formatter(x.formatter); | |
} | |
if (x.grids) manager.gridUrl(x.grids); | |
if (x.resolution) resolution = x.resolution; | |
tilejson = x; | |
return manager; | |
}; | |
return manager; | |
}; | |
wax = wax || {}; | |
// Hash | |
// ---- | |
wax.hash = function(options) { | |
options = options || {}; | |
function getState() { | |
return location.hash.substring(1); | |
} | |
function pushState(state) { | |
var l = window.location; | |
l.replace(l.toString().replace(l.hash, '#' + state)); | |
} | |
var s0, // old hash | |
hash = {}, | |
lat = 90 - 1e-8; // allowable latitude range | |
function parseHash(s) { | |
var args = s.split('/'); | |
for (var i = 0; i < args.length; i++) { | |
args[i] = Number(args[i]); | |
if (isNaN(args[i])) return true; | |
} | |
if (args.length < 3) { | |
// replace bogus hash | |
return true; | |
} else if (args.length == 3) { | |
options.setCenterZoom(args); | |
} | |
} | |
function move() { | |
var s1 = options.getCenterZoom(); | |
if (s0 !== s1) { | |
s0 = s1; | |
// don't recenter the map! | |
pushState(s0); | |
} | |
} | |
function stateChange(state) { | |
// ignore spurious hashchange events | |
if (state === s0) return; | |
if (parseHash(s0 = state)) { | |
// replace bogus hash | |
move(); | |
} | |
} | |
var _move = wax.u.throttle(move, 500); | |
hash.add = function() { | |
stateChange(getState()); | |
options.bindChange(_move); | |
return this; | |
}; | |
hash.remove = function() { | |
options.unbindChange(_move); | |
return this; | |
}; | |
return hash.add(); | |
}; | |
wax = wax || {}; | |
wax.interaction = function() { | |
var gm = wax.gm(), | |
interaction = {}, | |
_downLock = false, | |
_clickTimeout = false, | |
// Active feature | |
// Down event | |
_d, | |
// Touch tolerance | |
tol = 4, | |
grid, | |
attach, | |
detach, | |
parent, | |
map, | |
tileGrid; | |
var defaultEvents = { | |
mousemove: onMove, | |
touchstart: onDown, | |
mousedown: onDown | |
}; | |
var touchEnds = { | |
touchend: onUp, | |
touchmove: onUp, | |
touchcancel: touchCancel | |
}; | |
// Abstract getTile method. Depends on a tilegrid with | |
// grid[ [x, y, tile] ] structure. | |
function getTile(e) { | |
var g = grid(); | |
for (var i = 0; i < g.length; i++) { | |
if ((g[i][0] < e.y) && | |
((g[i][0] + 256) > e.y) && | |
(g[i][1] < e.x) && | |
((g[i][1] + 256) > e.x)) return g[i][2]; | |
} | |
return false; | |
} | |
// Clear the double-click timeout to prevent double-clicks from | |
// triggering popups. | |
function killTimeout() { | |
if (_clickTimeout) { | |
window.clearTimeout(_clickTimeout); | |
_clickTimeout = null; | |
return true; | |
} else { | |
return false; | |
} | |
} | |
function onMove(e) { | |
// If the user is actually dragging the map, exit early | |
// to avoid performance hits. | |
if (_downLock) return; | |
var pos = wax.u.eventoffset(e); | |
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); | |
// 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; | |
function urlX(url) { | |
// Data URIs are subject to a bug in Firefox | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 | |
// which let them be a vector. But WebKit does 'the right thing' | |
// or at least 'something' about this situation, so we'll tolerate | |
// them. | |
if (/^(https?:\/\/|data:image)/.test(url)) { | |
return url; | |
} | |
} | |
function idX(id) { | |
return id; | |
} | |
legend.element = function() { | |
return container; | |
}; | |
legend.content = function(content) { | |
if (!arguments.length) return element.innerHTML; | |
if (content) { | |
element.innerHTML = html_sanitize(content, urlX, idX); | |
element.style.display = 'block'; | |
} else { | |
element.innerHTML = ''; | |
element.style.display = 'none'; | |
} | |
return legend; | |
}; | |
legend.add = function() { | |
container = document.createElement('div'); | |
container.className = 'wax-legends'; | |
element = container.appendChild(document.createElement('div')); | |
element.className = 'wax-legend'; | |
element.style.display = 'none'; | |
return legend; | |
}; | |
return legend.add(); | |
}; | |
var wax = wax || {}; | |
wax.location = function() { | |
var t = {}; | |
function on(o) { | |
console.log(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 = 'wax-tooltip wax-tooltip-0'; | |
tooltip.innerHTML = feature; | |
return tooltip; | |
} | |
// Hide a given tooltip. | |
function hide() { | |
if (tooltip) { | |
tooltip.parentNode.removeChild(tooltip); | |
tooltip = null; | |
} | |
} | |
function on(o) { | |
var content; | |
if (popped) return; | |
if ((o.e.type === 'mousemove' || !o.e.type)) { | |
content = o.formatter({ format: 'teaser' }, o.data); | |
if (!content) return; | |
hide(); | |
parent.style.cursor = 'pointer'; | |
tooltip = document.body.appendChild(getTooltip(content)); | |
} else { | |
content = o.formatter({ format: 'teaser' }, o.data); | |
if (!content) return; | |
hide(); | |
var tt = document.body.appendChild(getTooltip(content)); | |
tt.className += ' wax-popup'; | |
var close = tt.appendChild(document.createElement('a')); | |
close.href = '#close'; | |
close.className = 'close'; | |
close.innerHTML = 'Close'; | |
popped = true; | |
tooltip = tt; | |
_tooltipOffset = wax.u.offset(tooltip); | |
_contextOffset = wax.u.offset(parent); | |
moveTooltip(o.e); | |
bean.add(close, 'click touchend', function closeClick(e) { | |
e.stop(); | |
hide(); | |
popped = false; | |
}); | |
} | |
if (tooltip) { | |
_tooltipOffset = wax.u.offset(tooltip); | |
_contextOffset = wax.u.offset(parent); | |
moveTooltip(o.e); | |
} | |
} | |
function off() { | |
parent.style.cursor = 'default'; | |
if (!popped) hide(); | |
} | |
t.parent = function(x) { | |
if (!arguments.length) return parent; | |
parent = x; | |
return t; | |
}; | |
t.events = function() { | |
return { | |
on: on, | |
off: off | |
}; | |
}; | |
return t; | |
}; | |
// Wax GridUtil | |
// ------------ | |
// Wax header | |
var wax = wax || {}; | |
// Request | |
// ------- | |
// Request data cache. `callback(data)` where `data` is the response data. | |
wax.request = { | |
cache: {}, | |
locks: {}, | |
promises: {}, | |
get: function(url, callback) { | |
// Cache hit. | |
if (this.cache[url]) { | |
return callback(this.cache[url][0], this.cache[url][1]); | |
// Cache miss. | |
} else { | |
this.promises[url] = this.promises[url] || []; | |
this.promises[url].push(callback); | |
// Lock hit. | |
if (this.locks[url]) return; | |
// Request. | |
var that = this; | |
this.locks[url] = true; | |
reqwest({ | |
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid', | |
type: 'jsonp', | |
jsonpCallback: 'callback', | |
success: function(data) { | |
that.locks[url] = false; | |
that.cache[url] = [null, data]; | |
for (var i = 0; i < that.promises[url].length; i++) { | |
that.promises[url][i](that.cache[url][0], that.cache[url][1]); | |
} | |
}, | |
error: function(err) { | |
that.locks[url] = false; | |
that.cache[url] = [err, null]; | |
for (var i = 0; i < that.promises[url].length; i++) { | |
that.promises[url][i](that.cache[url][0], that.cache[url][1]); | |
} | |
} | |
}); | |
} | |
} | |
}; | |
// Templating | |
// --------- | |
wax.template = function(x) { | |
var template = {}; | |
function urlX(url) { | |
// Data URIs are subject to a bug in Firefox | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107 | |
// which let them be a vector. But WebKit does 'the right thing' | |
// or at least 'something' about this situation, so we'll tolerate | |
// them. | |
if (/^(https?:\/\/|data:image)/.test(url)) { | |
return url; | |
} | |
} | |
function idX(id) { | |
return id; | |
} | |
// Clone the data object such that the '__[format]__' key is only | |
// set for this instance of templating. | |
template.format = function(options, data) { | |
var clone = {}; | |
for (var key in data) { | |
clone[key] = data[key]; | |
} | |
if (options.format) { | |
clone['__' + options.format + '__'] = true; | |
} | |
return html_sanitize(Mustache.to_html(x, clone), urlX, idX); | |
}; | |
return template; | |
}; | |
if (!wax) var wax = {}; | |
// A wrapper for reqwest jsonp to easily load TileJSON from a URL. | |
wax.tilejson = function(url, callback) { | |
reqwest({ | |
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid', | |
type: 'jsonp', | |
jsonpCallback: 'callback', | |
success: callback, | |
error: callback | |
}); | |
}; | |
var wax = wax || {}; | |
wax.tooltip = {}; | |
wax.tooltip = function() { | |
var popped = false, | |
animate = false, | |
t = {}, | |
tooltips = [], | |
_currentContent, | |
transitionEvent, | |
parent; | |
if (document.body.style['-webkit-transition'] !== undefined) { | |
transitionEvent = 'webkitTransitionEnd'; | |
} else if (document.body.style.MozTransition !== undefined) { | |
transitionEvent = 'transitionend'; | |
} | |
// Get the active tooltip for a layer or create a new one if no tooltip exists. | |
// Hide any tooltips on layers underneath this one. | |
function getTooltip(feature) { | |
var tooltip = document.createElement('div'); | |
tooltip.className = 'wax-tooltip wax-tooltip-0'; | |
tooltip.innerHTML = feature; | |
return tooltip; | |
} | |
function remove() { | |
if (this.parentNode) this.parentNode.removeChild(this); | |
} | |
// Hide a given tooltip. | |
function hide() { | |
var _ct; | |
while (_ct = tooltips.pop()) { | |
if (animate && transitionEvent) { | |
// This code assumes that transform-supporting browsers | |
// also support proper events. IE9 does both. | |
bean.add(_ct, transitionEvent, remove); | |
_ct.className += ' wax-fade'; | |
} else { | |
if (_ct.parentNode) _ct.parentNode.removeChild(_ct); | |
} | |
} | |
} | |
function on(o) { | |
var content; | |
if (o.e.type === 'mousemove' || !o.e.type) { | |
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 += ' wax-popup'; | |
var close = tt.appendChild(document.createElement('a')); | |
close.href = '#close'; | |
close.className = 'close'; | |
close.innerHTML = 'Close'; | |
popped = true; | |
tooltips.push(tt); | |
bean.add(close, '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) { | |
if (match = style.match(/translate\((.+)px, (.+)px\)/)) { | |
top += parseInt(match[2], 10); | |
left += parseInt(match[1], 10); | |
} else if (match = style.match(/translate3d\((.+)px, (.+)px, (.+)px\)/)) { | |
top += parseInt(match[2], 10); | |
left += parseInt(match[1], 10); | |
} else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) { | |
var pts = match[1].split(','); | |
top += parseInt(pts[13], 10); | |
left += parseInt(pts[12], 10); | |
} else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) { | |
top += parseInt(match[2], 10); | |
left += parseInt(match[1], 10); | |
} | |
} | |
}; | |
calculateOffset(el); | |
try { | |
while (el = el.offsetParent) { calculateOffset(el); } | |
} catch(e) { | |
// Hello, internet explorer. | |
} | |
// Offsets from the body | |
top += doc_body.offsetTop; | |
left += doc_body.offsetLeft; | |
// Offsets from the HTML element | |
top += doc_body.parentNode.offsetTop; | |
left += doc_body.parentNode.offsetLeft; | |
// Firefox and other weirdos. Similar technique to jQuery's | |
// `doesNotIncludeMarginInBodyOffset`. | |
var htmlComputed = document.defaultView ? | |
window.getComputedStyle(doc_body.parentNode, null) : | |
doc_body.parentNode.currentStyle; | |
if (doc_body.parentNode.offsetTop !== | |
parseInt(htmlComputed.marginTop, 10) && | |
!isNaN(parseInt(htmlComputed.marginTop, 10))) { | |
top += parseInt(htmlComputed.marginTop, 10); | |
left += parseInt(htmlComputed.marginLeft, 10); | |
} | |
return { | |
top: top, | |
left: left, | |
height: height, | |
width: width | |
}; | |
}, | |
'$': function(x) { | |
return (typeof x === 'string') ? | |
document.getElementById(x) : | |
x; | |
}, | |
// IE doesn't have indexOf | |
indexOf: function(array, item) { | |
var nativeIndexOf = Array.prototype.indexOf; | |
if (array === null) return -1; | |
var i, l; | |
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); | |
for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; | |
return -1; | |
}, | |
// From quirksmode: normalize the offset of an event from the top-left | |
// of the page. | |
eventoffset: function(e) { | |
var posx = 0; | |
var posy = 0; | |
if (!e) { e = window.event; } | |
if (e.pageX || e.pageY) { | |
// Good browsers | |
return { | |
x: e.pageX, | |
y: e.pageY | |
}; | |
} else if (e.clientX || e.clientY) { | |
// Internet Explorer | |
var doc = document.documentElement, body = document.body; | |
var htmlComputed = document.body.parentNode.currentStyle; | |
var topMargin = parseInt(htmlComputed.marginTop, 10) || 0; | |
var leftMargin = parseInt(htmlComputed.marginLeft, 10) || 0; | |
return { | |
x: e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - | |
(doc && doc.clientLeft || body && body.clientLeft || 0) + leftMargin, | |
y: e.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - | |
(doc && doc.clientTop || body && body.clientTop || 0) + topMargin | |
}; | |
} else if (e.touches && e.touches.length === 1) { | |
// Touch browsers | |
return { | |
x: e.touches[0].pageX, | |
y: e.touches[0].pageY | |
}; | |
} | |
}, | |
// Ripped from underscore.js | |
// Internal function used to implement `_.throttle` and `_.debounce`. | |
limit: function(func, wait, debounce) { | |
var timeout; | |
return function() { | |
var context = this, args = arguments; | |
var throttler = function() { | |
timeout = null; | |
func.apply(context, args); | |
}; | |
if (debounce) clearTimeout(timeout); | |
if (debounce || !timeout) timeout = setTimeout(throttler, wait); | |
}; | |
}, | |
// Returns a function, that, when invoked, will only be triggered at most once | |
// during a given window of time. | |
throttle: function(func, wait) { | |
return this.limit(func, wait, false); | |
} | |
}; | |
wax = wax || {}; | |
wax.leaf = wax.leaf || {}; | |
wax.leaf.interaction = function() { | |
var dirty = false, _grid, map; | |
function setdirty() { dirty = true; } | |
function grid() { | |
// TODO: don't build for tiles outside of viewport | |
// Touch interaction leads to intermediate | |
//var zoomLayer = map.createOrGetLayer(Math.round(map.getZoom())); //?what is this doing? | |
// Calculate a tile grid and cache it, by using the `.tiles` | |
// element on this map. | |
if (!dirty && _grid) { | |
return _grid; | |
} else { | |
return (_grid = (function(layers) { | |
var o = []; | |
for (var layerId in layers) { | |
// This only supports tiled layers | |
if (layers[layerId]._tiles) { | |
for (var tile in layers[layerId]._tiles) { | |
var offset = wax.u.offset(layers[layerId]._tiles[tile]); | |
o.push([offset.top, offset.left, layers[layerId]._tiles[tile]]); | |
} | |
} | |
} | |
return o; | |
})(map._layers)); | |
} | |
} | |
function attach(x) { | |
if (!arguments.length) return map; | |
map = x; | |
var l = ['moveend']; | |
for (var i = 0; i < l.length; i++) { | |
map.on(l[i], setdirty); | |
} | |
} | |
function detach(x) { | |
if (!arguments.length) return map; | |
map = x; | |
var l = ['moveend']; | |
for (var i = 0; i < l.length; i++) { | |
map.off(l[i], setdirty); | |
} | |
} | |
return wax.interaction() | |
.attach(attach) | |
.detach(detach) | |
.parent(function() { | |
return map._container; | |
}) | |
.grid(grid); | |
}; | |
wax = wax || {}; | |
wax.leaf = wax.leaf || {}; | |
// Legend Control | |
// -------------- | |
// The Leaflet version of this control is a very, very | |
// light wrapper around the `/lib` code for legends. | |
wax.leaf.legend = function(map, tilejson) { | |
tilejson = tilejson || {}; | |
var l, // parent legend | |
legend = {}; | |
legend.add = function() { | |
l = wax.legend() | |
.content(tilejson.legend || ''); | |
return this; | |
}; | |
legend.content = function(x) { | |
if (x) l.content(x.legend || ''); | |
}; | |
legend.element = function() { | |
return l.element(); | |
}; | |
legend.appendTo = function(elem) { | |
wax.u.$(elem).appendChild(l.element()); | |
return this; | |
}; | |
return legend.add(); | |
}; | |
wax = wax || {}; | |
wax.leaf = wax.leaf || {}; | |
wax.leaf.connector = L.TileLayer.extend({ | |
initialize: function(options) { | |
options = options || {}; | |
options.minZoom = options.minzoom || 0; | |
options.maxZoom = options.maxzoom || 22; | |
L.TileLayer.prototype.initialize.call(this, options.tiles[0], options); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment