2766 lines
84 KiB
JavaScript
2766 lines
84 KiB
JavaScript
/* wax - 4.1.3 - 1.0.4-477-gba3b176 */
|
|
|
|
|
|
/*!
|
|
* Reqwest! A general purpose XHR connection manager
|
|
* copyright Dustin Diaz 2011
|
|
* https://github.com/ded/reqwest
|
|
* license MIT
|
|
*/
|
|
!function(context,win){function serial(a){var b=a.name;if(a.disabled||!b)return"";b=enc(b);switch(a.tagName.toLowerCase()){case"input":switch(a.type){case"reset":case"button":case"image":case"file":return"";case"checkbox":case"radio":return a.checked?b+"="+(a.value?enc(a.value):!0)+"&":"";default:return b+"="+(a.value?enc(a.value):"")+"&"}break;case"textarea":return b+"="+enc(a.value)+"&";case"select":return b+"="+enc(a.options[a.selectedIndex].value)+"&"}return""}function enc(a){return encodeURIComponent(a)}function reqwest(a,b){return new Reqwest(a,b)}function init(o,fn){function error(a){o.error&&o.error(a),complete(a)}function success(resp){o.timeout&&clearTimeout(self.timeout)&&(self.timeout=null);var r=resp.responseText;if(r)switch(type){case"json":resp=win.JSON?win.JSON.parse(r):eval("("+r+")");break;case"js":resp=eval(r);break;case"html":resp=r}fn(resp),o.success&&o.success(resp),complete(resp)}function complete(a){o.complete&&o.complete(a)}this.url=typeof o=="string"?o:o.url,this.timeout=null;var type=o.type||setType(this.url),self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){self.abort(),error()},o.timeout)),this.request=getRequest(o,success,error)}function setType(a){if(/\.json$/.test(a))return"json";if(/\.jsonp$/.test(a))return"jsonp";if(/\.js$/.test(a))return"js";if(/\.html?$/.test(a))return"html";if(/\.xml$/.test(a))return"xml";return"js"}function Reqwest(a,b){this.o=a,this.fn=b,init.apply(this,arguments)}function getRequest(a,b,c){if(a.type!="jsonp"){var f=xhr();f.open(a.method||"GET",typeof a=="string"?a:a.url,!0),setHeaders(f,a),f.onreadystatechange=handleReadyState(f,b,c),a.before&&a.before(f),f.send(a.data||null);return f}var d=doc.createElement("script"),e=0;win[getCallbackName(a)]=generalCallback,d.type="text/javascript",d.src=a.url,d.async=!0,d.onload=d.onreadystatechange=function(){if(d[readyState]&&d[readyState]!=="complete"&&d[readyState]!=="loaded"||e)return!1;d.onload=d.onreadystatechange=null,a.success&&a.success(lastValue),lastValue=undefined,head.removeChild(d),e=1},head.appendChild(d)}function generalCallback(a){lastValue=a}function getCallbackName(a){var b=a.jsonpCallback||"callback";if(a.url.slice(-(b.length+2))==b+"=?"){var c="reqwest_"+uniqid++;a.url=a.url.substr(0,a.url.length-1)+c;return c}var d=new RegExp(b+"=([\\w]+)");return a.url.match(d)[1]}function setHeaders(a,b){var c=b.headers||{};c.Accept=c.Accept||"text/javascript, text/html, application/xml, text/xml, */*",b.crossOrigin||(c["X-Requested-With"]=c["X-Requested-With"]||"XMLHttpRequest"),c[contentType]=c[contentType]||"application/x-www-form-urlencoded";for(var d in c)c.hasOwnProperty(d)&&a.setRequestHeader(d,c[d],!1)}function handleReadyState(a,b,c){return function(){a&&a[readyState]==4&&(twoHundo.test(a.status)?b(a):c(a))}}var twoHundo=/^20\d$/,doc=document,byTag="getElementsByTagName",readyState="readyState",contentType="Content-Type",head=doc[byTag]("head")[0],uniqid=0,lastValue,xhr="XMLHttpRequest"in win?function(){return new XMLHttpRequest}:function(){return new ActiveXObject("Microsoft.XMLHTTP")};Reqwest.prototype={abort:function(){this.request.abort()},retry:function(){init.call(this,this.o,this.fn)}},reqwest.serialize=function(a){var b=[a[byTag]("input"),a[byTag]("select"),a[byTag]("textarea")],c=[],d,e;for(d=0,l=b.length;d<l;++d)for(e=0,l2=b[d].length;e<l2;++e)c.push(serial(b[d][e]));return c.join("").replace(/&$/,"")},reqwest.serializeArray=function(a){for(var b=this.serialize(a).split("&"),c=0,d=b.length,e=[],f;c<d;c++)b[c]&&(f=b[c].split("="))&&e.push({name:f[0],value:f[1]});return e};var old=context.reqwest;reqwest.noConflict=function(){context.reqwest=old;return this},typeof module!="undefined"?module.exports=reqwest:context.reqwest=reqwest}(this,window)// Copyright Google Inc.
|
|
// Licensed under the Apache Licence Version 2.0
|
|
// Autogenerated at Tue Oct 11 13:36:46 EDT 2011
|
|
// @provides html4
|
|
var html4 = {};
|
|
html4.atype = {
|
|
NONE: 0,
|
|
URI: 1,
|
|
URI_FRAGMENT: 11,
|
|
SCRIPT: 2,
|
|
STYLE: 3,
|
|
ID: 4,
|
|
IDREF: 5,
|
|
IDREFS: 6,
|
|
GLOBAL_NAME: 7,
|
|
LOCAL_NAME: 8,
|
|
CLASSES: 9,
|
|
FRAME_TARGET: 10
|
|
};
|
|
html4.ATTRIBS = {
|
|
'*::class': 9,
|
|
'*::dir': 0,
|
|
'*::id': 4,
|
|
'*::lang': 0,
|
|
'*::onclick': 2,
|
|
'*::ondblclick': 2,
|
|
'*::onkeydown': 2,
|
|
'*::onkeypress': 2,
|
|
'*::onkeyup': 2,
|
|
'*::onload': 2,
|
|
'*::onmousedown': 2,
|
|
'*::onmousemove': 2,
|
|
'*::onmouseout': 2,
|
|
'*::onmouseover': 2,
|
|
'*::onmouseup': 2,
|
|
'*::style': 3,
|
|
'*::title': 0,
|
|
'a::accesskey': 0,
|
|
'a::coords': 0,
|
|
'a::href': 1,
|
|
'a::hreflang': 0,
|
|
'a::name': 7,
|
|
'a::onblur': 2,
|
|
'a::onfocus': 2,
|
|
'a::rel': 0,
|
|
'a::rev': 0,
|
|
'a::shape': 0,
|
|
'a::tabindex': 0,
|
|
'a::target': 10,
|
|
'a::type': 0,
|
|
'area::accesskey': 0,
|
|
'area::alt': 0,
|
|
'area::coords': 0,
|
|
'area::href': 1,
|
|
'area::nohref': 0,
|
|
'area::onblur': 2,
|
|
'area::onfocus': 2,
|
|
'area::shape': 0,
|
|
'area::tabindex': 0,
|
|
'area::target': 10,
|
|
'bdo::dir': 0,
|
|
'blockquote::cite': 1,
|
|
'br::clear': 0,
|
|
'button::accesskey': 0,
|
|
'button::disabled': 0,
|
|
'button::name': 8,
|
|
'button::onblur': 2,
|
|
'button::onfocus': 2,
|
|
'button::tabindex': 0,
|
|
'button::type': 0,
|
|
'button::value': 0,
|
|
'canvas::height': 0,
|
|
'canvas::width': 0,
|
|
'caption::align': 0,
|
|
'col::align': 0,
|
|
'col::char': 0,
|
|
'col::charoff': 0,
|
|
'col::span': 0,
|
|
'col::valign': 0,
|
|
'col::width': 0,
|
|
'colgroup::align': 0,
|
|
'colgroup::char': 0,
|
|
'colgroup::charoff': 0,
|
|
'colgroup::span': 0,
|
|
'colgroup::valign': 0,
|
|
'colgroup::width': 0,
|
|
'del::cite': 1,
|
|
'del::datetime': 0,
|
|
'dir::compact': 0,
|
|
'div::align': 0,
|
|
'dl::compact': 0,
|
|
'font::color': 0,
|
|
'font::face': 0,
|
|
'font::size': 0,
|
|
'form::accept': 0,
|
|
'form::action': 1,
|
|
'form::autocomplete': 0,
|
|
'form::enctype': 0,
|
|
'form::method': 0,
|
|
'form::name': 7,
|
|
'form::onreset': 2,
|
|
'form::onsubmit': 2,
|
|
'form::target': 10,
|
|
'h1::align': 0,
|
|
'h2::align': 0,
|
|
'h3::align': 0,
|
|
'h4::align': 0,
|
|
'h5::align': 0,
|
|
'h6::align': 0,
|
|
'hr::align': 0,
|
|
'hr::noshade': 0,
|
|
'hr::size': 0,
|
|
'hr::width': 0,
|
|
'iframe::align': 0,
|
|
'iframe::frameborder': 0,
|
|
'iframe::height': 0,
|
|
'iframe::marginheight': 0,
|
|
'iframe::marginwidth': 0,
|
|
'iframe::width': 0,
|
|
'img::align': 0,
|
|
'img::alt': 0,
|
|
'img::border': 0,
|
|
'img::height': 0,
|
|
'img::hspace': 0,
|
|
'img::ismap': 0,
|
|
'img::name': 7,
|
|
'img::src': 1,
|
|
'img::usemap': 11,
|
|
'img::vspace': 0,
|
|
'img::width': 0,
|
|
'input::accept': 0,
|
|
'input::accesskey': 0,
|
|
'input::align': 0,
|
|
'input::alt': 0,
|
|
'input::autocomplete': 0,
|
|
'input::checked': 0,
|
|
'input::disabled': 0,
|
|
'input::ismap': 0,
|
|
'input::maxlength': 0,
|
|
'input::name': 8,
|
|
'input::onblur': 2,
|
|
'input::onchange': 2,
|
|
'input::onfocus': 2,
|
|
'input::onselect': 2,
|
|
'input::readonly': 0,
|
|
'input::size': 0,
|
|
'input::src': 1,
|
|
'input::tabindex': 0,
|
|
'input::type': 0,
|
|
'input::usemap': 11,
|
|
'input::value': 0,
|
|
'ins::cite': 1,
|
|
'ins::datetime': 0,
|
|
'label::accesskey': 0,
|
|
'label::for': 5,
|
|
'label::onblur': 2,
|
|
'label::onfocus': 2,
|
|
'legend::accesskey': 0,
|
|
'legend::align': 0,
|
|
'li::type': 0,
|
|
'li::value': 0,
|
|
'map::name': 7,
|
|
'menu::compact': 0,
|
|
'ol::compact': 0,
|
|
'ol::start': 0,
|
|
'ol::type': 0,
|
|
'optgroup::disabled': 0,
|
|
'optgroup::label': 0,
|
|
'option::disabled': 0,
|
|
'option::label': 0,
|
|
'option::selected': 0,
|
|
'option::value': 0,
|
|
'p::align': 0,
|
|
'pre::width': 0,
|
|
'q::cite': 1,
|
|
'select::disabled': 0,
|
|
'select::multiple': 0,
|
|
'select::name': 8,
|
|
'select::onblur': 2,
|
|
'select::onchange': 2,
|
|
'select::onfocus': 2,
|
|
'select::size': 0,
|
|
'select::tabindex': 0,
|
|
'table::align': 0,
|
|
'table::bgcolor': 0,
|
|
'table::border': 0,
|
|
'table::cellpadding': 0,
|
|
'table::cellspacing': 0,
|
|
'table::frame': 0,
|
|
'table::rules': 0,
|
|
'table::summary': 0,
|
|
'table::width': 0,
|
|
'tbody::align': 0,
|
|
'tbody::char': 0,
|
|
'tbody::charoff': 0,
|
|
'tbody::valign': 0,
|
|
'td::abbr': 0,
|
|
'td::align': 0,
|
|
'td::axis': 0,
|
|
'td::bgcolor': 0,
|
|
'td::char': 0,
|
|
'td::charoff': 0,
|
|
'td::colspan': 0,
|
|
'td::headers': 6,
|
|
'td::height': 0,
|
|
'td::nowrap': 0,
|
|
'td::rowspan': 0,
|
|
'td::scope': 0,
|
|
'td::valign': 0,
|
|
'td::width': 0,
|
|
'textarea::accesskey': 0,
|
|
'textarea::cols': 0,
|
|
'textarea::disabled': 0,
|
|
'textarea::name': 8,
|
|
'textarea::onblur': 2,
|
|
'textarea::onchange': 2,
|
|
'textarea::onfocus': 2,
|
|
'textarea::onselect': 2,
|
|
'textarea::readonly': 0,
|
|
'textarea::rows': 0,
|
|
'textarea::tabindex': 0,
|
|
'tfoot::align': 0,
|
|
'tfoot::char': 0,
|
|
'tfoot::charoff': 0,
|
|
'tfoot::valign': 0,
|
|
'th::abbr': 0,
|
|
'th::align': 0,
|
|
'th::axis': 0,
|
|
'th::bgcolor': 0,
|
|
'th::char': 0,
|
|
'th::charoff': 0,
|
|
'th::colspan': 0,
|
|
'th::headers': 6,
|
|
'th::height': 0,
|
|
'th::nowrap': 0,
|
|
'th::rowspan': 0,
|
|
'th::scope': 0,
|
|
'th::valign': 0,
|
|
'th::width': 0,
|
|
'thead::align': 0,
|
|
'thead::char': 0,
|
|
'thead::charoff': 0,
|
|
'thead::valign': 0,
|
|
'tr::align': 0,
|
|
'tr::bgcolor': 0,
|
|
'tr::char': 0,
|
|
'tr::charoff': 0,
|
|
'tr::valign': 0,
|
|
'ul::compact': 0,
|
|
'ul::type': 0
|
|
};
|
|
html4.eflags = {
|
|
OPTIONAL_ENDTAG: 1,
|
|
EMPTY: 2,
|
|
CDATA: 4,
|
|
RCDATA: 8,
|
|
UNSAFE: 16,
|
|
FOLDABLE: 32,
|
|
SCRIPT: 64,
|
|
STYLE: 128
|
|
};
|
|
html4.ELEMENTS = {
|
|
'a': 0,
|
|
'abbr': 0,
|
|
'acronym': 0,
|
|
'address': 0,
|
|
'applet': 16,
|
|
'area': 2,
|
|
'b': 0,
|
|
'base': 18,
|
|
'basefont': 18,
|
|
'bdo': 0,
|
|
'big': 0,
|
|
'blockquote': 0,
|
|
'body': 49,
|
|
'br': 2,
|
|
'button': 0,
|
|
'canvas': 0,
|
|
'caption': 0,
|
|
'center': 0,
|
|
'cite': 0,
|
|
'code': 0,
|
|
'col': 2,
|
|
'colgroup': 1,
|
|
'dd': 1,
|
|
'del': 0,
|
|
'dfn': 0,
|
|
'dir': 0,
|
|
'div': 0,
|
|
'dl': 0,
|
|
'dt': 1,
|
|
'em': 0,
|
|
'fieldset': 0,
|
|
'font': 0,
|
|
'form': 0,
|
|
'frame': 18,
|
|
'frameset': 16,
|
|
'h1': 0,
|
|
'h2': 0,
|
|
'h3': 0,
|
|
'h4': 0,
|
|
'h5': 0,
|
|
'h6': 0,
|
|
'head': 49,
|
|
'hr': 2,
|
|
'html': 49,
|
|
'i': 0,
|
|
'iframe': 4,
|
|
'img': 2,
|
|
'input': 2,
|
|
'ins': 0,
|
|
'isindex': 18,
|
|
'kbd': 0,
|
|
'label': 0,
|
|
'legend': 0,
|
|
'li': 1,
|
|
'link': 18,
|
|
'map': 0,
|
|
'menu': 0,
|
|
'meta': 18,
|
|
'nobr': 0,
|
|
'noembed': 4,
|
|
'noframes': 20,
|
|
'noscript': 20,
|
|
'object': 16,
|
|
'ol': 0,
|
|
'optgroup': 0,
|
|
'option': 1,
|
|
'p': 1,
|
|
'param': 18,
|
|
'pre': 0,
|
|
'q': 0,
|
|
's': 0,
|
|
'samp': 0,
|
|
'script': 84,
|
|
'select': 0,
|
|
'small': 0,
|
|
'span': 0,
|
|
'strike': 0,
|
|
'strong': 0,
|
|
'style': 148,
|
|
'sub': 0,
|
|
'sup': 0,
|
|
'table': 0,
|
|
'tbody': 1,
|
|
'td': 1,
|
|
'textarea': 8,
|
|
'tfoot': 1,
|
|
'th': 1,
|
|
'thead': 1,
|
|
'title': 24,
|
|
'tr': 1,
|
|
'tt': 0,
|
|
'u': 0,
|
|
'ul': 0,
|
|
'var': 0
|
|
};
|
|
html4.ueffects = {
|
|
NOT_LOADED: 0,
|
|
SAME_DOCUMENT: 1,
|
|
NEW_DOCUMENT: 2
|
|
};
|
|
html4.URIEFFECTS = {
|
|
'a::href': 2,
|
|
'area::href': 2,
|
|
'blockquote::cite': 0,
|
|
'body::background': 1,
|
|
'del::cite': 0,
|
|
'form::action': 2,
|
|
'img::src': 1,
|
|
'input::src': 1,
|
|
'ins::cite': 0,
|
|
'q::cite': 0
|
|
};
|
|
html4.ltypes = {
|
|
UNSANDBOXED: 2,
|
|
SANDBOXED: 1,
|
|
DATA: 0
|
|
};
|
|
html4.LOADERTYPES = {
|
|
'a::href': 2,
|
|
'area::href': 2,
|
|
'blockquote::cite': 2,
|
|
'body::background': 1,
|
|
'del::cite': 2,
|
|
'form::action': 2,
|
|
'img::src': 1,
|
|
'input::src': 1,
|
|
'ins::cite': 2,
|
|
'q::cite': 2
|
|
};;
|
|
// Copyright (C) 2006 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview
|
|
* An HTML sanitizer that can satisfy a variety of security policies.
|
|
*
|
|
* <p>
|
|
* The HTML sanitizer is built around a SAX parser and HTML element and
|
|
* attributes schemas.
|
|
*
|
|
* @author mikesamuel@gmail.com
|
|
* @requires html4
|
|
* @overrides window
|
|
* @provides html, html_sanitize
|
|
*/
|
|
|
|
/**
|
|
* @namespace
|
|
*/
|
|
var html = (function (html4) {
|
|
var lcase;
|
|
// The below may not be true on browsers in the Turkish locale.
|
|
if ('script' === 'SCRIPT'.toLowerCase()) {
|
|
lcase = function (s) { return s.toLowerCase(); };
|
|
} else {
|
|
/**
|
|
* {@updoc
|
|
* $ lcase('SCRIPT')
|
|
* # 'script'
|
|
* $ lcase('script')
|
|
* # 'script'
|
|
* }
|
|
*/
|
|
lcase = function (s) {
|
|
return s.replace(
|
|
/[A-Z]/g,
|
|
function (ch) {
|
|
return String.fromCharCode(ch.charCodeAt(0) | 32);
|
|
});
|
|
};
|
|
}
|
|
|
|
var ENTITIES = {
|
|
lt : '<',
|
|
gt : '>',
|
|
amp : '&',
|
|
nbsp : '\240',
|
|
quot : '"',
|
|
apos : '\''
|
|
};
|
|
|
|
// Schemes on which to defer to uripolicy. Urls with other schemes are denied
|
|
var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i;
|
|
|
|
var decimalEscapeRe = /^#(\d+)$/;
|
|
var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/;
|
|
/**
|
|
* Decodes an HTML entity.
|
|
*
|
|
* {@updoc
|
|
* $ lookupEntity('lt')
|
|
* # '<'
|
|
* $ lookupEntity('GT')
|
|
* # '>'
|
|
* $ lookupEntity('amp')
|
|
* # '&'
|
|
* $ lookupEntity('nbsp')
|
|
* # '\xA0'
|
|
* $ lookupEntity('apos')
|
|
* # "'"
|
|
* $ lookupEntity('quot')
|
|
* # '"'
|
|
* $ lookupEntity('#xa')
|
|
* # '\n'
|
|
* $ lookupEntity('#10')
|
|
* # '\n'
|
|
* $ lookupEntity('#x0a')
|
|
* # '\n'
|
|
* $ lookupEntity('#010')
|
|
* # '\n'
|
|
* $ lookupEntity('#x00A')
|
|
* # '\n'
|
|
* $ lookupEntity('Pi') // Known failure
|
|
* # '\u03A0'
|
|
* $ lookupEntity('pi') // Known failure
|
|
* # '\u03C0'
|
|
* }
|
|
*
|
|
* @param name the content between the '&' and the ';'.
|
|
* @return a single unicode code-point as a string.
|
|
*/
|
|
function lookupEntity(name) {
|
|
name = lcase(name); // TODO: π is different from Π
|
|
if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; }
|
|
var m = name.match(decimalEscapeRe);
|
|
if (m) {
|
|
return String.fromCharCode(parseInt(m[1], 10));
|
|
} else if (!!(m = name.match(hexEscapeRe))) {
|
|
return String.fromCharCode(parseInt(m[1], 16));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function decodeOneEntity(_, name) {
|
|
return lookupEntity(name);
|
|
}
|
|
|
|
var nulRe = /\0/g;
|
|
function stripNULs(s) {
|
|
return s.replace(nulRe, '');
|
|
}
|
|
|
|
var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;
|
|
/**
|
|
* The plain text of a chunk of HTML CDATA which possibly containing.
|
|
*
|
|
* {@updoc
|
|
* $ unescapeEntities('')
|
|
* # ''
|
|
* $ unescapeEntities('hello World!')
|
|
* # 'hello World!'
|
|
* $ unescapeEntities('1 < 2 && 4 > 3 ')
|
|
* # '1 < 2 && 4 > 3\n'
|
|
* $ unescapeEntities('<< <- unfinished entity>')
|
|
* # '<< <- unfinished entity>'
|
|
* $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS
|
|
* # '/foo?bar=baz©=true'
|
|
* $ unescapeEntities('pi=ππ, Pi=Π\u03A0') // FIXME: known failure
|
|
* # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0'
|
|
* }
|
|
*
|
|
* @param s a chunk of HTML CDATA. It must not start or end inside an HTML
|
|
* entity.
|
|
*/
|
|
function unescapeEntities(s) {
|
|
return s.replace(entityRe, decodeOneEntity);
|
|
}
|
|
|
|
var ampRe = /&/g;
|
|
var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi;
|
|
var ltRe = /</g;
|
|
var gtRe = />/g;
|
|
var quotRe = /\"/g;
|
|
var eqRe = /\=/g; // Backslash required on JScript.net
|
|
|
|
/**
|
|
* Escapes HTML special characters in attribute values as HTML entities.
|
|
*
|
|
* {@updoc
|
|
* $ escapeAttrib('')
|
|
* # ''
|
|
* $ escapeAttrib('"<<&==&>>"') // Do not just escape the first occurrence.
|
|
* # '"<<&==&>>"'
|
|
* $ escapeAttrib('Hello <World>!')
|
|
* # 'Hello <World>!'
|
|
* }
|
|
*/
|
|
function escapeAttrib(s) {
|
|
// Escaping '=' defangs many UTF-7 and SGML short-tag attacks.
|
|
return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>')
|
|
.replace(quotRe, '"').replace(eqRe, '=');
|
|
}
|
|
|
|
/**
|
|
* Escape entities in RCDATA that can be escaped without changing the meaning.
|
|
* {@updoc
|
|
* $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8')
|
|
* # '1 < 2 && 3 > 4 && 5 < 7&8'
|
|
* }
|
|
*/
|
|
function normalizeRCData(rcdata) {
|
|
return rcdata
|
|
.replace(looseAmpRe, '&$1')
|
|
.replace(ltRe, '<')
|
|
.replace(gtRe, '>');
|
|
}
|
|
|
|
|
|
// TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html
|
|
|
|
/** token definitions. */
|
|
var INSIDE_TAG_TOKEN = new RegExp(
|
|
// Don't capture space.
|
|
'^\\s*(?:'
|
|
// Capture an attribute name in group 1, and value in group 3.
|
|
// We capture the fact that there was an attribute in group 2, since
|
|
// interpreters are inconsistent in whether a group that matches nothing
|
|
// is null, undefined, or the empty string.
|
|
+ ('(?:'
|
|
+ '([a-z][a-z-]*)' // attribute name
|
|
+ ('(' // optionally followed
|
|
+ '\\s*=\\s*'
|
|
+ ('('
|
|
// A double quoted string.
|
|
+ '\"[^\"]*\"'
|
|
// A single quoted string.
|
|
+ '|\'[^\']*\''
|
|
// The positive lookahead is used to make sure that in
|
|
// <foo bar= baz=boo>, the value for bar is blank, not "baz=boo".
|
|
+ '|(?=[a-z][a-z-]*\\s*=)'
|
|
// An unquoted value that is not an attribute name.
|
|
// We know it is not an attribute name because the previous
|
|
// zero-width match would've eliminated that possibility.
|
|
+ '|[^>\"\'\\s]*'
|
|
+ ')'
|
|
)
|
|
+ ')'
|
|
) + '?'
|
|
+ ')'
|
|
)
|
|
// End of tag captured in group 3.
|
|
+ '|(\/?>)'
|
|
// Don't capture cruft
|
|
+ '|[\\s\\S][^a-z\\s>]*)',
|
|
'i');
|
|
|
|
var OUTSIDE_TAG_TOKEN = new RegExp(
|
|
'^(?:'
|
|
// Entity captured in group 1.
|
|
+ '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);'
|
|
// Comment, doctypes, and processing instructions not captured.
|
|
+ '|<\!--[\\s\\S]*?--\>|<!\\w[^>]*>|<\\?[^>*]*>'
|
|
// '/' captured in group 2 for close tags, and name captured in group 3.
|
|
+ '|<(\/)?([a-z][a-z0-9]*)'
|
|
// Text captured in group 4.
|
|
+ '|([^<&>]+)'
|
|
// Cruft captured in group 5.
|
|
+ '|([<&>]))',
|
|
'i');
|
|
|
|
/**
|
|
* Given a SAX-like event handler, produce a function that feeds those
|
|
* events and a parameter to the event handler.
|
|
*
|
|
* The event handler has the form:{@code
|
|
* {
|
|
* // Name is an upper-case HTML tag name. Attribs is an array of
|
|
* // alternating upper-case attribute names, and attribute values. The
|
|
* // attribs array is reused by the parser. Param is the value passed to
|
|
* // the saxParser.
|
|
* startTag: function (name, attribs, param) { ... },
|
|
* endTag: function (name, param) { ... },
|
|
* pcdata: function (text, param) { ... },
|
|
* rcdata: function (text, param) { ... },
|
|
* cdata: function (text, param) { ... },
|
|
* startDoc: function (param) { ... },
|
|
* endDoc: function (param) { ... }
|
|
* }}
|
|
*
|
|
* @param {Object} handler a record containing event handlers.
|
|
* @return {Function} that takes a chunk of html and a parameter.
|
|
* The parameter is passed on to the handler methods.
|
|
*/
|
|
function makeSaxParser(handler) {
|
|
return function parse(htmlText, param) {
|
|
htmlText = String(htmlText);
|
|
var htmlLower = null;
|
|
|
|
var inTag = false; // True iff we're currently processing a tag.
|
|
var attribs = []; // Accumulates attribute names and values.
|
|
var tagName = void 0; // The name of the tag currently being processed.
|
|
var eflags = void 0; // The element flags for the current tag.
|
|
var openTag = void 0; // True if the current tag is an open tag.
|
|
|
|
if (handler.startDoc) { handler.startDoc(param); }
|
|
|
|
while (htmlText) {
|
|
var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN);
|
|
htmlText = htmlText.substring(m[0].length);
|
|
|
|
if (inTag) {
|
|
if (m[1]) { // attribute
|
|
// setAttribute with uppercase names doesn't work on IE6.
|
|
var attribName = lcase(m[1]);
|
|
var decodedValue;
|
|
if (m[2]) {
|
|
var encodedValue = m[3];
|
|
switch (encodedValue.charCodeAt(0)) { // Strip quotes
|
|
case 34: case 39:
|
|
encodedValue = encodedValue.substring(
|
|
1, encodedValue.length - 1);
|
|
break;
|
|
}
|
|
decodedValue = unescapeEntities(stripNULs(encodedValue));
|
|
} else {
|
|
// Use name as value for valueless attribs, so
|
|
// <input type=checkbox checked>
|
|
// gets attributes ['type', 'checkbox', 'checked', 'checked']
|
|
decodedValue = attribName;
|
|
}
|
|
attribs.push(attribName, decodedValue);
|
|
} else if (m[4]) {
|
|
if (eflags !== void 0) { // False if not in whitelist.
|
|
if (openTag) {
|
|
if (handler.startTag) {
|
|
handler.startTag(tagName, attribs, param);
|
|
}
|
|
} else {
|
|
if (handler.endTag) {
|
|
handler.endTag(tagName, param);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (openTag
|
|
&& (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) {
|
|
if (htmlLower === null) {
|
|
htmlLower = lcase(htmlText);
|
|
} else {
|
|
htmlLower = htmlLower.substring(
|
|
htmlLower.length - htmlText.length);
|
|
}
|
|
var dataEnd = htmlLower.indexOf('</' + tagName);
|
|
if (dataEnd < 0) { dataEnd = htmlText.length; }
|
|
if (dataEnd) {
|
|
if (eflags & html4.eflags.CDATA) {
|
|
if (handler.cdata) {
|
|
handler.cdata(htmlText.substring(0, dataEnd), param);
|
|
}
|
|
} else if (handler.rcdata) {
|
|
handler.rcdata(
|
|
normalizeRCData(htmlText.substring(0, dataEnd)), param);
|
|
}
|
|
htmlText = htmlText.substring(dataEnd);
|
|
}
|
|
}
|
|
|
|
tagName = eflags = openTag = void 0;
|
|
attribs.length = 0;
|
|
inTag = false;
|
|
}
|
|
} else {
|
|
if (m[1]) { // Entity
|
|
if (handler.pcdata) { handler.pcdata(m[0], param); }
|
|
} else if (m[3]) { // Tag
|
|
openTag = !m[2];
|
|
inTag = true;
|
|
tagName = lcase(m[3]);
|
|
eflags = html4.ELEMENTS.hasOwnProperty(tagName)
|
|
? html4.ELEMENTS[tagName] : void 0;
|
|
} else if (m[4]) { // Text
|
|
if (handler.pcdata) { handler.pcdata(m[4], param); }
|
|
} else if (m[5]) { // Cruft
|
|
if (handler.pcdata) {
|
|
var ch = m[5];
|
|
handler.pcdata(
|
|
ch === '<' ? '<' : ch === '>' ? '>' : '&',
|
|
param);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (handler.endDoc) { handler.endDoc(param); }
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a function that strips unsafe tags and attributes from html.
|
|
* @param {Function} sanitizeAttributes
|
|
* maps from (tagName, attribs[]) to null or a sanitized attribute array.
|
|
* The attribs array can be arbitrarily modified, but the same array
|
|
* instance is reused, so should not be held.
|
|
* @return {Function} from html to sanitized html
|
|
*/
|
|
function makeHtmlSanitizer(sanitizeAttributes) {
|
|
var stack;
|
|
var ignoring;
|
|
return makeSaxParser({
|
|
startDoc: function (_) {
|
|
stack = [];
|
|
ignoring = false;
|
|
},
|
|
startTag: function (tagName, attribs, out) {
|
|
if (ignoring) { return; }
|
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
|
|
var eflags = html4.ELEMENTS[tagName];
|
|
if (eflags & html4.eflags.FOLDABLE) {
|
|
return;
|
|
} else if (eflags & html4.eflags.UNSAFE) {
|
|
ignoring = !(eflags & html4.eflags.EMPTY);
|
|
return;
|
|
}
|
|
attribs = sanitizeAttributes(tagName, attribs);
|
|
// TODO(mikesamuel): relying on sanitizeAttributes not to
|
|
// insert unsafe attribute names.
|
|
if (attribs) {
|
|
if (!(eflags & html4.eflags.EMPTY)) {
|
|
stack.push(tagName);
|
|
}
|
|
|
|
out.push('<', tagName);
|
|
for (var i = 0, n = attribs.length; i < n; i += 2) {
|
|
var attribName = attribs[i],
|
|
value = attribs[i + 1];
|
|
if (value !== null && value !== void 0) {
|
|
out.push(' ', attribName, '="', escapeAttrib(value), '"');
|
|
}
|
|
}
|
|
out.push('>');
|
|
}
|
|
},
|
|
endTag: function (tagName, out) {
|
|
if (ignoring) {
|
|
ignoring = false;
|
|
return;
|
|
}
|
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
|
|
var eflags = html4.ELEMENTS[tagName];
|
|
if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY
|
|
| html4.eflags.FOLDABLE))) {
|
|
var index;
|
|
if (eflags & html4.eflags.OPTIONAL_ENDTAG) {
|
|
for (index = stack.length; --index >= 0;) {
|
|
var stackEl = stack[index];
|
|
if (stackEl === tagName) { break; }
|
|
if (!(html4.ELEMENTS[stackEl]
|
|
& html4.eflags.OPTIONAL_ENDTAG)) {
|
|
// Don't pop non optional end tags looking for a match.
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
for (index = stack.length; --index >= 0;) {
|
|
if (stack[index] === tagName) { break; }
|
|
}
|
|
}
|
|
if (index < 0) { return; } // Not opened.
|
|
for (var i = stack.length; --i > index;) {
|
|
var stackEl = stack[i];
|
|
if (!(html4.ELEMENTS[stackEl]
|
|
& html4.eflags.OPTIONAL_ENDTAG)) {
|
|
out.push('</', stackEl, '>');
|
|
}
|
|
}
|
|
stack.length = index;
|
|
out.push('</', tagName, '>');
|
|
}
|
|
},
|
|
pcdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
rcdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
cdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
endDoc: function (out) {
|
|
for (var i = stack.length; --i >= 0;) {
|
|
out.push('</', stack[i], '>');
|
|
}
|
|
stack.length = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
// From RFC3986
|
|
var URI_SCHEME_RE = new RegExp(
|
|
"^" +
|
|
"(?:" +
|
|
"([^:\/?#]+)" + // scheme
|
|
":)?"
|
|
);
|
|
|
|
/**
|
|
* Strips unsafe tags and attributes from html.
|
|
* @param {string} htmlText to sanitize
|
|
* @param {Function} opt_uriPolicy -- a transform to apply to uri/url
|
|
* attribute values. If no opt_uriPolicy is provided, no uris
|
|
* are allowed ie. the default uriPolicy rewrites all uris to null
|
|
* @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to
|
|
* apply to names, ids, and classes. If no opt_nmTokenPolicy is provided,
|
|
* all names, ids and classes are passed through ie. the default
|
|
* nmTokenPolicy is an identity transform
|
|
* @return {string} html
|
|
*/
|
|
function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) {
|
|
var out = [];
|
|
makeHtmlSanitizer(
|
|
function sanitizeAttribs(tagName, attribs) {
|
|
for (var i = 0; i < attribs.length; i += 2) {
|
|
var attribName = attribs[i];
|
|
var value = attribs[i + 1];
|
|
var atype = null, attribKey;
|
|
if ((attribKey = tagName + '::' + attribName,
|
|
html4.ATTRIBS.hasOwnProperty(attribKey))
|
|
|| (attribKey = '*::' + attribName,
|
|
html4.ATTRIBS.hasOwnProperty(attribKey))) {
|
|
atype = html4.ATTRIBS[attribKey];
|
|
}
|
|
if (atype !== null) {
|
|
switch (atype) {
|
|
case html4.atype.NONE: break;
|
|
case html4.atype.SCRIPT:
|
|
case html4.atype.STYLE:
|
|
value = null;
|
|
break;
|
|
case html4.atype.ID:
|
|
case html4.atype.IDREF:
|
|
case html4.atype.IDREFS:
|
|
case html4.atype.GLOBAL_NAME:
|
|
case html4.atype.LOCAL_NAME:
|
|
case html4.atype.CLASSES:
|
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
|
|
break;
|
|
case html4.atype.URI:
|
|
var parsedUri = ('' + value).match(URI_SCHEME_RE);
|
|
if (!parsedUri) {
|
|
value = null;
|
|
} else if (!parsedUri[1] ||
|
|
WHITELISTED_SCHEMES.test(parsedUri[1])) {
|
|
value = opt_uriPolicy && opt_uriPolicy(value);
|
|
} else {
|
|
value = null;
|
|
}
|
|
break;
|
|
case html4.atype.URI_FRAGMENT:
|
|
if (value && '#' === value.charAt(0)) {
|
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
|
|
if (value) { value = '#' + value; }
|
|
} else {
|
|
value = null;
|
|
}
|
|
break;
|
|
default:
|
|
value = null;
|
|
break;
|
|
}
|
|
} else {
|
|
value = null;
|
|
}
|
|
attribs[i + 1] = value;
|
|
}
|
|
return attribs;
|
|
})(htmlText, out);
|
|
return out.join('');
|
|
}
|
|
|
|
return {
|
|
escapeAttrib: escapeAttrib,
|
|
makeHtmlSanitizer: makeHtmlSanitizer,
|
|
makeSaxParser: makeSaxParser,
|
|
normalizeRCData: normalizeRCData,
|
|
sanitize: sanitize,
|
|
unescapeEntities: unescapeEntities
|
|
};
|
|
})(html4);
|
|
|
|
var html_sanitize = html.sanitize;
|
|
|
|
// Exports for closure compiler. Note this file is also cajoled
|
|
// for domado and run in an environment without 'window'
|
|
if (typeof window !== 'undefined') {
|
|
window['html'] = html;
|
|
window['html_sanitize'] = html_sanitize;
|
|
}
|
|
// Loosen restrictions of Caja's
|
|
// html-sanitizer to allow for styling
|
|
html4.ATTRIBS['*::style'] = 0;
|
|
html4.ELEMENTS['style'] = 0;
|
|
/*
|
|
mustache.js — Logic-less templates in JavaScript
|
|
|
|
See http://mustache.github.com/ for more info.
|
|
*/
|
|
|
|
var Mustache = function() {
|
|
var regexCache = {};
|
|
var Renderer = function() {};
|
|
|
|
Renderer.prototype = {
|
|
otag: "{{",
|
|
ctag: "}}",
|
|
pragmas: {},
|
|
buffer: [],
|
|
pragmas_implemented: {
|
|
"IMPLICIT-ITERATOR": true
|
|
},
|
|
context: {},
|
|
|
|
render: function(template, context, partials, in_recursion) {
|
|
// reset buffer & set context
|
|
if(!in_recursion) {
|
|
this.context = context;
|
|
this.buffer = []; // TODO: make this non-lazy
|
|
}
|
|
|
|
// fail fast
|
|
if(!this.includes("", template)) {
|
|
if(in_recursion) {
|
|
return template;
|
|
} else {
|
|
this.send(template);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// get the pragmas together
|
|
template = this.render_pragmas(template);
|
|
|
|
// render the template
|
|
var html = this.render_section(template, context, partials);
|
|
|
|
// render_section did not find any sections, we still need to render the tags
|
|
if (html === false) {
|
|
html = this.render_tags(template, context, partials, in_recursion);
|
|
}
|
|
|
|
if (in_recursion) {
|
|
return html;
|
|
} else {
|
|
this.sendLines(html);
|
|
}
|
|
},
|
|
|
|
/*
|
|
Sends parsed lines
|
|
*/
|
|
send: function(line) {
|
|
if(line !== "") {
|
|
this.buffer.push(line);
|
|
}
|
|
},
|
|
|
|
sendLines: function(text) {
|
|
if (text) {
|
|
var lines = text.split("\n");
|
|
for (var i = 0; i < lines.length; i++) {
|
|
this.send(lines[i]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
Looks for %PRAGMAS
|
|
*/
|
|
render_pragmas: function(template) {
|
|
// no pragmas
|
|
if(!this.includes("%", template)) {
|
|
return template;
|
|
}
|
|
|
|
var that = this;
|
|
var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) {
|
|
return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g");
|
|
});
|
|
|
|
return template.replace(regex, function(match, pragma, options) {
|
|
if(!that.pragmas_implemented[pragma]) {
|
|
throw({message:
|
|
"This implementation of mustache doesn't understand the '" +
|
|
pragma + "' pragma"});
|
|
}
|
|
that.pragmas[pragma] = {};
|
|
if(options) {
|
|
var opts = options.split("=");
|
|
that.pragmas[pragma][opts[0]] = opts[1];
|
|
}
|
|
return "";
|
|
// ignore unknown pragmas silently
|
|
});
|
|
},
|
|
|
|
/*
|
|
Tries to find a partial in the curent scope and render it
|
|
*/
|
|
render_partial: function(name, context, partials) {
|
|
name = this.trim(name);
|
|
if(!partials || partials[name] === undefined) {
|
|
throw({message: "unknown_partial '" + name + "'"});
|
|
}
|
|
if(typeof(context[name]) != "object") {
|
|
return this.render(partials[name], context, partials, true);
|
|
}
|
|
return this.render(partials[name], context[name], partials, true);
|
|
},
|
|
|
|
/*
|
|
Renders inverted (^) and normal (#) sections
|
|
*/
|
|
render_section: function(template, context, partials) {
|
|
if(!this.includes("#", template) && !this.includes("^", template)) {
|
|
// did not render anything, there were no sections
|
|
return false;
|
|
}
|
|
|
|
var that = this;
|
|
|
|
var regex = this.getCachedRegex("render_section", function(otag, ctag) {
|
|
// This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
|
|
return new RegExp(
|
|
"^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1)
|
|
|
|
otag + // {{
|
|
"(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3)
|
|
ctag + // }}
|
|
|
|
"\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped
|
|
|
|
otag + // {{
|
|
"\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag).
|
|
ctag + // }}
|
|
|
|
"\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped.
|
|
|
|
"g");
|
|
});
|
|
|
|
|
|
// for each {{#foo}}{{/foo}} section do...
|
|
return template.replace(regex, function(match, before, type, name, content, after) {
|
|
// before contains only tags, no sections
|
|
var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
|
|
|
|
// after may contain both sections and tags, so use full rendering function
|
|
renderedAfter = after ? that.render(after, context, partials, true) : "",
|
|
|
|
// will be computed below
|
|
renderedContent,
|
|
|
|
value = that.find(name, context);
|
|
|
|
if (type === "^") { // inverted section
|
|
if (!value || that.is_array(value) && value.length === 0) {
|
|
// false or empty list, render it
|
|
renderedContent = that.render(content, context, partials, true);
|
|
} else {
|
|
renderedContent = "";
|
|
}
|
|
} else if (type === "#") { // normal section
|
|
if (that.is_array(value)) { // Enumerable, Let's loop!
|
|
renderedContent = that.map(value, function(row) {
|
|
return that.render(content, that.create_context(row), partials, true);
|
|
}).join("");
|
|
} else if (that.is_object(value)) { // Object, Use it as subcontext!
|
|
renderedContent = that.render(content, that.create_context(value),
|
|
partials, true);
|
|
} else if (typeof value === "function") {
|
|
// higher order section
|
|
renderedContent = value.call(context, content, function(text) {
|
|
return that.render(text, context, partials, true);
|
|
});
|
|
} else if (value) { // boolean section
|
|
renderedContent = that.render(content, context, partials, true);
|
|
} else {
|
|
renderedContent = "";
|
|
}
|
|
}
|
|
|
|
return renderedBefore + renderedContent + renderedAfter;
|
|
});
|
|
},
|
|
|
|
/*
|
|
Replace {{foo}} and friends with values from our view
|
|
*/
|
|
render_tags: function(template, context, partials, in_recursion) {
|
|
// tit for tat
|
|
var that = this;
|
|
|
|
|
|
|
|
var new_regex = function() {
|
|
return that.getCachedRegex("render_tags", function(otag, ctag) {
|
|
return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g");
|
|
});
|
|
};
|
|
|
|
var regex = new_regex();
|
|
var tag_replace_callback = function(match, operator, name) {
|
|
switch(operator) {
|
|
case "!": // ignore comments
|
|
return "";
|
|
case "=": // set new delimiters, rebuild the replace regexp
|
|
that.set_delimiters(name);
|
|
regex = new_regex();
|
|
return "";
|
|
case ">": // render partial
|
|
return that.render_partial(name, context, partials);
|
|
case "{": // the triple mustache is unescaped
|
|
return that.find(name, context);
|
|
default: // escape the value
|
|
return that.escape(that.find(name, context));
|
|
}
|
|
};
|
|
var lines = template.split("\n");
|
|
for(var i = 0; i < lines.length; i++) {
|
|
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
|
|
if(!in_recursion) {
|
|
this.send(lines[i]);
|
|
}
|
|
}
|
|
|
|
if(in_recursion) {
|
|
return lines.join("\n");
|
|
}
|
|
},
|
|
|
|
set_delimiters: function(delimiters) {
|
|
var dels = delimiters.split(" ");
|
|
this.otag = this.escape_regex(dels[0]);
|
|
this.ctag = this.escape_regex(dels[1]);
|
|
},
|
|
|
|
escape_regex: function(text) {
|
|
// thank you Simon Willison
|
|
if(!arguments.callee.sRE) {
|
|
var specials = [
|
|
'/', '.', '*', '+', '?', '|',
|
|
'(', ')', '[', ']', '{', '}', '\\'
|
|
];
|
|
arguments.callee.sRE = new RegExp(
|
|
'(\\' + specials.join('|\\') + ')', 'g'
|
|
);
|
|
}
|
|
return text.replace(arguments.callee.sRE, '\\$1');
|
|
},
|
|
|
|
/*
|
|
find `name` in current `context`. That is find me a value
|
|
from the view object
|
|
*/
|
|
find: function(name, context) {
|
|
name = this.trim(name);
|
|
|
|
// Checks whether a value is thruthy or false or 0
|
|
function is_kinda_truthy(bool) {
|
|
return bool === false || bool === 0 || bool;
|
|
}
|
|
|
|
var value;
|
|
if(is_kinda_truthy(context[name])) {
|
|
value = context[name];
|
|
} else if(is_kinda_truthy(this.context[name])) {
|
|
value = this.context[name];
|
|
}
|
|
|
|
if(typeof value === "function") {
|
|
return value.apply(context);
|
|
}
|
|
if(value !== undefined) {
|
|
return value;
|
|
}
|
|
// silently ignore unkown variables
|
|
return "";
|
|
},
|
|
|
|
// Utility methods
|
|
|
|
/* includes tag */
|
|
includes: function(needle, haystack) {
|
|
return haystack.indexOf(this.otag + needle) != -1;
|
|
},
|
|
|
|
/*
|
|
Does away with nasty characters
|
|
*/
|
|
escape: function(s) {
|
|
s = String(s === null ? "" : s);
|
|
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
|
|
switch(s) {
|
|
case "&": return "&";
|
|
case '"': return '"';
|
|
case "'": return ''';
|
|
case "<": return "<";
|
|
case ">": return ">";
|
|
default: return s;
|
|
}
|
|
});
|
|
},
|
|
|
|
// by @langalex, support for arrays of strings
|
|
create_context: function(_context) {
|
|
if(this.is_object(_context)) {
|
|
return _context;
|
|
} else {
|
|
var iterator = ".";
|
|
if(this.pragmas["IMPLICIT-ITERATOR"]) {
|
|
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
|
|
}
|
|
var ctx = {};
|
|
ctx[iterator] = _context;
|
|
return ctx;
|
|
}
|
|
},
|
|
|
|
is_object: function(a) {
|
|
return a && typeof a == "object";
|
|
},
|
|
|
|
is_array: function(a) {
|
|
return Object.prototype.toString.call(a) === '[object Array]';
|
|
},
|
|
|
|
/*
|
|
Gets rid of leading and trailing whitespace
|
|
*/
|
|
trim: function(s) {
|
|
return s.replace(/^\s*|\s*$/g, "");
|
|
},
|
|
|
|
/*
|
|
Why, why, why? Because IE. Cry, cry cry.
|
|
*/
|
|
map: function(array, fn) {
|
|
if (typeof array.map == "function") {
|
|
return array.map(fn);
|
|
} else {
|
|
var r = [];
|
|
var l = array.length;
|
|
for(var i = 0; i < l; i++) {
|
|
r.push(fn(array[i]));
|
|
}
|
|
return r;
|
|
}
|
|
},
|
|
|
|
getCachedRegex: function(name, generator) {
|
|
var byOtag = regexCache[this.otag];
|
|
if (!byOtag) {
|
|
byOtag = regexCache[this.otag] = {};
|
|
}
|
|
|
|
var byCtag = byOtag[this.ctag];
|
|
if (!byCtag) {
|
|
byCtag = byOtag[this.ctag] = {};
|
|
}
|
|
|
|
var regex = byCtag[name];
|
|
if (!regex) {
|
|
regex = byCtag[name] = generator(this.otag, this.ctag);
|
|
}
|
|
|
|
return regex;
|
|
}
|
|
};
|
|
|
|
return({
|
|
name: "mustache.js",
|
|
version: "0.4.0-dev",
|
|
|
|
/*
|
|
Turns a template and view into HTML
|
|
*/
|
|
to_html: function(template, view, partials, send_fun) {
|
|
var renderer = new Renderer();
|
|
if(send_fun) {
|
|
renderer.send = send_fun;
|
|
}
|
|
renderer.render(template, view || {}, partials);
|
|
if(!send_fun) {
|
|
return renderer.buffer.join("\n");
|
|
}
|
|
}
|
|
});
|
|
}();
|
|
;wax = wax || {};
|
|
|
|
// Attribution
|
|
// -----------
|
|
wax.attribution = function() {
|
|
var container,
|
|
a = {};
|
|
|
|
function urlX(url) {
|
|
// Data URIs are subject to a bug in Firefox
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
|
// which let them be a vector. But WebKit does 'the right thing'
|
|
// or at least 'something' about this situation, so we'll tolerate
|
|
// them.
|
|
if (/^(https?:\/\/|data:image)/.test(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
function idX(id) {
|
|
return id;
|
|
}
|
|
|
|
a.set = function(content) {
|
|
if (typeof content === 'undefined') return;
|
|
container.innerHTML = html_sanitize(content, urlX, idX);
|
|
return this;
|
|
};
|
|
|
|
a.element = function() {
|
|
return container;
|
|
};
|
|
|
|
a.init = function() {
|
|
container = document.createElement('div');
|
|
container.className = 'wax-attribution';
|
|
return this;
|
|
};
|
|
|
|
return a.init();
|
|
};
|
|
wax = wax || {};
|
|
|
|
// Attribution
|
|
// -----------
|
|
wax.bwdetect = function(options, callback) {
|
|
var detector = {},
|
|
threshold = options.threshold || 400,
|
|
// test image: 30.29KB
|
|
testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()),
|
|
// High-bandwidth assumed
|
|
// 1: high bandwidth (.png, .jpg)
|
|
// 0: low bandwidth (.png128, .jpg70)
|
|
bw = 1,
|
|
// Alternative versions
|
|
auto = options.auto === undefined ? true : options.auto;
|
|
|
|
function bwTest() {
|
|
wax.bw = -1;
|
|
var im = new Image();
|
|
im.src = testImage;
|
|
var first = true;
|
|
var timeout = setTimeout(function() {
|
|
if (first && wax.bw == -1) {
|
|
detector.bw(0);
|
|
first = false;
|
|
}
|
|
}, threshold);
|
|
im.onload = function() {
|
|
if (first && wax.bw == -1) {
|
|
clearTimeout(timeout);
|
|
detector.bw(1);
|
|
first = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
detector.bw = function(x) {
|
|
if (!arguments.length) return bw;
|
|
var oldBw = bw;
|
|
if (wax.bwlisteners && wax.bwlisteners.length) (function () {
|
|
listeners = wax.bwlisteners;
|
|
wax.bwlisteners = [];
|
|
for (i = 0; i < listeners; i++) {
|
|
listeners[i](x);
|
|
}
|
|
})();
|
|
wax.bw = x;
|
|
|
|
if (bw != (bw = x)) callback(x);
|
|
};
|
|
|
|
detector.add = function() {
|
|
if (auto) bwTest();
|
|
return this;
|
|
};
|
|
|
|
if (wax.bw == -1) {
|
|
wax.bwlisteners = wax.bwlisteners || [];
|
|
wax.bwlisteners.push(detector.bw);
|
|
} else if (wax.bw !== undefined) {
|
|
detector.bw(wax.bw);
|
|
} else {
|
|
detector.add();
|
|
}
|
|
return detector;
|
|
};
|
|
// Formatter
|
|
// ---------
|
|
//
|
|
// This code is no longer the recommended code path for Wax -
|
|
// see `template.js`, a safe implementation of Mustache templates.
|
|
wax.formatter = function(x) {
|
|
var formatter = {},
|
|
f;
|
|
|
|
// Prevent against just any input being used.
|
|
if (x && typeof x === 'string') {
|
|
try {
|
|
// Ugly, dangerous use of eval.
|
|
eval('f = ' + x);
|
|
} catch (e) {
|
|
if (console) console.log(e);
|
|
}
|
|
} else if (x && typeof x === 'function') {
|
|
f = x;
|
|
} else {
|
|
f = function() {};
|
|
}
|
|
|
|
function urlX(url) {
|
|
if (/^(https?:\/\/|data:image)/.test(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
function idX(id) {
|
|
return id;
|
|
}
|
|
|
|
// Wrap the given formatter function in order to
|
|
// catch exceptions that it may throw.
|
|
formatter.format = function(options, data) {
|
|
try {
|
|
return html_sanitize(f(options, data), urlX, idX);
|
|
} catch (e) {
|
|
if (console) console.log(e);
|
|
}
|
|
};
|
|
|
|
return formatter;
|
|
};
|
|
// GridInstance
|
|
// ------------
|
|
// GridInstances are queryable, fully-formed
|
|
// objects for acquiring features from events.
|
|
//
|
|
// This code ignores format of 1.1-1.2
|
|
wax.GridInstance = function(grid_tile, formatter, 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 section of the mbtiles spec](https://github.com/mapbox/mbtiles-spec/blob/master/1.1/utfgrid.md)
|
|
// 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.
|
|
// * `options` options to give to the formatter: minimally having a `format`
|
|
// member, being `full`, `teaser`, or something else.
|
|
instance.tileFeature = function(x, y, tile_element, options) {
|
|
if (!grid_tile) return;
|
|
// IE problem here - though recoverable, for whatever reason
|
|
var offset = wax.util.offset(tile_element);
|
|
feature = this.gridFeature(x - offset.left, y - offset.top);
|
|
|
|
if (feature) return formatter.format(options, 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.GridManager = function(options) {
|
|
options = options || {};
|
|
|
|
var resolution = options.resolution || 4,
|
|
version = options.version || '1.1',
|
|
grid_tiles = {},
|
|
manager = {},
|
|
formatter;
|
|
|
|
var gridUrl = function(url) {
|
|
return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json');
|
|
};
|
|
|
|
function templatedGridUrl(template) {
|
|
if (typeof template === 'string') template = [template];
|
|
return function templatedGridFinder(url) {
|
|
if (!url) return;
|
|
var xyz = /(\d+)\/(\d+)\/(\d+)\.[\w\._]+/g.exec(url);
|
|
if (!xyz) return;
|
|
return template[parseInt(xyz[2], 10) % template.length]
|
|
.replace('{z}', xyz[1])
|
|
.replace('{x}', xyz[2])
|
|
.replace('{y}', xyz[3]);
|
|
};
|
|
}
|
|
|
|
manager.formatter = function(x) {
|
|
if (!arguments.length) return formatter;
|
|
formatter = wax.formatter(x);
|
|
return manager;
|
|
};
|
|
|
|
manager.template = function(x) {
|
|
if (!arguments.length) return formatter;
|
|
formatter = wax.template(x);
|
|
return manager;
|
|
};
|
|
|
|
manager.gridUrl = function(x) {
|
|
if (!arguments.length) return gridUrl;
|
|
gridUrl = typeof x === 'function' ?
|
|
x : templatedGridUrl(x);
|
|
return manager;
|
|
};
|
|
|
|
manager.getGrid = function(url, callback) {
|
|
var gurl = gridUrl(url);
|
|
if (!formatter || !gurl) return callback(null, null);
|
|
|
|
wax.request.get(gurl, function(err, t) {
|
|
if (err) return callback(err, null);
|
|
callback(null, wax.GridInstance(t, formatter, {
|
|
resolution: resolution || 4
|
|
}));
|
|
});
|
|
return manager;
|
|
};
|
|
|
|
manager.add = function(options) {
|
|
if (options.template) {
|
|
manager.template(options.template);
|
|
} else if (options.formatter) {
|
|
manager.formatter(options.formatter);
|
|
}
|
|
|
|
if (options.grids) {
|
|
manager.gridUrl(options.grids);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
return manager.add(options);
|
|
};
|
|
wax = wax || {};
|
|
|
|
// Hash
|
|
// ----
|
|
wax.hash = function(options) {
|
|
options = options || {};
|
|
|
|
function getState() {
|
|
return location.hash.substring(1);
|
|
}
|
|
|
|
function pushState(state) {
|
|
location.hash = '#' + state;
|
|
}
|
|
|
|
var s0, // old hash
|
|
hash = {},
|
|
lat = 90 - 1e-8; // allowable latitude range
|
|
|
|
function parseHash(s) {
|
|
var args = s.split('/');
|
|
for (var i = 0; i < args.length; i++) {
|
|
args[i] = Number(args[i]);
|
|
if (isNaN(args[i])) return true;
|
|
}
|
|
if (args.length < 3) {
|
|
// replace bogus hash
|
|
return true;
|
|
} else if (args.length == 3) {
|
|
options.setCenterZoom(args);
|
|
}
|
|
}
|
|
|
|
function move() {
|
|
var s1 = options.getCenterZoom();
|
|
if (s0 !== s1) {
|
|
s0 = s1;
|
|
// don't recenter the map!
|
|
pushState(s0);
|
|
}
|
|
}
|
|
|
|
function stateChange(state) {
|
|
// ignore spurious hashchange events
|
|
if (state === s0) return;
|
|
if (parseHash(s0 = state)) {
|
|
// replace bogus hash
|
|
move();
|
|
}
|
|
}
|
|
|
|
var _move = wax.util.throttle(move, 500);
|
|
|
|
hash.add = function() {
|
|
stateChange(getState());
|
|
options.bindChange(_move);
|
|
return this;
|
|
};
|
|
|
|
hash.remove = function() {
|
|
options.unbindChange(_move);
|
|
return this;
|
|
};
|
|
|
|
return hash.add();
|
|
};
|
|
// Wax Legend
|
|
// ----------
|
|
|
|
// Wax header
|
|
var wax = wax || {};
|
|
|
|
wax.legend = function() {
|
|
var element,
|
|
legend = {},
|
|
container;
|
|
|
|
function urlX(url) {
|
|
// Data URIs are subject to a bug in Firefox
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
|
// which let them be a vector. But WebKit does 'the right thing'
|
|
// or at least 'something' about this situation, so we'll tolerate
|
|
// them.
|
|
if (/^(https?:\/\/|data:image)/.test(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
function idX(id) {
|
|
return id;
|
|
}
|
|
|
|
legend.element = function() {
|
|
return container;
|
|
};
|
|
|
|
legend.content = function(content) {
|
|
if (!arguments.length) return element.innerHTML;
|
|
if (content) {
|
|
element.innerHTML = html_sanitize(content, urlX, idX);
|
|
element.style.display = 'block';
|
|
} else {
|
|
element.innerHTML = '';
|
|
element.style.display = 'none';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
legend.add = function() {
|
|
container = document.createElement('div');
|
|
container.className = 'wax-legends';
|
|
|
|
element = document.createElement('div');
|
|
element.className = 'wax-legend';
|
|
element.style.display = 'none';
|
|
|
|
container.appendChild(element);
|
|
return this;
|
|
};
|
|
|
|
return legend.add();
|
|
};
|
|
// Like underscore's bind, except it runs a function
|
|
// with no arguments off of an object.
|
|
//
|
|
// var map = ...;
|
|
// w(map).melt(myFunction);
|
|
//
|
|
// is equivalent to
|
|
//
|
|
// var map = ...;
|
|
// myFunction(map);
|
|
//
|
|
var w = function(self) {
|
|
self.melt = function(func, obj) {
|
|
return func.apply(obj, [self, obj]);
|
|
};
|
|
return self;
|
|
};
|
|
var wax = wax || {};
|
|
wax.movetip = {};
|
|
|
|
wax.movetip = function(options) {
|
|
options = options || {};
|
|
var t = {},
|
|
_currentTooltip = undefined,
|
|
_context = undefined,
|
|
_animationOut = options.animationOut,
|
|
_animationIn = options.animationIn;
|
|
|
|
// Helper function to determine whether a given element is a wax popup.
|
|
function isPopup (el) {
|
|
return el && el.className.indexOf('wax-popup') !== -1;
|
|
}
|
|
|
|
function getTooltip(feature, context) {
|
|
var tooltip = document.createElement('div');
|
|
tooltip.className = 'wax-movetip';
|
|
tooltip.style.cssText = 'position:absolute;';
|
|
tooltip.innerHTML = feature;
|
|
context.appendChild(tooltip);
|
|
_context = context;
|
|
_tooltipOffset = wax.util.offset(tooltip);
|
|
_contextOffset = wax.util.offset(_context);
|
|
return tooltip;
|
|
}
|
|
|
|
function moveTooltip(e) {
|
|
if (!_currentTooltip) return;
|
|
var eo = wax.util.eventoffset(e);
|
|
|
|
_currentTooltip.className = 'wax-movetip';
|
|
|
|
// faux-positioning
|
|
if ((_tooltipOffset.height + eo.y) >
|
|
(_contextOffset.top + _contextOffset.height) &&
|
|
(_contextOffset.height > _tooltipOffset.height)) {
|
|
eo.y -= _tooltipOffset.height;
|
|
_currentTooltip.className += ' flip-y';
|
|
}
|
|
|
|
// faux-positioning
|
|
if ((_tooltipOffset.width + eo.x) >
|
|
(_contextOffset.left + _contextOffset.width)) {
|
|
eo.x -= _tooltipOffset.width;
|
|
_currentTooltip.className += ' flip-x';
|
|
}
|
|
|
|
_currentTooltip.style.left = eo.x + 'px';
|
|
_currentTooltip.style.top = eo.y + 'px';
|
|
}
|
|
|
|
// Hide a given tooltip.
|
|
function hideTooltip(el) {
|
|
if (!el) return;
|
|
var event,
|
|
remove = function() {
|
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
};
|
|
|
|
if (el.style['-webkit-transition'] !== undefined && _animationOut) {
|
|
event = 'webkitTransitionEnd';
|
|
} else if (el.style.MozTransition !== undefined && _animationOut) {
|
|
event = 'transitionend';
|
|
}
|
|
|
|
if (event) {
|
|
// This code assumes that transform-supporting browsers
|
|
// also support proper events. IE9 does both.
|
|
el.addEventListener(event, remove, false);
|
|
el.addEventListener('transitionend', remove, false);
|
|
el.className += ' ' + _animationOut;
|
|
} else {
|
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
}
|
|
}
|
|
|
|
// Expand a tooltip to be a "popup". Suspends all other tooltips from being
|
|
// shown until this popup is closed or another popup is opened.
|
|
function click(feature, context) {
|
|
// Hide any current tooltips.
|
|
if (_currentTooltip) {
|
|
hideTooltip(_currentTooltip);
|
|
_currentTooltip = undefined;
|
|
}
|
|
|
|
var tooltip = getTooltip(feature, context);
|
|
tooltip.className += ' wax-popup';
|
|
tooltip.innerHTML = feature;
|
|
|
|
var close = document.createElement('a');
|
|
close.href = '#close';
|
|
close.className = 'close';
|
|
close.innerHTML = 'Close';
|
|
tooltip.appendChild(close);
|
|
|
|
var closeClick = function(ev) {
|
|
hideTooltip(tooltip);
|
|
_currentTooltip = undefined;
|
|
ev.returnValue = false; // Prevents hash change.
|
|
if (ev.stopPropagation) ev.stopPropagation();
|
|
if (ev.preventDefault) ev.preventDefault();
|
|
return false;
|
|
};
|
|
|
|
// IE compatibility.
|
|
if (close.addEventListener) {
|
|
close.addEventListener('click', closeClick, false);
|
|
} else if (close.attachEvent) {
|
|
close.attachEvent('onclick', closeClick);
|
|
}
|
|
|
|
_currentTooltip = tooltip;
|
|
}
|
|
|
|
t.over = function(feature, context, e) {
|
|
if (!feature) return;
|
|
context.style.cursor = 'pointer';
|
|
|
|
if (isPopup(_currentTooltip)) {
|
|
return;
|
|
} else {
|
|
_currentTooltip = getTooltip(feature, context);
|
|
moveTooltip(e);
|
|
if (context.addEventListener) {
|
|
context.addEventListener('mousemove', moveTooltip);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Hide all tooltips on this layer and show the first hidden tooltip on the
|
|
// highest layer underneath if found.
|
|
t.out = function(context) {
|
|
context.style.cursor = 'default';
|
|
|
|
if (isPopup(_currentTooltip)) {
|
|
return;
|
|
} else if (_currentTooltip) {
|
|
hideTooltip(_currentTooltip);
|
|
if (context.removeEventListener) {
|
|
context.removeEventListener('mousemove', moveTooltip);
|
|
}
|
|
_currentTooltip = undefined;
|
|
}
|
|
};
|
|
|
|
return t;
|
|
};
|
|
// Wax GridUtil
|
|
// ------------
|
|
|
|
// Wax header
|
|
var wax = wax || {};
|
|
|
|
// Request
|
|
// -------
|
|
// Request data cache. `callback(data)` where `data` is the response data.
|
|
wax.request = {
|
|
cache: {},
|
|
locks: {},
|
|
promises: {},
|
|
get: function(url, callback) {
|
|
// Cache hit.
|
|
if (this.cache[url]) {
|
|
return callback(this.cache[url][0], this.cache[url][1]);
|
|
// Cache miss.
|
|
} else {
|
|
this.promises[url] = this.promises[url] || [];
|
|
this.promises[url].push(callback);
|
|
// Lock hit.
|
|
if (this.locks[url]) return;
|
|
// Request.
|
|
var that = this;
|
|
this.locks[url] = true;
|
|
reqwest({
|
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
|
|
type: 'jsonp',
|
|
jsonpCallback: 'callback',
|
|
success: function(data) {
|
|
that.locks[url] = false;
|
|
that.cache[url] = [null, data];
|
|
for (var i = 0; i < that.promises[url].length; i++) {
|
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
|
|
}
|
|
},
|
|
error: function(err) {
|
|
that.locks[url] = false;
|
|
that.cache[url] = [err, null];
|
|
for (var i = 0; i < that.promises[url].length; i++) {
|
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
// Templating
|
|
// ---------
|
|
wax.template = function(x) {
|
|
var template = {};
|
|
|
|
function urlX(url) {
|
|
// Data URIs are subject to a bug in Firefox
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
|
// which let them be a vector. But WebKit does 'the right thing'
|
|
// or at least 'something' about this situation, so we'll tolerate
|
|
// them.
|
|
if (/^(https?:\/\/|data:image)/.test(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
function idX(id) {
|
|
return id;
|
|
}
|
|
|
|
// Clone the data object such that the '__[format]__' key is only
|
|
// set for this instance of templating.
|
|
template.format = function(options, data) {
|
|
var clone = {};
|
|
for (var key in data) {
|
|
clone[key] = data[key];
|
|
}
|
|
if (options.format) {
|
|
clone['__' + options.format + '__'] = true;
|
|
}
|
|
return html_sanitize(Mustache.to_html(x, clone), urlX, idX);
|
|
};
|
|
|
|
return template;
|
|
};
|
|
if (!wax) var wax = {};
|
|
|
|
// A wrapper for reqwest jsonp to easily load TileJSON from a URL.
|
|
wax.tilejson = function(url, callback) {
|
|
reqwest({
|
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
|
|
type: 'jsonp',
|
|
jsonpCallback: 'callback',
|
|
success: callback,
|
|
error: callback
|
|
});
|
|
};
|
|
var wax = wax || {};
|
|
wax.tooltip = {};
|
|
|
|
wax.tooltip = function(options) {
|
|
this._currentTooltip = undefined;
|
|
options = options || {};
|
|
if (options.animationOut) this.animationOut = options.animationOut;
|
|
if (options.animationIn) this.animationIn = options.animationIn;
|
|
};
|
|
|
|
// Helper function to determine whether a given element is a wax popup.
|
|
wax.tooltip.prototype.isPopup = function(el) {
|
|
return el && el.className.indexOf('wax-popup') !== -1;
|
|
};
|
|
|
|
// Get the active tooltip for a layer or create a new one if no tooltip exists.
|
|
// Hide any tooltips on layers underneath this one.
|
|
wax.tooltip.prototype.getTooltip = function(feature, context) {
|
|
var tooltip = document.createElement('div');
|
|
tooltip.className = 'wax-tooltip wax-tooltip-0';
|
|
tooltip.innerHTML = feature;
|
|
context.appendChild(tooltip);
|
|
return tooltip;
|
|
};
|
|
|
|
// Hide a given tooltip.
|
|
wax.tooltip.prototype.hideTooltip = function(el) {
|
|
if (!el) return;
|
|
var event,
|
|
remove = function() {
|
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
};
|
|
|
|
if (el.style['-webkit-transition'] !== undefined && this.animationOut) {
|
|
event = 'webkitTransitionEnd';
|
|
} else if (el.style.MozTransition !== undefined && this.animationOut) {
|
|
event = 'transitionend';
|
|
}
|
|
|
|
if (event) {
|
|
// This code assumes that transform-supporting browsers
|
|
// also support proper events. IE9 does both.
|
|
el.addEventListener(event, remove, false);
|
|
el.addEventListener('transitionend', remove, false);
|
|
el.className += ' ' + this.animationOut;
|
|
} else {
|
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
}
|
|
};
|
|
|
|
// Expand a tooltip to be a "popup". Suspends all other tooltips from being
|
|
// shown until this popup is closed or another popup is opened.
|
|
wax.tooltip.prototype.click = function(feature, context) {
|
|
// Hide any current tooltips.
|
|
if (this._currentTooltip) {
|
|
this.hideTooltip(this._currentTooltip);
|
|
this._currentTooltip = undefined;
|
|
}
|
|
|
|
var tooltip = this.getTooltip(feature, context);
|
|
tooltip.className += ' wax-popup';
|
|
tooltip.innerHTML = feature;
|
|
|
|
var close = document.createElement('a');
|
|
close.href = '#close';
|
|
close.className = 'close';
|
|
close.innerHTML = 'Close';
|
|
tooltip.appendChild(close);
|
|
|
|
var closeClick = wax.util.bind(function(ev) {
|
|
this.hideTooltip(tooltip);
|
|
this._currentTooltip = undefined;
|
|
ev.returnValue = false; // Prevents hash change.
|
|
if (ev.stopPropagation) ev.stopPropagation();
|
|
if (ev.preventDefault) ev.preventDefault();
|
|
return false;
|
|
}, this);
|
|
|
|
// IE compatibility.
|
|
if (close.addEventListener) {
|
|
close.addEventListener('click', closeClick, false);
|
|
} else if (close.attachEvent) {
|
|
close.attachEvent('onclick', closeClick);
|
|
}
|
|
|
|
this._currentTooltip = tooltip;
|
|
};
|
|
|
|
// Show a tooltip.
|
|
wax.tooltip.prototype.over = function(feature, context) {
|
|
if (!feature) return;
|
|
context.style.cursor = 'pointer';
|
|
|
|
if (this.isPopup(this._currentTooltip)) {
|
|
return;
|
|
} else {
|
|
this._currentTooltip = this.getTooltip(feature, context);
|
|
}
|
|
};
|
|
|
|
|
|
// Hide all tooltips on this layer and show the first hidden tooltip on the
|
|
// highest layer underneath if found.
|
|
wax.tooltip.prototype.out = function(context) {
|
|
context.style.cursor = 'default';
|
|
|
|
if (this.isPopup(this._currentTooltip)) {
|
|
return;
|
|
} else if (this._currentTooltip) {
|
|
this.hideTooltip(this._currentTooltip);
|
|
this._currentTooltip = undefined;
|
|
}
|
|
};
|
|
var wax = wax || {};
|
|
wax.util = wax.util || {};
|
|
|
|
// Utils are extracted from other libraries or
|
|
// written from scratch to plug holes in browser compatibility.
|
|
wax.util = {
|
|
// From Bonzo
|
|
offset: function(el) {
|
|
// TODO: window margins
|
|
//
|
|
// Okay, so fall back to styles if offsetWidth and height are botched
|
|
// by Firefox.
|
|
var width = el.offsetWidth || parseInt(el.style.width, 10),
|
|
height = el.offsetHeight || parseInt(el.style.height, 10),
|
|
doc_body = document.body,
|
|
top = 0,
|
|
left = 0;
|
|
|
|
var calculateOffset = function(el) {
|
|
if (el === doc_body || el === document.documentElement) return;
|
|
top += el.offsetTop;
|
|
left += el.offsetLeft;
|
|
|
|
var style = el.style.transform ||
|
|
el.style.WebkitTransform ||
|
|
el.style.OTransform ||
|
|
el.style.MozTransform ||
|
|
el.style.msTransform;
|
|
|
|
if (style) {
|
|
if (match = style.match(/translate\((.+)px, (.+)px\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
} else if (match = style.match(/translate3d\((.+)px, (.+)px, (.+)px\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
} else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) {
|
|
var pts = match[1].split(',');
|
|
top += parseInt(pts[13], 10);
|
|
left += parseInt(pts[12], 10);
|
|
} else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
calculateOffset(el);
|
|
|
|
try {
|
|
while (el = el.offsetParent) calculateOffset(el);
|
|
} catch(e) {
|
|
// Hello, internet explorer.
|
|
}
|
|
|
|
// Offsets from the body
|
|
top += doc_body.offsetTop;
|
|
left += doc_body.offsetLeft;
|
|
// Offsets from the HTML element
|
|
top += doc_body.parentNode.offsetTop;
|
|
left += doc_body.parentNode.offsetLeft;
|
|
|
|
// Firefox and other weirdos. Similar technique to jQuery's
|
|
// `doesNotIncludeMarginInBodyOffset`.
|
|
var htmlComputed = document.defaultView ?
|
|
window.getComputedStyle(doc_body.parentNode, null) :
|
|
doc_body.parentNode.currentStyle;
|
|
if (doc_body.parentNode.offsetTop !==
|
|
parseInt(htmlComputed.marginTop, 10) &&
|
|
!isNaN(parseInt(htmlComputed.marginTop, 10))) {
|
|
top += parseInt(htmlComputed.marginTop, 10);
|
|
left += parseInt(htmlComputed.marginLeft, 10);
|
|
}
|
|
|
|
return {
|
|
top: top,
|
|
left: left,
|
|
height: height,
|
|
width: width
|
|
};
|
|
},
|
|
|
|
'$': function(x) {
|
|
return (typeof x === 'string') ?
|
|
document.getElementById(x) :
|
|
x;
|
|
},
|
|
|
|
// From underscore, minus funcbind for now.
|
|
// Returns a version of a function that always has the second parameter,
|
|
// `obj`, as `this`.
|
|
bind: function(func, obj) {
|
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
return function() {
|
|
return func.apply(obj, args.concat(Array.prototype.slice.call(arguments)));
|
|
};
|
|
},
|
|
// From underscore
|
|
isString: function(obj) {
|
|
return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
|
|
},
|
|
// IE doesn't have indexOf
|
|
indexOf: function(array, item) {
|
|
var nativeIndexOf = Array.prototype.indexOf;
|
|
if (array === null) return -1;
|
|
var i, l;
|
|
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
|
|
for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
|
|
return -1;
|
|
},
|
|
// is this object an array?
|
|
isArray: Array.isArray || function(obj) {
|
|
return Object.prototype.toString.call(obj) === '[object Array]';
|
|
},
|
|
// From underscore: reimplement the ECMA5 `Object.keys()` method
|
|
keys: Object.keys || function(obj) {
|
|
var ho = Object.prototype.hasOwnProperty;
|
|
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
|
var keys = [];
|
|
for (var key in obj) if (ho.call(obj, key)) keys[keys.length] = key;
|
|
return keys;
|
|
},
|
|
// From quirksmode: normalize the offset of an event from the top-left
|
|
// of the page.
|
|
eventoffset: function(e) {
|
|
var posx = 0;
|
|
var posy = 0;
|
|
if (!e) var e = window.event;
|
|
if (e.pageX || e.pageY) {
|
|
// Good browsers
|
|
return {
|
|
x: e.pageX,
|
|
y: e.pageY
|
|
};
|
|
} else if (e.clientX || e.clientY) {
|
|
// Internet Explorer
|
|
var doc = document.documentElement, body = document.body;
|
|
var htmlComputed = document.body.parentNode.currentStyle;
|
|
var topMargin = parseInt(htmlComputed.marginTop, 10) || 0;
|
|
var leftMargin = parseInt(htmlComputed.marginLeft, 10) || 0;
|
|
return {
|
|
x: e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
|
|
(doc && doc.clientLeft || body && body.clientLeft || 0) + leftMargin,
|
|
y: e.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
|
|
(doc && doc.clientTop || body && body.clientTop || 0) + topMargin
|
|
};
|
|
} else if (e.touches && e.touches.length === 1) {
|
|
// Touch browsers
|
|
return {
|
|
x: e.touches[0].pageX,
|
|
y: e.touches[0].pageY
|
|
};
|
|
}
|
|
},
|
|
|
|
// Ripped from underscore.js
|
|
// Internal function used to implement `_.throttle` and `_.debounce`.
|
|
limit: function(func, wait, debounce) {
|
|
var timeout;
|
|
return function() {
|
|
var context = this, args = arguments;
|
|
var throttler = function() {
|
|
timeout = null;
|
|
func.apply(context, args);
|
|
};
|
|
if (debounce) clearTimeout(timeout);
|
|
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
|
|
};
|
|
},
|
|
|
|
// Returns a function, that, when invoked, will only be triggered at most once
|
|
// during a given window of time.
|
|
throttle: function(func, wait) {
|
|
return this.limit(func, wait, false);
|
|
},
|
|
// parseUri 1.2.2
|
|
// Steven Levithan <stevenlevithan.com>
|
|
parseUri: function(str) {
|
|
var o = {
|
|
strictMode: false,
|
|
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
|
|
q: {
|
|
name: "queryKey",
|
|
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
|
|
},
|
|
parser: {
|
|
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
|
|
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
|
|
}
|
|
},
|
|
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
|
|
uri = {},
|
|
i = 14;
|
|
|
|
while (i--) uri[o.key[i]] = m[i] || "";
|
|
|
|
uri[o.q.name] = {};
|
|
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
|
|
if ($1) uri[o.q.name][$1] = $2;
|
|
});
|
|
return uri;
|
|
},
|
|
// appends callback onto urls regardless of existing query params
|
|
addUrlData: function(url, data) {
|
|
url += (this.parseUri(url).query) ? '&' : '?';
|
|
return url += data;
|
|
}
|
|
};
|
|
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.util.$(elem).appendChild(a.element());
|
|
return this;
|
|
};
|
|
|
|
attribution.init = function() {
|
|
a = wax.attribution();
|
|
a.set(tilejson.attribution);
|
|
a.element().className = 'wax-attribution wax-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 || {};
|
|
|
|
// A control that adds interaction to a google Map object.
|
|
//
|
|
// Takes an options object with the following keys:
|
|
//
|
|
// * `callbacks` (optional): an `out`, `over`, and `click` callback.
|
|
// If not given, the `wax.tooltip` library will be expected.
|
|
// * `clickAction` (optional): **full** or **location**: default is
|
|
// **full**.
|
|
wax.g.interaction = function(map, tilejson, options) {
|
|
tilejson = tilejson || {};
|
|
options = options || {};
|
|
// Our GridManager (from `gridutil.js`). This will keep the
|
|
// cache of grid information and provide friendly utility methods
|
|
// that return `GridTile` objects instead of raw data.
|
|
var interaction = {
|
|
waxGM: new wax.GridManager(tilejson),
|
|
|
|
// This requires wax.Tooltip or similar
|
|
callbacks: options.callbacks || new wax.tooltip(),
|
|
clickAction: options.clickAction || 'full',
|
|
eventHandlers:{},
|
|
|
|
// Attach listeners to the map
|
|
add: function() {
|
|
this.eventHandlers.tileloaded = google.maps.event.addListener(map, 'tileloaded',
|
|
wax.util.bind(this.clearTileGrid, this));
|
|
|
|
this.eventHandlers.idle = google.maps.event.addListener(map, 'idle',
|
|
wax.util.bind(this.clearTileGrid, this));
|
|
|
|
this.eventHandlers.mousemove = google.maps.event.addListener(map, 'mousemove',
|
|
this.onMove());
|
|
|
|
this.eventHandlers.click = google.maps.event.addListener(map, 'click',
|
|
this.click());
|
|
|
|
return this;
|
|
},
|
|
|
|
// Remove interaction events from the map.
|
|
remove: function() {
|
|
google.maps.event.removeListener(this.eventHandlers.tileloaded);
|
|
google.maps.event.removeListener(this.eventHandlers.idle);
|
|
google.maps.event.removeListener(this.eventHandlers.mousemove);
|
|
google.maps.event.removeListener(this.eventHandlers.click);
|
|
return this;
|
|
},
|
|
|
|
// Search through `.tiles` and determine the position,
|
|
// from the top-left of the **document**, and cache that data
|
|
// so that `mousemove` events don't always recalculate.
|
|
getTileGrid: function() {
|
|
// Get all 'marked' tiles, added by the `wax.g.MapType` layer.
|
|
// Return an array of objects which have the **relative** offset of
|
|
// each tile, with a reference to the tile object in `tile`, since the API
|
|
// returns evt coordinates as relative to the map object.
|
|
if (!this._getTileGrid) {
|
|
this._getTileGrid = [];
|
|
var zoom = map.getZoom();
|
|
var mapOffset = wax.util.offset(map.getDiv());
|
|
var get = wax.util.bind(function(mapType) {
|
|
if (!mapType.interactive) return;
|
|
for (var key in mapType.cache) {
|
|
if (key.split('/')[0] != zoom) continue;
|
|
var tileOffset = wax.util.offset(mapType.cache[key]);
|
|
this._getTileGrid.push([
|
|
tileOffset.top - mapOffset.top,
|
|
tileOffset.left - mapOffset.left,
|
|
mapType.cache[key]
|
|
]);
|
|
}
|
|
}, this);
|
|
// Iterate over base mapTypes and overlayMapTypes.
|
|
for (var i in map.mapTypes) get(map.mapTypes[i]);
|
|
map.overlayMapTypes.forEach(get);
|
|
}
|
|
return this._getTileGrid;
|
|
},
|
|
|
|
clearTileGrid: function(map, e) {
|
|
this._getTileGrid = null;
|
|
},
|
|
|
|
getTile: function(evt) {
|
|
var tile;
|
|
var grid = this.getTileGrid();
|
|
for (var i = 0; i < grid.length; i++) {
|
|
if ((grid[i][0] < evt.pixel.y) &&
|
|
((grid[i][0] + 256) > evt.pixel.y) &&
|
|
(grid[i][1] < evt.pixel.x) &&
|
|
((grid[i][1] + 256) > evt.pixel.x)) {
|
|
tile = grid[i][2];
|
|
break;
|
|
}
|
|
}
|
|
return tile || false;
|
|
},
|
|
|
|
onMove: function(evt) {
|
|
if (!this._onMove) this._onMove = wax.util.bind(function(evt) {
|
|
var tile = this.getTile(evt);
|
|
if (tile) {
|
|
this.waxGM.getGrid(tile.src, wax.util.bind(function(err, g) {
|
|
if (err || !g) return;
|
|
var feature = g.tileFeature(
|
|
evt.pixel.x + wax.util.offset(map.getDiv()).left,
|
|
evt.pixel.y + wax.util.offset(map.getDiv()).top,
|
|
tile,
|
|
{ format: 'teaser' }
|
|
);
|
|
// Support only a single layer.
|
|
// Thus a layer index of **0** is given to the tooltip library
|
|
if (feature && this.feature !== feature) {
|
|
this.feature = feature;
|
|
this.callbacks.out(map.getDiv());
|
|
this.callbacks.over(feature, map.getDiv(), 0, evt);
|
|
} else if (!feature) {
|
|
this.feature = null;
|
|
this.callbacks.out(map.getDiv());
|
|
}
|
|
}, this));
|
|
}
|
|
}, this);
|
|
return this._onMove;
|
|
},
|
|
|
|
click: function(evt) {
|
|
if (!this._onClick) this._onClick = wax.util.bind(function(evt) {
|
|
// Feature previously found? Don't continue with the event...
|
|
// @vizzuality change!
|
|
if (window.event && window.event.cancelBubble) {
|
|
return false;
|
|
}
|
|
|
|
var tile = this.getTile(evt);
|
|
if (tile) {
|
|
this.waxGM.getGrid(tile.src, wax.util.bind(function(err, g) {
|
|
if (err || !g) return;
|
|
var feature = g.tileFeature(
|
|
evt.pixel.x + wax.util.offset(map.getDiv()).left,
|
|
evt.pixel.y + wax.util.offset(map.getDiv()).top,
|
|
tile,
|
|
{ format: this.clickAction }
|
|
);
|
|
if (feature) {
|
|
// Stop propagation of the click event to avoid fire more wax clicks!!!
|
|
// @vizzuality change!
|
|
|
|
// For Firefox browser that doesn't recognize window.event
|
|
if (!window.event) {
|
|
window.event = {};
|
|
setTimeout(function(){
|
|
window.event = null;
|
|
},150);
|
|
}
|
|
|
|
window.event.cancelBubble = true;
|
|
|
|
switch (this.clickAction) {
|
|
case 'full':
|
|
this.callbacks.click(feature, map.getDiv(), 0, evt);
|
|
break;
|
|
case 'location':
|
|
window.location = feature;
|
|
break;
|
|
}
|
|
}
|
|
}, this));
|
|
}
|
|
}, this);
|
|
return this._onClick;
|
|
}
|
|
};
|
|
|
|
// Return the interaction control such that the caller may manipulate it
|
|
// e.g. remove it.
|
|
return interaction.add(map);
|
|
};
|
|
|
|
wax = wax || {};
|
|
wax.g = wax.g || {};
|
|
|
|
// Legend Control
|
|
// --------------
|
|
// Adds legends to a google Map object.
|
|
wax.g.legend = function(map, tilejson) {
|
|
tilejson = tilejson || {};
|
|
var l, // parent legend
|
|
legend = {};
|
|
|
|
legend.add = function() {
|
|
l = wax.legend()
|
|
.content(tilejson.legend || '');
|
|
return this;
|
|
};
|
|
|
|
legend.element = function() {
|
|
return l.element();
|
|
};
|
|
|
|
legend.appendTo = function(elem) {
|
|
wax.util.$(elem).appendChild(l.element());
|
|
return this;
|
|
};
|
|
|
|
return legend.add();
|
|
};
|
|
// Wax for Google Maps API v3
|
|
// --------------------------
|
|
|
|
// Wax header
|
|
var wax = wax || {};
|
|
wax.g = wax.g || {};
|
|
|
|
// Wax Google Maps MapType: takes an object of options in the form
|
|
//
|
|
// {
|
|
// name: '',
|
|
// filetype: '.png',
|
|
// layerName: 'world-light',
|
|
// alt: '',
|
|
// zoomRange: [0, 18],
|
|
// baseUrl: 'a url',
|
|
// }
|
|
wax.g.connector = function(options) {
|
|
options = options || {};
|
|
|
|
this.options = {
|
|
tiles: options.tiles,
|
|
scheme: options.scheme || 'xyz',
|
|
blankImage: options.blankImage
|
|
};
|
|
|
|
this.minZoom = options.minzoom || 0;
|
|
this.maxZoom = options.maxzoom || 22;
|
|
|
|
this.name = options.name || '';
|
|
this.description = options.description || '';
|
|
|
|
// non-configurable options
|
|
this.interactive = true;
|
|
this.tileSize = new google.maps.Size(256, 256);
|
|
|
|
// DOM element cache
|
|
this.cache = {};
|
|
};
|
|
|
|
// Get a tile element from a coordinate, zoom level, and an ownerDocument.
|
|
wax.g.connector.prototype.getTile = function(coord, zoom, ownerDocument) {
|
|
var key = zoom + '/' + coord.x + '/' + coord.y;
|
|
if (!this.cache[key]) {
|
|
var img = this.cache[key] = new Image(256, 256);
|
|
this.cache[key].src = this.getTileUrl(coord, zoom);
|
|
this.cache[key].setAttribute('gTileKey', key);
|
|
this.cache[key].onerror = function() { img.style.display = 'none'; };
|
|
}
|
|
return this.cache[key];
|
|
};
|
|
|
|
// Remove a tile that has fallen out of the map's viewport.
|
|
//
|
|
// TODO: expire cache data in the gridmanager.
|
|
wax.g.connector.prototype.releaseTile = function(tile) {
|
|
var key = tile.getAttribute('gTileKey');
|
|
this.cache[key] && delete this.cache[key];
|
|
tile.parentNode && tile.parentNode.removeChild(tile);
|
|
};
|
|
|
|
// Get a tile url, based on x, y coordinates and a z value.
|
|
wax.g.connector.prototype.getTileUrl = function(coord, z) {
|
|
// Y coordinate is flipped in Mapbox, compared to Google
|
|
var mod = Math.pow(2, z),
|
|
y = (this.options.scheme === 'tms') ?
|
|
(mod - 1) - coord.y :
|
|
coord.y,
|
|
x = (coord.x % mod);
|
|
|
|
x = (x < 0) ? (coord.x % mod) + mod : x;
|
|
|
|
if (y < 0) return this.options.blankImage;
|
|
|
|
return this.options.tiles
|
|
[parseInt(x + y, 10) %
|
|
this.options.tiles.length]
|
|
.replace('{z}', z)
|
|
.replace('{x}', x)
|
|
.replace('{y}', y);
|
|
}; |