3371 lines
103 KiB
JavaScript
3371 lines
103 KiB
JavaScript
/* wax - 7.0.1 - v6.0.4-181-ga34788e */
|
|
|
|
|
|
!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);
|
|
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
|
|
'MSPointerUp MSPointerDown MSPointerCancel MSPointerMove ' + // MS Pointer events
|
|
'MSPointerOver MSPointerOut ' + // MS Pointer events
|
|
'pointerup pointerdown pointermove pointercancel' + // MS Pointer events
|
|
'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;
|
|
/*!
|
|
* Reqwest! A general purpose XHR connection manager
|
|
* (c) Dustin Diaz 2012
|
|
* https://github.com/ded/reqwest
|
|
* license MIT
|
|
*/
|
|
;(function (name, context, definition) {
|
|
context[name] = definition()
|
|
})('reqwest', this, function () {
|
|
|
|
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
|
|
, callbackPrefix = 'reqwest_' + (+new Date())
|
|
, lastValue // data stored by the most recent JSONP callback
|
|
, xmlHttpRequest = 'XMLHttpRequest'
|
|
, noop = function () {}
|
|
|
|
, isArray = typeof Array.isArray == 'function'
|
|
? Array.isArray
|
|
: function (a) {
|
|
return a instanceof Array
|
|
}
|
|
|
|
, defaultHeaders = {
|
|
contentType: 'application/x-www-form-urlencoded'
|
|
, requestedWith: xmlHttpRequest
|
|
, 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'
|
|
}
|
|
}
|
|
|
|
, xhr = win[xmlHttpRequest]
|
|
? function () {
|
|
return new XMLHttpRequest()
|
|
}
|
|
: function () {
|
|
return new ActiveXObject('Microsoft.XMLHTTP')
|
|
}
|
|
|
|
function handleReadyState (r, success, error) {
|
|
return function () {
|
|
// use _aborted to mitigate against IE err c00c023f
|
|
// (can't read props on aborted request objects)
|
|
if (r._aborted) return error(r.request)
|
|
if (r.request && r.request[readyState] == 4) {
|
|
r.request.onreadystatechange = noop
|
|
if (twoHundo.test(r.request.status))
|
|
success(r.request)
|
|
else
|
|
error(r.request)
|
|
}
|
|
}
|
|
}
|
|
|
|
function setHeaders (http, o) {
|
|
var headers = o.headers || {}
|
|
, h
|
|
|
|
headers.Accept = headers.Accept
|
|
|| defaultHeaders.accept[o.type]
|
|
|| defaultHeaders.accept['*']
|
|
|
|
// breaks cross-origin requests with legacy browsers
|
|
if (!o.crossOrigin && !headers[requestedWith]) headers[requestedWith] = defaultHeaders.requestedWith
|
|
if (!headers[contentType]) headers[contentType] = o.contentType || defaultHeaders.contentType
|
|
for (h in headers)
|
|
headers.hasOwnProperty(h) && http.setRequestHeader(h, headers[h])
|
|
}
|
|
|
|
function setCredentials (http, o) {
|
|
if (typeof o.withCredentials !== 'undefined' && typeof http.withCredentials !== 'undefined') {
|
|
http.withCredentials = !!o.withCredentials
|
|
}
|
|
}
|
|
|
|
function generalCallback (data) {
|
|
lastValue = data
|
|
}
|
|
|
|
function urlappend (url, s) {
|
|
return url + (/\?/.test(url) ? '&' : '?') + s
|
|
}
|
|
|
|
function handleJsonp (o, fn, err, url) {
|
|
var reqId = uniqid++
|
|
, cbkey = o.jsonpCallback || 'callback' // the 'callback' key
|
|
, cbval = o.jsonpCallbackName || reqwest.getcallbackPrefix(reqId)
|
|
// , cbval = o.jsonpCallbackName || ('reqwest_' + reqId) // the 'callback' value
|
|
, cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
|
|
, match = url.match(cbreg)
|
|
, script = doc.createElement('script')
|
|
, loaded = 0
|
|
, isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1;
|
|
|
|
if (match) {
|
|
if (match[3] === '?') {
|
|
url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name
|
|
} else {
|
|
cbval = match[3] // provided callback func name
|
|
}
|
|
} else {
|
|
url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em
|
|
}
|
|
|
|
win[cbval] = generalCallback
|
|
|
|
script.type = 'text/javascript'
|
|
if (typeof script.onreadystatechange !== 'undefined' && !isIE10) {
|
|
// need this for IE due to out-of-order onreadystatechange(), binding script
|
|
// execution to an event listener gives us control over when the script
|
|
// is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
|
|
//
|
|
// if this hack is used in IE10 jsonp callback are never called
|
|
script.event = 'onclick'
|
|
script.htmlFor = script.id = '_reqwest_' + reqId
|
|
}
|
|
|
|
script.onload = script.onreadystatechange = function () {
|
|
if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
|
|
return false
|
|
}
|
|
script.onload = script.onreadystatechange = null
|
|
script.onclick && script.onclick()
|
|
// Call the user callback with the last value stored and clean up values and scripts.
|
|
o.success && o.success(lastValue)
|
|
lastValue = undefined
|
|
head.removeChild(script)
|
|
loaded = 1
|
|
}
|
|
|
|
script.src = url
|
|
script.async = true
|
|
|
|
// Add the script to the DOM head
|
|
head.appendChild(script)
|
|
|
|
// Enable JSONP timeout
|
|
return {
|
|
abort: function () {
|
|
script.onload = script.onreadystatechange = null
|
|
o.error && o.error({}, 'Request is aborted: timeout', {})
|
|
lastValue = undefined
|
|
head.removeChild(script)
|
|
loaded = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRequest (fn, err) {
|
|
var o = this.o
|
|
, method = (o.method || 'GET').toUpperCase()
|
|
, url = typeof o === 'string' ? o : o.url
|
|
// convert non-string objects to query-string form unless o.processData is false
|
|
, data = (o.processData !== false && o.data && typeof o.data !== 'string')
|
|
? reqwest.toQueryString(o.data)
|
|
: (o.data || null)
|
|
, http
|
|
|
|
// if we're working on a GET request and we have data then we should append
|
|
// query string to end of URL and not post data
|
|
if ((o.type == 'jsonp' || method == 'GET') && data) {
|
|
url = urlappend(url, data)
|
|
data = null
|
|
}
|
|
|
|
if (o.type == 'jsonp') return handleJsonp(o, fn, err, url)
|
|
|
|
http = xhr()
|
|
http.open(method, url, true)
|
|
setHeaders(http, o)
|
|
setCredentials(http, o)
|
|
http.onreadystatechange = handleReadyState(this, fn, err)
|
|
o.before && o.before(http)
|
|
http.send(data)
|
|
return http
|
|
}
|
|
|
|
function Reqwest (o, fn) {
|
|
this.o = o
|
|
this.fn = fn
|
|
|
|
init.apply(this, arguments)
|
|
}
|
|
|
|
function setType (url) {
|
|
var m = url.match(/\.(json|jsonp|html|xml)(\?|$)/)
|
|
return m ? m[1] : 'js'
|
|
}
|
|
|
|
function init (o, fn) {
|
|
|
|
this.url = typeof o == 'string' ? o : o.url
|
|
this.timeout = null
|
|
|
|
// whether request has been fulfilled for purpose
|
|
// of tracking the Promises
|
|
this._fulfilled = false
|
|
// success handlers
|
|
this._fulfillmentHandlers = []
|
|
// error handlers
|
|
this._errorHandlers = []
|
|
// complete (both success and fail) handlers
|
|
this._completeHandlers = []
|
|
this._erred = false
|
|
this._responseArgs = {}
|
|
|
|
var self = this
|
|
, type = o.type || setType(this.url)
|
|
|
|
fn = fn || function () {}
|
|
|
|
if (o.timeout) {
|
|
this.timeout = setTimeout(function () {
|
|
self.abort()
|
|
}, o.timeout)
|
|
}
|
|
|
|
if (o.success) {
|
|
this._fulfillmentHandlers.push(function () {
|
|
o.success.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
if (o.error) {
|
|
this._errorHandlers.push(function () {
|
|
o.error.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
if (o.complete) {
|
|
this._completeHandlers.push(function () {
|
|
o.complete.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
function complete (resp) {
|
|
o.timeout && clearTimeout(self.timeout)
|
|
self.timeout = null
|
|
while (self._completeHandlers.length > 0) {
|
|
self._completeHandlers.shift()(resp)
|
|
}
|
|
}
|
|
|
|
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
|
|
break
|
|
case 'xml':
|
|
resp = resp.responseXML
|
|
&& resp.responseXML.parseError // IE trololo
|
|
&& resp.responseXML.parseError.errorCode
|
|
&& resp.responseXML.parseError.reason
|
|
? null
|
|
: resp.responseXML
|
|
break
|
|
}
|
|
}
|
|
|
|
self._responseArgs.resp = resp
|
|
self._fulfilled = true
|
|
fn(resp)
|
|
while (self._fulfillmentHandlers.length > 0) {
|
|
self._fulfillmentHandlers.shift()(resp)
|
|
}
|
|
|
|
complete(resp)
|
|
}
|
|
|
|
function error (resp, msg, t) {
|
|
self._responseArgs.resp = resp
|
|
self._responseArgs.msg = msg
|
|
self._responseArgs.t = t
|
|
self._erred = true
|
|
while (self._errorHandlers.length > 0) {
|
|
self._errorHandlers.shift()(resp, msg, t)
|
|
}
|
|
complete(resp)
|
|
}
|
|
|
|
this.request = getRequest.call(this, success, error)
|
|
}
|
|
|
|
Reqwest.prototype = {
|
|
abort: function () {
|
|
this._aborted = true
|
|
this.request.abort()
|
|
}
|
|
|
|
, retry: function () {
|
|
init.call(this, this.o, this.fn)
|
|
}
|
|
|
|
/**
|
|
* Small deviation from the Promises A CommonJs specification
|
|
* http://wiki.commonjs.org/wiki/Promises/A
|
|
*/
|
|
|
|
/**
|
|
* `then` will execute upon successful requests
|
|
*/
|
|
, then: function (success, fail) {
|
|
if (this._fulfilled) {
|
|
success(this._responseArgs.resp)
|
|
} else if (this._erred) {
|
|
fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
|
} else {
|
|
this._fulfillmentHandlers.push(success)
|
|
this._errorHandlers.push(fail)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* `always` will execute whether the request succeeds or fails
|
|
*/
|
|
, always: function (fn) {
|
|
if (this._fulfilled || this._erred) {
|
|
fn(this._responseArgs.resp)
|
|
} else {
|
|
this._completeHandlers.push(fn)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* `fail` will execute when the request fails
|
|
*/
|
|
, fail: function (fn) {
|
|
if (this._erred) {
|
|
fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
|
} else {
|
|
this._errorHandlers.push(fn)
|
|
}
|
|
return this
|
|
}
|
|
}
|
|
|
|
function reqwest (o, fn) {
|
|
return new Reqwest(o, fn)
|
|
}
|
|
|
|
// normalize newline variants according to spec -> CRLF
|
|
function normalize (s) {
|
|
return s ? s.replace(/\r?\n/g, '\r\n') : ''
|
|
}
|
|
|
|
function serial (el, cb) {
|
|
var n = el.name
|
|
, t = el.tagName.toLowerCase()
|
|
, optCb = function (o) {
|
|
// IE gives value="" even where there is no value attribute
|
|
// 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
|
|
if (o && !o.disabled)
|
|
cb(n, normalize(o.attributes.value && o.attributes.value.specified ? o.value : o.text))
|
|
}
|
|
, ch, ra, val, i
|
|
|
|
// don't serialize elements that are disabled or without a name
|
|
if (el.disabled || !n) return
|
|
|
|
switch (t) {
|
|
case 'input':
|
|
if (!/reset|button|image|file/i.test(el.type)) {
|
|
ch = /checkbox/i.test(el.type)
|
|
ra = /radio/i.test(el.type)
|
|
val = el.value
|
|
// WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
|
|
;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
|
|
}
|
|
break
|
|
case 'textarea':
|
|
cb(n, normalize(el.value))
|
|
break
|
|
case 'select':
|
|
if (el.type.toLowerCase() === 'select-one') {
|
|
optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
|
|
} else {
|
|
for (i = 0; el.length && i < el.length; i++) {
|
|
el.options[i].selected && optCb(el.options[i])
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// collect up all form elements found from the passed argument elements all
|
|
// the way down to child elements; pass a '<form>' or form fields.
|
|
// called with 'this'=callback to use for serial() on each element
|
|
function eachFormElement () {
|
|
var cb = this
|
|
, e, i
|
|
, serializeSubtags = function (e, tags) {
|
|
var i, j, fa
|
|
for (i = 0; i < tags.length; i++) {
|
|
fa = e[byTag](tags[i])
|
|
for (j = 0; j < fa.length; j++) serial(fa[j], cb)
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < arguments.length; i++) {
|
|
e = arguments[i]
|
|
if (/input|select|textarea/i.test(e.tagName)) serial(e, cb)
|
|
serializeSubtags(e, [ 'input', 'select', 'textarea' ])
|
|
}
|
|
}
|
|
|
|
// standard query string style serialization
|
|
function serializeQueryString () {
|
|
return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments))
|
|
}
|
|
|
|
// { 'name': 'value', ... } style serialization
|
|
function serializeHash () {
|
|
var hash = {}
|
|
eachFormElement.apply(function (name, value) {
|
|
if (name in hash) {
|
|
hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]])
|
|
hash[name].push(value)
|
|
} else hash[name] = value
|
|
}, arguments)
|
|
return hash
|
|
}
|
|
|
|
// [ { name: 'name', value: 'value' }, ... ] style serialization
|
|
reqwest.serializeArray = function () {
|
|
var arr = []
|
|
eachFormElement.apply(function (name, value) {
|
|
arr.push({name: name, value: value})
|
|
}, arguments)
|
|
return arr
|
|
}
|
|
|
|
reqwest.serialize = function () {
|
|
if (arguments.length === 0) return ''
|
|
var opt, fn
|
|
, args = Array.prototype.slice.call(arguments, 0)
|
|
|
|
opt = args.pop()
|
|
opt && opt.nodeType && args.push(opt) && (opt = null)
|
|
opt && (opt = opt.type)
|
|
|
|
if (opt == 'map') fn = serializeHash
|
|
else if (opt == 'array') fn = reqwest.serializeArray
|
|
else fn = serializeQueryString
|
|
|
|
return fn.apply(null, args)
|
|
}
|
|
|
|
reqwest.toQueryString = function (o) {
|
|
var qs = '', i
|
|
, enc = encodeURIComponent
|
|
, push = function (k, v) {
|
|
qs += enc(k) + '=' + enc(v) + '&'
|
|
}
|
|
, k, v
|
|
|
|
if (isArray(o)) {
|
|
for (i = 0; o && i < o.length; i++) push(o[i].name, o[i].value)
|
|
} else {
|
|
for (k in o) {
|
|
if (!Object.hasOwnProperty.call(o, k)) continue
|
|
v = o[k]
|
|
if (isArray(v)) {
|
|
for (i = 0; i < v.length; i++) push(k, v[i])
|
|
} else push(k, o[k])
|
|
}
|
|
}
|
|
|
|
// spaces should be + according to spec
|
|
return qs.replace(/&$/, '').replace(/%20/g, '+')
|
|
}
|
|
|
|
reqwest.getcallbackPrefix = function () {
|
|
return callbackPrefix
|
|
}
|
|
|
|
// jQuery and Zepto compatibility, differences can be remapped here so you can call
|
|
// .ajax.compat(options, callback)
|
|
reqwest.compat = function (o, fn) {
|
|
if (o) {
|
|
o.type && (o.method = o.type) && delete o.type
|
|
o.dataType && (o.type = o.dataType)
|
|
o.jsonpCallback && (o.jsonpCallbackName = o.jsonpCallback) && delete o.jsonpCallback
|
|
o.jsonp && (o.jsonpCallback = o.jsonp)
|
|
}
|
|
return new Reqwest(o, fn)
|
|
}
|
|
|
|
return 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(manager.tileRegexp())
|
|
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]);
|
|
};
|
|
}
|
|
|
|
// return the regexp to catch the tile number given the url
|
|
manager.tileRegexp = function() {
|
|
var tileTemplate = tilejson.tiles[0];
|
|
// remove params
|
|
var p = tileTemplate.indexOf('?');
|
|
if(p !== -1) {
|
|
tileTemplate = tileTemplate.substr(0, p);
|
|
}
|
|
// remove from the url all the special characters
|
|
// replacing them by a dot (dont mind the character)
|
|
tileTemplate = tileTemplate.
|
|
replace(/[\(\)\?\$\*\+\^]/g,'.')
|
|
|
|
// the browser removes the port in the case it matchs with
|
|
// the default port of the protocol
|
|
if(tileTemplate.indexOf('https') === 0) {
|
|
tileTemplate = tileTemplate.replace(':443', '[:0-9]*')
|
|
} else if(tileTemplate.indexOf('http') === 0) {
|
|
tileTemplate = tileTemplate.replace(':80', '[:0-9]*')
|
|
}
|
|
|
|
var r = '';
|
|
if(tilejson.tiles.length > 1) {
|
|
var t0 = tilejson.tiles[0];
|
|
var t1 = tilejson.tiles[1];
|
|
//search characters where differs
|
|
for(var i = 0; i < t0.length; ++i) {
|
|
if(t0.charAt(i) != t1.charAt(i)) {
|
|
r += '.';
|
|
} else {
|
|
r += tileTemplate.charAt(i) || '';
|
|
}
|
|
}
|
|
} else {
|
|
r = tileTemplate;
|
|
}
|
|
|
|
// replace the first {x}{y}{z} by (\\d+)
|
|
return r
|
|
.replace(/\{x\}/,'(\\d+)')
|
|
.replace(/\{y\}/,'(\\d+)')
|
|
.replace(/\{z\}/,'(\\d+)')
|
|
}
|
|
|
|
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 = null,
|
|
// Active feature
|
|
// Down event
|
|
_d,
|
|
// Touch tolerance
|
|
tol = 4,
|
|
grid,
|
|
attach,
|
|
detach,
|
|
parent,
|
|
map,
|
|
tileGrid,
|
|
// google maps sends touchmove and click at the same time
|
|
// most of the time when an user taps the screen, see onUp
|
|
// for more information
|
|
_discardTouchMove = false;
|
|
|
|
var defaultEvents = {
|
|
mousemove: onMove,
|
|
touchstart: onDown,
|
|
mousedown: onDown
|
|
};
|
|
|
|
var touchEnds = {
|
|
touchend: onUp,
|
|
touchmove: onUp,
|
|
touchcancel: touchCancel
|
|
};
|
|
|
|
var mspointerEnds = {
|
|
MSPointerUp: onUp,
|
|
MSPointerMove: onUp,
|
|
MSPointerCancel: touchCancel
|
|
};
|
|
|
|
var pointerEnds = {
|
|
pointerup: onUp,
|
|
pointermove: onUp,
|
|
pointercancel: touchCancel
|
|
};
|
|
|
|
// Abstract getTile method. Depends on a tilegrid with
|
|
// grid[ [x, y, tile] ] structure.
|
|
function getTile(e) {
|
|
var g = grid();
|
|
var regExp = new RegExp(gm.tileRegexp());
|
|
for (var i = 0; i < g.length; i++) {
|
|
if (e) {
|
|
var isInside = ((g[i][0] <= e.y) &&
|
|
((g[i][0] + 256) > e.y) &&
|
|
(g[i][1] <= e.x) &&
|
|
((g[i][1] + 256) > e.x));
|
|
if(isInside && regExp.exec(g[i][2].src)) {
|
|
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 _e = (e.type !== "MSPointerMove" && e.type !== "pointermove" ? e : e.originalEvent);
|
|
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) {
|
|
|
|
// 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;
|
|
var _e = (e.type !== "MSPointerDown" && e.type !== "pointerdown" ? e : e.originalEvent);
|
|
_d = wax.u.eventoffset(_e);
|
|
if (e.type === 'mousedown') {
|
|
bean.add(document.body, 'click', onUp);
|
|
// track mouse up to remove lockDown when the drags end
|
|
bean.add(document.body, 'mouseup', dragEnd);
|
|
|
|
// Only track single-touches. Double-touches will not affect this
|
|
// control
|
|
} else if (e.type === 'touchstart' && e.touches.length === 1) {
|
|
//GMaps fix: Because it's triggering always mousedown and click, we've to remove it
|
|
bean.remove(document.body, 'click', onUp); //GMaps fix
|
|
|
|
//When we finish dragging, then the click will be
|
|
bean.add(document.body, 'click', onUp);
|
|
bean.add(document.body, 'touchEnd', dragEnd);
|
|
} else if (e.originalEvent.type === "MSPointerDown" && e.originalEvent.touches && e.originalEvent.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(), mspointerEnds);
|
|
} else if (e.type === "pointerdown" && e.originalEvent.touches && e.originalEvent.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(), pointerEnds);
|
|
} else {
|
|
// Fix layer interaction in IE10/11 (CDBjs #139)
|
|
// Reason: Internet Explorer is triggering pointerdown when you click on the marker, and other browsers don't.
|
|
// Because of that, _downLock was active and it believed that you're dragging the map, instead of dragging the marker
|
|
_downLock = false;
|
|
}
|
|
|
|
}
|
|
|
|
function dragEnd() {
|
|
_downLock = false;
|
|
}
|
|
|
|
function touchCancel() {
|
|
bean.remove(parent(), touchEnds);
|
|
bean.remove(parent(), mspointerEnds);
|
|
bean.remove(parent(), pointerEnds);
|
|
_downLock = false;
|
|
}
|
|
|
|
function onUp(e) {
|
|
var evt = {};
|
|
var _e = (e.type !== "MSPointerMove" && e.type !== "MSPointerUp" && e.type !== "pointerup" && e.type !== "pointermove" ? e : e.originalEvent);
|
|
var pos = wax.u.eventoffset(_e);
|
|
_downLock = false;
|
|
|
|
for (var key in _e) {
|
|
evt[key] = _e[key];
|
|
}
|
|
|
|
// for (var key in e) {
|
|
// evt[key] = e[key];
|
|
// }
|
|
|
|
bean.remove(document.body, 'mouseup', onUp);
|
|
bean.remove(parent(), touchEnds);
|
|
bean.remove(parent(), mspointerEnds);
|
|
bean.remove(parent(), pointerEnds);
|
|
|
|
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 (pos && _d) {
|
|
// If pos is not defined means wax can't calculate event position,
|
|
// So next cases aren't possible.
|
|
|
|
if (evt.type === "MSPointerMove" || evt.type === "MSPointerUp") {
|
|
evt.changedTouches = [];
|
|
interaction.click(evt, pos);
|
|
} else if (evt.type === "pointermove" || evt.type === "pointerup") {
|
|
interaction.click(evt, pos);
|
|
} else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
|
|
Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
|
|
// if mousemove and click are sent at the same time this code
|
|
// will not trigger click event because less than 150ms pass between
|
|
// those events.
|
|
// Because of that this flag discards touchMove
|
|
if (_discardTouchMove && evt.type === 'touchmove') return onUp;
|
|
// Contain the event data in a closure.
|
|
// Ignore double-clicks by ignoring clicks within 300ms of
|
|
// each other.
|
|
if(!_clickTimeout) {
|
|
_clickTimeout = window.setTimeout(function() {
|
|
_clickTimeout = null;
|
|
interaction.click(evt, pos);
|
|
}, 150);
|
|
} else {
|
|
killTimeout();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return onUp;
|
|
}
|
|
|
|
interaction.discardTouchMove = function(_) {
|
|
if (!arguments.length) return _discardTouchMove;
|
|
_discardTouchMove = _;
|
|
return interaction;
|
|
}
|
|
|
|
// 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);
|
|
bean.add(parent(), 'MSPointerDown', onDown);
|
|
bean.add(parent(), 'pointerdown', 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;
|
|
};
|
|
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;
|
|
|
|
};
|
|
// 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) {
|
|
|
|
// mustache.js has been removed as a dependency
|
|
throw new Error('mustache.js templates are no longer supported');
|
|
|
|
// 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 || {};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
};
|
|
|
|
//Function that protects 'Unspected error' with Internet Explorer 11
|
|
function calculateOffsetIE(){
|
|
calculateOffset(el);
|
|
try {
|
|
while (el = el.offsetParent) { calculateOffset(el); }
|
|
} catch(e) {
|
|
// Hello, internet explorer.
|
|
}
|
|
}
|
|
|
|
// from jquery, offset.js
|
|
if ( typeof el.getBoundingClientRect !== "undefined" ) {
|
|
var body = document.body;
|
|
var doc = el.ownerDocument.documentElement;
|
|
var clientTop = document.clientTop || body.clientTop || 0;
|
|
var clientLeft = document.clientLeft || body.clientLeft || 0;
|
|
var scrollTop = window.pageYOffset || doc.scrollTop;
|
|
var scrollLeft = window.pageXOffset || doc.scrollLeft;
|
|
|
|
//With Internet Explorer 11, the function getBoundingClientRect() sometimes
|
|
//triggers the error: 'Unspected error.' Protecting it with try/catch
|
|
try {
|
|
var box = el.getBoundingClientRect();
|
|
top = box.top + scrollTop - clientTop;
|
|
left = box.left + scrollLeft - clientLeft;
|
|
} catch(e) {
|
|
calculateOffsetIE();
|
|
}
|
|
} else {
|
|
calculateOffsetIE();
|
|
}
|
|
|
|
// 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.type == "MSPointerMove" || e.type == "MSPointerDown" || e.type == "MSPointerUp") {
|
|
return {
|
|
x: e.pageX + window.pageXOffset,
|
|
y: e.pageY + window.pageYOffset
|
|
}
|
|
}
|
|
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.leaf = wax.leaf || {};
|
|
|
|
wax.leaf.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,
|
|
center.lat.toFixed(precision),
|
|
center.lng.toFixed(precision)
|
|
].join('/');
|
|
},
|
|
|
|
setCenterZoom: function (args) {
|
|
map.setView(new L.LatLng(args[1], args[2]), args[0]);
|
|
},
|
|
|
|
bindChange: function (fn) {
|
|
map.on('moveend', fn);
|
|
},
|
|
|
|
unbindChange: function (fn) {
|
|
map.off('moveend', fn);
|
|
}
|
|
});
|
|
};
|
|
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 _tile = layers[layerId]._tiles[tile];
|
|
// avoid adding tiles without src, grid url can't be found for them
|
|
if(_tile.src) {
|
|
var offset = wax.u.offset(_tile);
|
|
o.push([offset.top, offset.left, _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 || {};
|
|
|
|
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);
|
|
}
|
|
});
|
|
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, interactionEnabled = false;
|
|
var tileloadListener = null,
|
|
idleListener = null;
|
|
|
|
function setdirty() { dirty = true; }
|
|
|
|
function grid() {
|
|
|
|
// when interaction is enabled there should be grid tiles
|
|
if (!dirty && _grid && (_grid.length > 0 || !interactionEnabled)) {
|
|
return _grid;
|
|
} else {
|
|
_grid = [];
|
|
var zoom = map.getZoom();
|
|
var mapOffset = wax.u.offset(map.getDiv());
|
|
var get = function(mapType) {
|
|
if (!mapType || !mapType.interactive) return;
|
|
interactionEnabled = true;
|
|
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)
|
|
.discardTouchMove(true)
|
|
.parent(function() {
|
|
return map.getDiv();
|
|
})
|
|
.grid(grid);
|
|
};
|
|
// 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.opacity = options.opacity || 0;
|
|
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].setAttribute("style","opacity: "+this.opacity+"; filter: alpha(opacity="+(this.opacity*100)+");");
|
|
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\}/g, z)
|
|
.replace(/\{x\}/g, x)
|
|
.replace(/\{y\}/g, y);
|
|
};
|