Skip to content

Instantly share code, notes, and snippets.

@auremoser
Last active April 5, 2016 17:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save auremoser/d2e883314470768a07f8 to your computer and use it in GitHub Desktop.
Save auremoser/d2e883314470768a07f8 to your computer and use it in GitHub Desktop.
Historical Lakes Odyssey

This gist demos how to pull a CartoDB into your own javascript projects!

<!DOCTYPE html>
<html>
<head>
<title>World Water Day</title>
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="favicon.png" sizes="32x32">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- add cartoCSS stylesheet -->
<link rel="stylesheet" href="http://libs.cartocdn.com/cartodb.js/v3/themes/css/cartodb.css"/>
<style>
body {
text-align: center;
margin: 0; padding: 0;
font-family: 'Open Sans', sans-serif;
background: #0078A8;
}
h1, h2 {
font-weight: bold;
color: #0078A8;
}
#layout {
width: 768px;
margin: 0px auto;
padding-top: 70px;
background: #F1F0EA;
height: 4400px;
}
#map_pos {
height: 400px;
}
.attachTop {
position: fixed;
width: 768px;
top: 0;
}
#content {
padding: 0 40px;
height: 1500px;
text-align:left;
}
#content h1{
text-align: center;
margin-top: 50px;
}
#content h2, h3, h4{
text-align: center;
margin: 15px;
}
#content p {
margin: 10px;
}
</style>
</head>
<body>
<div id="layout">
<h1>World Water Day: Human-Influenced Lakes</h1>
<br>
<h3>"Perhaps the truth depends on a walk around the lake." ~ Wallace Stevens</h3>
<br>
<div id="map_pos">
<div id="map_container">
<div id="map" style="width: 100%; height: 400px"></div>
</div>
</div>
<div id="content">
<!-- INTRO SECTION -->
<h2 id="intro"> Shrinking Seas </h2>
<p>A combination of satellite imagery and water resource data confirms that many natural lakes are progressively shrinking in size and coverage. This can both devastate landscapes dependent on water, and reveal new landscapes that expose terrain to human civilization.</p>
<p>The following chapters tell that story using <a href="http://www.naturalearthdata.com/downloads/10m-physical-vectors/10m-lakes/">data from historical lakes and current lakes</a>.</p>
<!-- ARAL SEA -->
<h2 id="aral"> Aral Sea </h2>
<p>"Formerly one of the four largest lakes in the world with an area of 68,000 km2 (26,300 sq mi), the Aral Sea has been steadily shrinking since the 1960s after the rivers that fed it were diverted by Soviet irrigation projects. By 2007, it had declined to 10% of its original size, splitting into four lakes – the North Aral Sea, the eastern and western basins of the once far larger South Aral Sea, and one smaller lake between the North and South Aral Seas."</p>
<p>"Satellite images taken by NASA in August 2014 revealed that for the first time in modern history the eastern basin of the Aral Sea had completely dried up. The eastern basin is now called the Aralkum desert."</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Aral_Sea">Wikipedia: Aral Sea</a></p>
<img src="http://upload.wikimedia.org/wikipedia/commons/7/75/AralSea1989_2014.jpg" alt="Aral Sea Satellite" style="width:680px;height:350px;border:0">
<h4>A comparison of the Aral Sea in 1989 (left) and 2014 (right). <a href="http://en.wikipedia.org/wiki/Aral_Sea#/media/File:AralSea1989_2014.jpg">Source: NASA</a></h4>
<!-- LAKE CHAD -->
<h2 id="chad"> Lake Chad </h2>
<p>"Lake Chad is a historically large, shallow, endorheic lake in Africa, the size of which has varied over the centuries. According to the Global Resource Information Database of the United Nations Environment Programme, it shrank as much as 95% from about 1963 to 1998, but "the 2007 (satellite) image shows significant improvement over previous years."</p>
<img src="http://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/Lake_Chad_from_Apollo_7.jpg/1024px-Lake_Chad_from_Apollo_7.jpg" alt="Lake Chad from Apollo 7" style="width:680px;height:350px;border:0">
<h4>Lake Chad as seen from Apollo 7. <a href="http://en.wikipedia.org/wiki/Lake_Chad#/media/File:Lake_Chad_from_Apollo_7.jpg">Source: NASA</a></h4>
<p>"Lake Chad is economically important, providing water to more than 68 million people living in the four countries surrounding it (Chad, Cameroon, Niger, and Nigeria) on the edge of the Sahara Desert. It is the largest lake in the Chad Basin."</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Lake_Chad">Wikipedia: Lake Chad</a></p>
<!-- SALTON SEA -->
<h2 id="salton"> Salton Sea </h2>
<p>"The Salton Sea is a shallow, saline, endorheic rift lake located directly on the San Andreas Fault, predominantly in California's Imperial and Coachella valleys. The modern sea was accidentally created by the engineers of the California Development Company in 1905. In an effort to increase water flow into the area for farming, irrigation canals were dug from the Colorado River into the valley."" </p>
<p>"Due to fears of silt buildup, a cut was made in the bank of the Colorado River to further increase the water flow. The resulting outflow overwhelmed the engineered canal, and the river flowed into the Salton Basin for two years, filling the historic dry lake bed and creating the modern sea, before repairs were completed."</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Salton_Sea">Wikipedia: Salton Sea</a></p>
<img src="http://upload.wikimedia.org/wikipedia/commons/3/32/Salton_Sea_from_Space.jpg" alt="Salton Sea" style="width:680px;border:0">
<h4>Satellite Image of the Salton Sea. <a href="http://upload.wikimedia.org/wikipedia/commons/3/32/Salton_Sea_from_Space.jpg">Source: NASA</a></h4>
<!-- MEXICO CITY -->
<h2 id="texcoco"> Lake Texcoco </h2>
<p>"The Valley of Mexico is a basin with an average elevation of 2,236 metres (7,336 ft) above mean sea level located in the southern highlands of Mexico's central altiplano. Lake Texcoco formerly extended over a large portion in the southern half of the basin, where it was the largest of an interconnected chain of five major and several smaller lakes (the other main lakes being Xaltocan, Zumpango, Chalco and Xochimilco lakes)."</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Lake_Texcoco">Wikipedia: Lake Texcoco</a></p>
<img src="http://upload.wikimedia.org/wikipedia/commons/5/5d/Smog_boven_mexico_city_november_1985.jpg"alt="Mexico Basin Smog from NASA" style="width:680px;height:350px;border:0">
<h4>Smog in Mexico City, Mexico November 1985. <a href="http://earth.jsc.nasa.gov/sseop/efs/lores.pl?PHOTO=STS61B-40-65">Source: NASA</a></h4>
<p>Lake Texcoco was the lowest-lying of all the lakes, and occupied the minimum elevation in the valley so that water ultimately drained towards it. The lake basin is now occupied by Mexico City, the capital of the present-day nation of Mexico."</p>
<h4>Made with love and <a href="http://cartodb.github.io/odyssey.js/">Odyssey.JS</a></h4>
</div>
</div> <!-- layout -->
<!-- include cartodb.js library -->
<script src="http://libs.cartocdn.com/cartodb.js/v3/cartodb.js"></script>
<script src="odyssey.js"></script>
<script>
var vis;
var URL = 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
var map = L.map('map', {scrollWheelZoom: false}).setView([0, 0.0], 2);
L.tileLayer(URL, { attribution: 'data OSM - map CartoDB' }).addTo(map);
// add your url ID here between viz/ and /viz.json
var layerUrl = 'http://andye.cartodb.com/api/v2/viz/37231a7e-d3d0-11e4-8142-0e853d047bba/viz.json';
// create map with layers from cartodb editor
cartodb.createLayer(map, layerUrl)
.addTo(map)
.on('done', function(layers) {
console.log('layers', layers)
window.layers = layers;
})
.on('error', function() {
//log the error
cartodb.log.log("some error occurred");
});
var intro = new L.LatLng(0.0, 0.0);
var aral = new L.LatLng(45.0000, 60.0000);
var chad = new L.LatLng(13.0000, 14.0000);
var salton = new L.LatLng(33.3000, -115.8000);
var texcoco = new L.LatLng(19.4333, -99.1333);
// trigger when map_pos go out of the screen
var edge = O.Edge(
O.Scroll().less('map_pos').offset(0),
O.Scroll().greater('map_pos').offset(0)
)
var TRIGGER_LINE = 400 + 50;
var story = O.Story()
.addEvent(
edge,
O.CSS($('#map_container')).toggleClass('attachTop')
)
.addState(
O.Scroll().within('intro').offset(TRIGGER_LINE),
map.actions.setView(intro, 2)
)
.addState(
O.Scroll().within('aral').offset(TRIGGER_LINE),
map.actions.setView(aral, 6)
)
.addState(
O.Scroll().within('chad').offset(TRIGGER_LINE),
map.actions.setView(chad, 7)
)
.addState(
O.Scroll().within('salton').offset(TRIGGER_LINE),
map.actions.setView(salton, 7)
)
.addState(
O.Scroll().within('texcoco').offset(TRIGGER_LINE),
map.actions.setView(texcoco, 9)
)
story.go(0);
</script>
</body>
</html>
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.O=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
var e = _dereq_('./lib/odyssey/story');
e.Actions = _dereq_('./lib/odyssey/actions');
e.Triggers = _dereq_('./lib/odyssey/triggers');
e.Core = _dereq_('./lib/odyssey/core');
e.Template = _dereq_('./lib/odyssey/template');
e.UI = _dereq_('./lib/odyssey/ui');
_dereq_('./lib/odyssey/util');
for (var k in e.Actions) {
e[k] = e.Actions[k];
}
for (var k in e.Triggers) {
e[k] = e.Triggers[k];
}
module.exports = e;
},{"./lib/odyssey/actions":5,"./lib/odyssey/core":13,"./lib/odyssey/story":14,"./lib/odyssey/template":15,"./lib/odyssey/triggers":17,"./lib/odyssey/ui":23,"./lib/odyssey/util":26}],2:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
function CSS(el) {
function _css() {};
_css.toggleClass = function(cl) {
return Action(function() {
el.toggleClass(cl);
});
};
_css.addClass = function(cl) {
return Action(function() {
el.addClass(cl);
});
};
_css.removeClass = function(cl) {
return Action(function() {
el.removeClass(cl);
});
};
return _css;
}
module.exports = CSS;
},{"../story":14}],3:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
//
// debug action
// prints information about current state
//
function Debug() {
function _debug() {};
_debug.log = function(_) {
return Action({
enter: function() {
console.log("STATE =>", _, arguments);
},
update: function() {
console.log("STATE (.)", _, arguments);
},
exit: function() {
console.log("STATE <=", _, arguments);
}
});
};
return _debug;
}
module.exports = Debug;
},{"../story":14}],4:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
var Audio = function(el){
return {
play: function() {
return Action(function() {
el.play()
});
},
pause: function() {
return Action(function() {
el.pause()
});
},
setCurrentTime: function(t) {
return Action(function() {
el.currentTime = t;
});
}
}
};
module.exports = Audio;
},{"../story":14}],5:[function(_dereq_,module,exports){
module.exports = {
Sleep: _dereq_('./sleep'),
Debug: _dereq_('./debug'),
Location: _dereq_('./location'),
Audio: _dereq_('./html5audio'),
Leaflet: {
Marker: _dereq_('./leaflet/marker'),
Map: _dereq_('./leaflet/map'),
Popup: _dereq_('./leaflet/popup')
},
CSS: _dereq_('./css'),
Slides: _dereq_('./slides'),
MiniProgressBar: _dereq_('./mini_progressbar')
};
},{"./css":2,"./debug":3,"./html5audio":4,"./leaflet/map":6,"./leaflet/marker":7,"./leaflet/popup":8,"./location":9,"./mini_progressbar":10,"./sleep":11,"./slides":12}],6:[function(_dereq_,module,exports){
var Action = _dereq_('../../story').Action;
function MapActions(map) {
function _map() {}
// helper method to translate leaflet methods to actions
function leaflet_method(name) {
_map[name] = function() {
var args = arguments;
return Action(function() {
map[name].apply(map, args);
});
};
}
function leaflet_move_method(name, signal) {
_map[name] = function() {
var args = arguments;
return Action({
enter: function() {
this.moveEnd = function() {
map.off(signal, this.moveEnd, this);
this.finish();
};
map.on(signal, this.moveEnd, this);
map[name].apply(map, args);
return true;
},
clear: function() {
map.off(signal, this.moveEnd, this);
}
});
};
}
_map.moveLinearTo = function(from, to, options) {
var opt = options || { k: 2 };
var animationTimer;
var posTarget;
var delta = 20;
// leaflet map animation is not being used because
// when the action is updated the map just stop the animatin
// and start again
// the animtion should be smooth
return Action({
enter: function () {
posTarget = map.project(map.getCenter());
animationTimer = setInterval(function() {
var c = map.project(map.getCenter());
var px = c.x + (posTarget.x - c.x)*delta*0.001* opt.k;
var py = c.y + (posTarget.y - c.y)*delta*0.001* opt.k;
map.panTo(map.unproject(L.point(px, py)), { animate: false });
}, delta);
},
update: function (t) {
var p0 = map.project(from);
var p1 = map.project(to);
posTarget = p0.add(p1.subtract(p0).multiplyBy(t));
},
exit: function() {
clearInterval(animationTimer);
},
clean: function() {
this.exit();
}
});
};
// leaflet methods
leaflet_move_method('panTo', 'moveend');
leaflet_move_method('setView', 'moveend');
leaflet_move_method('setZoom', 'zoomend');
return _map;
}
if (typeof window.L !== 'undefined') {
L.Map.addInitHook(function () {
this.actions = MapActions(this);
});
}
module.exports = MapActions;
},{"../../story":14}],7:[function(_dereq_,module,exports){
var Action = _dereq_('../../story').Action;
function MarkerActions(marker) {
function _marker() {}
_marker.addTo = function(map) {
return Action(function() {
marker.addTo(map);
});
};
_marker.addRemove = function(map) {
return Action({
enter: function() {
marker.addTo(map);
},
exit: function() {
map.removeLayer(marker);
},
clear: function() {
map.removeLayer(marker);
}
});
};
_marker.icon = function(iconEnabled, iconDisabled) {
iconEnabled = L.icon({
iconUrl: iconEnabled
});
iconDisabled = L.icon({
iconUrl: iconDisabled
});
return Action({
enter: function() {
marker.setIcon(iconEnabled);
},
exit: function() {
marker.setIcon(iconDisabled);
},
clear: function() { }
});
}
return _marker;
}
function PathActions(path) {
function _path() {};
_path.toggleStyle = function(styleDisabled, styleEnabled) {
return Action({
enter: function() {
path.setStyle(styleEnabled);
},
exit: function() {
path.setStyle(styleDisabled);
},
clear: function() { }
});
}
return _path;
}
if (typeof window.L !== 'undefined') {
L.Marker.addInitHook(function () {
this.actions = MarkerActions(this);
});
L.Path.addInitHook(function () {
this.actions = PathActions(this);
})
}
module.exports = MarkerActions;
//marker.actions.addTo(map);
//addState(, map.actions.moveTo(..).addMarker(m)
},{"../../story":14}],8:[function(_dereq_,module,exports){
/**
directional popup allows to create popups in the left and the right of a point,
not only on the top.
Same api than L.popup but you can spcify ``potision`` to 'left' or 'right'
*/
var Action = _dereq_('../../story').Action;
if (typeof window.L !== 'undefined') {
var DirectionalPopup = L.Popup.extend({
_updatePosition: function() {
L.Popup.prototype._updatePosition.call(this);
var offset = L.point(this.options.offset),
animated = this._animated;
switch(this.options.position) {
case 'left':
this._container.style.bottom = 'auto';
this._container.style.left = 'auto';
this._container.style.right = offset.x + (animated ? 0 : pos.x) + "px";
break;
case 'right':
this._container.style.bottom = 'auto';
this._container.style.left = offset.x + "px";
break;
}
}
});
L.DirectionalPopup = DirectionalPopup;
L.directionalPopup = function (options, source) {
return new L.DirectionalPopup(options, source);
};
function PopupActions(popup) {
function _popup() {}
// helper method to translate leaflet methods to actions
function leaflet_method(name) {
_popup[name] = function() {
var args = arguments;
return Action(function() {
popup[name].apply(popup, args);
});
};
}
// leaflet methods
leaflet_method('openOn');
_popup.openClose = function(map) {
if (!map) {
throw new Error("openClose gets map as first param");
}
return Action({
enter: function() {
popup.openOn(map);
},
exit: function() {
map.closePopup(popup);
}
});
};
return _popup;
}
L.Popup.addInitHook(function () {
this.actions = PopupActions(this);
});
}
},{"../../story":14}],9:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
var loc = window.location;
var Location = {
// changes the browser url hash
changeHash: function(hash) {
if (hash === undefined) throw new Error("hash should be a string");
return Action(function() {
loc.hash = hash;
});
}
};
module.exports = Location;
},{"../story":14}],10:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
/**
* mini progress var adds a small line to the top of the browser
* and moves according to story progress
* usage:
* var pg = MiniProgressBar()
* story.addAction(trigger, pg.percent(10)) //goes to 10%
*/
var MiniProgressBar = function(el) {
var defaultStyle = {
position: 'fixed',
left: 0,
top: 0,
height: '3px',
display: 'inline-block',
background: '#ff7373',
'z-index': 2
};
var pg = {};
// create an element and apply default style
var div = document.createElement('div');
div.setAttribute('class', 'oddysey-miniprogressbar');
for (var s in defaultStyle) {
div.style[s] = defaultStyle[s];
}
// append to element or to tge body
(el || document.body).appendChild(div);
/**
* returns an action that moves the percentaje bar to the specified one
*/
pg.percent = function(p) {
return Action(function() {
div.style.width = p + "%";
});
};
return pg;
}
module.exports = MiniProgressBar;
},{"../story":14}],11:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
function Sleep(ms) {
return Action({
enter: function() {
setTimeout(this.finish, ms);
return true;
}
});
}
module.exports = Sleep;
},{"../story":14}],12:[function(_dereq_,module,exports){
var Action = _dereq_('../story').Action;
var Core = _dereq_('../core');
var classList = _dereq_('../util/classList');
function Slides(el) {
function slides() {};
function _activate(idx) {
var slideElements = Core.getElement(el).children;
for(var i = 0; i < slideElements.length; ++i) {
if (i === idx) {
slideElements[i].classList.add("selected", "selected_slide");
} else {
slideElements[i].classList.remove("selected", "selected_slide");
}
}
}
slides.activate = function(i) {
return Action(function() {
_activate(i);
});
};
_activate(-1);
return slides;
}
module.exports = Slides;
},{"../core":13,"../story":14,"../util/classList":24}],13:[function(_dereq_,module,exports){
function getElement(el) {
if(typeof jQuery !== 'undefined') {
if (el instanceof jQuery) {
return el[0];
} else if(typeof el === 'string') {
if (el[0] === '#' || el[0] === '.') {
return getElement($(el));
}
}
}
if (el instanceof NodeList || el instanceof HTMLCollection) {
return el[0];
} else if (el instanceof Element) {
return el;
}
return document.getElementById(el);
}
module.exports = {
getElement: getElement
};
},{}],14:[function(_dereq_,module,exports){
_dereq_('../../vendor/d3.custom');
function Story() {
var triggers = [];
var events = [];
var currentState = null;
var prevState = null;
function story(t) {
}
// event non attached to states
story.addEvent = function(trigger, action) {
trigger._story(story, function() {
action.enter();
});
events.push({
a: trigger,
b: action
});
return story;
};
story.clear = function() {
var all = triggers.concat(events);
for(var i = 0; i < all.length; ++i) {
var a = all[i];
a.a.story = null;
a.a.trigger = null;
a.a.clear && a.a.clear();
a.b.clear && a.b.clear();
}
triggers = [];
events = [];
currentState = null;
prevState = null;
};
// go to state index
story.go = function(index, opts) {
opts = opts || {};
if(index < 0 && index > triggers.length) {
throw new Error("index should be less than states length");
}
if (story.state() !== index) {
if (opts.reverse) {
var a = triggers[index].a;
if (a.reverse) {
a.reverse();
}
}
// current state
story.state(index);
// raise exit
if (prevState !== null) {
var prev = triggers[prevState].b;
if (prev.exit) {
prev.exit();
}
}
var b = triggers[index].b;
// enter in current state
b.enter();
}
};
story.addState = function(a, b, opts) {
var i = triggers.length;
if(!a || !b) {
throw new Error("action and trigger must be defined");
}
triggers.push({
a: a,
b: b,
opts: opts
});
a._story(story, function() {
story.go(i);
});
return story;
};
story.addLinearState = function(a, b, opts) {
var j;
var i = triggers.length;
triggers.push({
a: a,
b: b,
opts: opts
});
a._story(story, function(t) {
if (story.state() !== i) {
story.go(i);
} else {
if (b.update) {
b.update(t);
}
}
});
return story;
};
story.state = function(_) {
if(_ === undefined) return currentState;
prevState = currentState;
currentState = _;
return;
};
return story;
}
//
// basic action
// t can be a function or an object
// if is a function it's called on ``enter`` event
// if t is an object with enter/exit/update methods
// they're called on state changes
function Action(t) {
var evt = d3.dispatch('finish');
var action = t;
if (t.enter === undefined && !(typeof(t) === 'function' && t.prototype.enter !== undefined)) {
action = {
enter: t
}
}
return d3.rebind(action, evt, 'on', 'finish');
}
function Trigger(t) {
t = t || {}
t._story = function(story, trigger) {
this.trigger = trigger;
this.story = story;
};
t.then = function(t, context) {
this.trigger = function() {
if (t.trigger) {
t.trigger();
if (t.reverse) t.reverse();
} else if (t.call) {
t.call(context || self);
} else {
throw new Error("then first param should be either function or trigger");
}
};
};
return t;
}
///
// executes actions in parallel
// usage:
// Parallel(action1, action2, action3)
//
// raises finish when all the tasks has been completed
//
function Parallel () {
var actions = Array.prototype.slice.call(arguments);
var tasksLeft;
function _Parallel() {}
function start() {
tasksLeft = actions.length;
}
function done() {
if (--tasksLeft === 0) {
_Parallel.finish();
}
}
function wait(action) {
action.on('finish.parallel', function() {
action.on('finish.parallel', null);
done();
});
}
_Parallel.enter = function() {
start();
for(var i = 0, len = actions.length; i < len; ++i) {
var a = actions[i];
if (a.enter) {
if (a.enter()) {
wait(a);
} else {
done();
}
}
}
};
_Parallel.exit = function() {
start();
for(var i = actions.length - 1; i >= 0; --i) {
var a = actions[i];
if (a.exit) {
if (a.exit()) {
wait(a);
} else {
done();
}
}
}
};
_Parallel.clear = function() {
for(var i = 0, len = actions.length; i < len; ++i) {
var a = actions[i];
a.clear && a.clear();
}
}
_Parallel = Action(_Parallel);
return _Parallel;
}
///
// executes actions serially, waits until the previous task
// is completed to start with the second and so on
// usage:
// Step(action1, action2, action3)
//
// raises finish when all the tasks has been completed
//
function Step() {
var actions = Array.prototype.slice.call(arguments);
var queue;
function _Step() {}
function next(method) {
if (queue.length === 0) {
_Step.finish();
return;
}
var a = queue.pop();
if (a.on) {
a.on('finish.chain', function() {
a.on('finish.chain', null);
next(method);
});
}
if (!a[method] || !a[method]()) {
next(method);
if(a.on) a.on('finish.chain', null);
}
}
_Step.enter = function() {
// call enter on each action
queue = actions.slice().reverse();
next('enter');
return true;
}
_Step.exit = function() {
// call exit on each action
queue = actions.slice();
next('exit');
return true;
}
_Step.clear = function() {
for(var i = 0, len = actions.length; i < len; ++i) {
var a = actions[i];
a.clear && a.clear();
}
}
_Step = Action(_Step);
return _Step;
}
// check change between two states and triggers
function Edge(a, b) {
var s = 0;
function t() {}
a._story(null, function() {
if(s !== 0) {
t.trigger();
}
s = 0;
});
b._story(null, function() {
if(s !== 1) {
t.trigger();
}
s = 1;
});
return Trigger(t);
}
module.exports = {
Story: Story,
Action: Action,
Trigger: Trigger,
Step: Step,
Parallel: Parallel,
Edge: Edge
};
},{"../../vendor/d3.custom":32}],15:[function(_dereq_,module,exports){
_dereq_('../../vendor/markdown');
var mapActions = {
'move map to current position': function() {
var center = this.map.getCenter()
return '- center: [' + center.lat.toFixed(4) + ', ' + center.lng.toFixed(4) + ']\n- zoom: ' + this.map.getZoom()
},
'show marker at current position': function() {
var center = this.map.getCenter()
return 'L.marker([' + center.lat.toFixed(4) + ', ' + center.lng.toFixed(4) + ']).actions.addRemove(S.map)';
},
'sleep': function() {
return "O.Actions.Sleep(1000)";
}
};
var Template = function(template) {
var initialized = false;
function readMessage(event) {
var msg = JSON.parse(event.data);
template.editor = true;
if (!initialized) {
configureEditor();
initialized = true;
}
function sendMsg(_) {
event.source.postMessage(JSON.stringify({
id: msg.id,
data: _
}), event.currentTarget ? event.currentTarget.location : event.source.location);
}
if (msg.type === 'md') {
var actions = actionsFromMarkdown(msg.code);
try {
template.update(actions);
sendMsg(null);
} catch(e) {
sendMsg(e.message);
}
} else if (msg.type === 'actions') {
var actions = [];
if (template.map && template.map instanceof L.Map) {
for (var k in mapActions) {
actions.push(k);
}
}
if (template.actions) {
for (var k in template.actions) {
actions.push(k);
}
}
sendMsg(actions);
} else if (msg.type === 'get_action') {
if (msg.code in mapActions) {
sendMsg(mapActions[msg.code].call(template));
} else {
sendMsg(template.actions[msg.code].call(template));
}
} else if (msg.type === 'code') {
sendMsg(eval(msg.code));
} else if (msg.type === 'change_slide') {
template.changeSlide && template.changeSlide(msg.slide);
}
}
if (!window.addEventListener) {
window.attachEvent("message", function load(event) {
readMessage(event);
});
} else {
window.addEventListener("message", function load(event) {
readMessage(event);
});
}
template.init(function() { });
function configureEditor() {
// add helpers
if (template.map && template.map instanceof L.Map) {
new L.CrossHair('//cartodb.github.io/odyssey.js/img/crosshair.png').addTo(template.map);
}
}
window.onload = function() {
var origin = location.pathname.split('/')[1];
Template.Storage.load(function(md) {
template.update(actionsFromMarkdown(md));
});
}
};
Template.Storage = {
save: function(md, template) {
location.hash = "md/" + template + "/" + btoa(md);
},
load: function(done) {
if (document.getElementById('md_template')) {
done(document.getElementById('md_template').text);
return;
}
var h = location.hash;
if (done && h) {
// #md/template/base_64_markdown
var tk = h.split('/');
if (tk[0] === '#md') {
done(atob(tk.slice(2).join('/')), tk[1]);
}
}
}
};
function Slide(tree, actions, prop) {
var html;
var md_tree = tree;
var properties = prop;
var action;
function compile(context) {
propertiesToActions();
//sort actions by order
actions.sort(function(a, b) {
return a.order - b.order;
});
if (!('map' in Array.prototype)) {
Array.prototype.map= function(mapper, that /*opt*/) {
var other= new Array(this.length);
for (var i= 0, n= this.length; i<n; i++)
if (i in this)
other[i]= mapper.call(that, this[i], i, this);
return other;
};
}
action = O.Step.apply(window,
actions.map(function(a) {
var o = a.cmd;
try {
var f = Function("S", "return " + o + ";");
} catch (e) {
throw new Error("error while compiling " + o + ": " + e.message);
}
return f(context);
})
);
}
/*
* there are special propertues like pos, marker and so
* on that should become actions
*/
function propertiesToActions() {
for (var k in properties) {
var prop = properties[k];
switch (k) {
case 'center':
var pos = prop.value;
if (properties.zoom === undefined) {
actions.push({
order: prop.order,
cmd: 'S.map.actions.panTo([' + pos[0] + ',' + pos[1] + '])'
});
} else {
actions.push({
order: prop.order,
cmd: 'S.map.actions.setView([' + pos[0] + ',' + pos[1] + '], ' + properties.zoom.value + ')'
});
}
case 'zoom':
if (!properties.center) {
actions.push({
order: prop.order,
cmd: 'S.map.actions.setZoom(' + properties.zoom.value+ ')'
});
}
}
}
}
function _slide(context) {
if (!action) compile(context);
return action;
}
_slide.html = function() {
if (!html) {
html = markdown.toHTML(['markdown'].concat(md_tree));
}
return html;
};
_slide.props = function() {
return properties || {}
};
_slide.get = function(_) {
return (properties || {})[_];
}
return _slide;
}
function trim(x) {
return x.replace(/^\s+|\s+$/gm,'');
}
var prop_re = /^-([^:]+):(.*)/;
function parseProperties(code) {
var props = {};
var lines = code.split('\n');
for (var i = 0; i < lines.length; ++i) {
var e = prop_re.exec(lines[i]);
if (e) {
try {
props[trim(e[1])] = JSON.parse(trim(e[2]));
} catch(e) {
}
}
}
return props;
}
/**
* given a markdown parsed tree return a list of slides with
* html and actions
*/
function md2json(tree) {
var slide = null;
var slides = [];
var globalProperties = {}
var elements = tree.slice(1);
for (var i = 0; i < elements.length; ++i) {
var el = elements[i];
var add = true;
if (el[0] === 'header' && el[1].level === 1) {
if (slide) slides.push(Slide(slide.md_tree, slide.actions, slide.properties));
slide = {
md_tree: [],
html: '',
actions: [],
properties: {}
};
} else if (el[0] === 'para') {
// search inline code inside it
var actions = [];
for(var j = 1; j < el.length; ++j) {
var subel = el[j];
if (subel[0] === "inlinecode") {
var lines = subel[1].split('\n');
for (var ln = 0; ln < lines.length; ++ln) {
var line = lines[ln];
if (trim(line) !== '') {
// if matches property, parse it
if (prop_re.exec(line)) {
var p = parseProperties(subel[1]);
var prop = slide ? slide.properties: globalProperties
for(var k in p) {
prop[k] = {
order: ln,
value: p[k]
}
}
} else {
slide && slide.actions.push({
cmd: line,
order: ln
});
}
}
}
// remove from list
el.splice(j, 1);
--j;
}
}
}
add && slide && slide.md_tree.push(el);
}
if (slide) slides.push(Slide(slide.md_tree, slide.actions, slide.properties));
// remove the order for globalProperties
slides.global = {};
for(var k in globalProperties) {
slides.global[k] = globalProperties[k].value;
}
return slides;
}
// return a list of Slide objects
function actionsFromMarkdown(md) {
var md_tree = markdown.parse(md);
return md2json(md_tree);
}
Template.parseProperties = parseProperties;
module.exports = Template;
},{"../../vendor/markdown":33}],16:[function(_dereq_,module,exports){
var Trigger = _dereq_('../story').Trigger;
var Core = _dereq_('../core');
var Hammer = _dereq_('hammerjs');
function Gestures(el) {
var gestures = {};
el = el || document;
function _hammerEvent(eventName, fnName) {
gestures[fnName]= function() {
var t = Trigger({});
Hammer(el).on(eventName, function() {
t.trigger();
});
return t;
};
}
_hammerEvent('swipeup', 'swipeUp');
_hammerEvent('swipedown', 'swipeDown');
_hammerEvent('swipeleft', 'swipeLeft');
_hammerEvent('swiperight', 'swipeRight');
return gestures;
}
module.exports = Gestures;
},{"../core":13,"../story":14,"hammerjs":31}],17:[function(_dereq_,module,exports){
module.exports = {
Scroll: _dereq_('./scroll'),
Sequential: _dereq_('./sequential'),
Keys: _dereq_('./keys'),
Video: _dereq_('./video'),
Gestures: _dereq_('./gestures')
};
},{"./gestures":16,"./keys":18,"./scroll":19,"./sequential":20,"./video":21}],18:[function(_dereq_,module,exports){
var Trigger = _dereq_('../story').Trigger;
var Core = _dereq_('../core');
/**
*
*
*/
function Keys() {
KEY_LEFT = 37;
KEY_UP = 38;
KEY_RIGHT = 39;
KEY_DOWN = 40;
var el = document;
var keys = {};
function listenForKey(el, k, callback) {
function keyDown(e) {
e = e || window.event;
var code = e.keyCode;
if (code === k) {
callback();
e.preventDefault ? e.preventDefault() : e.returnValue = false;
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
}
}
if (!window.addEventListener) {
el.attachEvent("onkeydown", function load(event) {
keyDown(event);
});
} else {
el.addEventListener("keydown", function load(event) {
keyDown(event);
});
}
}
keys.left = function() {
var t = Trigger({});
listenForKey(el, KEY_LEFT, function() {
t.trigger();
});
return t;
}
keys.right = function() {
var t = Trigger({});
listenForKey(el, KEY_RIGHT, function() {
t.trigger();
});
return t;
}
keys.on = function(element) {
el = Core.getElement(element);
return keys;
}
return keys;
}
module.exports = Keys;
},{"../core":13,"../story":14}],19:[function(_dereq_,module,exports){
var Trigger = _dereq_('../story').Trigger;
var Core = _dereq_('../core');
function cte(c) { return function() { return c; } }
//TODO: add support for elements != window
function Scroll() {
var scroller = window;
var scrolls = [];
var initialized = false;
var offset = cte(0);
var condition = null;
function scroll() {}
scroll.reach = function(el) {
function _reach () {}
Trigger(_reach);
_reach.scroll = function(scrollY) {
var e = Core.getElement(el);
var bounds = e.getBoundingClientRect();
var offset = _reach.offset();
var t = condition(bounds, offset, scrollY);
if (t !== null && t !== undefined) {
_reach.trigger(t);
}
return _reach;
};
_reach.condition = function(_) {
if (!arguments.length) {
return condition;
}
condition = _;
}
/// sets offset in px or % of element
// offset('50%') offset(100)
_reach.offset = function(_) {
if (!arguments.length) {
return offset();
}
if (typeof(_) === 'number') {
offset = cte(_);
} else {
offset = function() {
//remove %
var percent = +_.replace('%', '');
return scroller.innerHeight * percent * 0.01;
}
}
return _reach;
}
_reach.reverse = function() {
var e = document.getElementById(el);
var bounds = e.getBoundingClientRect();
var offset = _reach.offset();
scroller.scrollTo(0, bounds.top - offset);
};
_reach.clear = function() {
unregister(_reach);
}
// add to working scrolls
register(_reach);
return _reach;
};
scroll.within = function(el) {
var r = scroll.reach(el);
r.condition(function(bounds, offset) {
if(bounds.top <= offset && bounds.bottom >= offset) {
var t = (offset - bounds.top)/(bounds.bottom - bounds.top);
return t;
}
});
return r;
};
scroll.less = function(el, opt) {
opt = opt || {};
var r = scroll.reach(el);
var fixedBoundsTop;
if (opt.fixed) {
var e = Core.getElement(el);
fixedBoundsTop = scroller.scrollY + e.getBoundingClientRect().top;
}
r.condition(function(bounds, offset, scrollY) {
var t = opt.fixed ? fixedBoundsTop: bounds.top;
var o = opt.fixed ? scrollY: offset;
if(t >= o) {
return 0;
}
});
return r;
};
scroll.greater = function(el, opt) {
opt = opt || {};
var r = scroll.reach(el);
var fixedBoundsTop;
if (opt.fixed) {
var e = Core.getElement(el);
fixedBoundsTop = scroller.scrollY + e.getBoundingClientRect().top;
}
r.condition(function(bounds, offset, scrollY) {
var t = opt.fixed ? fixedBoundsTop: bounds.top;
var o = opt.fixed ? scrollY: offset;
if(t <= o) {
return 0;
}
});
return r;
};
function register(s) {
scrolls.push(s);
initScroll();
}
function unregister(s) {
var i = scrolls.indexOf(s);
if (i >= 0) {
scrolls.splice(i, 1);
}
}
function initScroll() {
if (!initialized) {
initialized = true;
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (callback, thisArg) {
var T, k;
if (this == null) {
throw new TypeError(" this is null or not defined");
}
// 1. Let O be the result of calling ToObject passing the |this| value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (thisArg) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
var kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with argument Pk.
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
function scrollEach() {
scrolls.forEach(function(s) {
s.scroll(window.scrollY);
});
}
if (!window.addEventListener) {
scroller.attachEvent("onscroll", function load(event) {
scrollEach();
});
} else {
window.addEventListener("scroll", function load(event) {
scrollEach();
});
}
}
}
return scroll;
}
Scroll._scrolls = [];
module.exports = Scroll;
},{"../core":13,"../story":14}],20:[function(_dereq_,module,exports){
var Trigger = _dereq_('../story').Trigger;
function Sequential() {
var current = 0;
var steps = [];
var max = 0;
var triggers = {};
function seq() {}
function update() {
for (var i = 0; i < steps.length; ++i) {
steps[i].check();
}
}
seq.state = seq.step = function(n) {
if (n in triggers) {
return triggers[n];
}
var t = Trigger({
check: function() {
if (n === current && this.trigger) this.trigger();
},
reverse: function() {
current = n;
}
});
max = Math.max(max, n);
steps.push(t);
return triggers[n] = t;
};
seq.next = function() {
current += 1;
if (current > max) {
current = 0;
}
update();
};
seq.clear = function() {
steps = [];
max = 0;
current = 0;
return seq;
};
seq.current = function(_) {
if (_ !== undefined) {
var c = Math.max(Math.min(max, _), 0);
if (c !== current) {
current = c;
update();
}
return this;
}
return current;
};
seq.prev = function() {
current -= 1;
if (current < 0) {
current = max;
}
update();
};
return seq;
}
module.exports = Sequential;
},{"../story":14}],21:[function(_dereq_,module,exports){
function Video(player) {
if (typeof YT === 'undefined' || !(player instanceof YT.Player)) {
throw new Error("player should be a YT.Player instance, see youtube API");
}
var triggers = [];
var i = setInterval(function() {
var seconds = player.getCurrentTime();
for (var i = 0; i < triggers.length; ++i) {
var t = triggers[i];
if (t.start <= seconds && t.end > seconds) {
t.trigger((seconds - t.start)/(t.end - t.start));
}
}
}, 100);
var clear = function(t) {
if (triggers.length === 0) {
clearInterval(i);
}
};
return {
between: function(start, end) {
var t = O.Trigger();
t.start = start;
t.end = end;
triggers.push(t);
t.clear = function() {
triggers.splice(triggers.indexOf(t), 1);
clear();
};
return t;
}
}
}
module.exports = Video
},{}],22:[function(_dereq_,module,exports){
/**
# dot progress
ui widget that controls dot progress
## usage
in order to use it you need to instanciate using a container, so for example:
<div id="dots"></div>
...
var progress = DotProgress('dots')
// we set the number of slides
progress.count(10);
// we can activate it as an action
// then the story enters in this state the second dot
// will be activated
story.addState(trigger, progress.activate(1))
// when an user clicks on the dot it can trigger an action
story.addState(progress.step(1), action);
## styling
the html rendered is the following:
```
<div id="dots">
<ul>
<li><href="#0"></a></li>
<li><href="#1" class="active"></a></li>
<li><href="#2"></a></li>
<li><href="#2"></a></li>
</ul>
</div>
```
so you can use active class to style the active one
*/
var Core = _dereq_('../core');
function DotProgress(el) {
var count = 0;
var element = Core.getElement(el);
var triggers = {};
function _progress() {
return _progress;
}
function render() {
var html = '<ul>';
for(var i = 0; i < count; ++i) {
html += '<li><a slide-index="' + i + '" href="#' + i + '"></a></li>';
}
html += "</ul>";
element.innerHTML = html;
}
_progress.count = function(_) {
count = _;
render();
return _progress;
};
// returns an action to activate the index
_progress.activate = function(activeIndex) {
return O.Action(function () {
var children = element.children[0].children;
for(var i = 0; i < children.length; ++i) {
children[i].children[0].setAttribute('class', '');
}
children[activeIndex].children[0].setAttribute('class', 'active');
});
};
element.onclick = function(e) {
e = e || window.event;
var idx = (e.target || e.srcElement).getAttribute('slide-index');
var t = triggers[idx];
if (t) {
t.trigger();
}
};
_progress.step = function(i) {
var t = O.Trigger();
triggers[i] = t;
return t;
};
return _progress;
}
module.exports = DotProgress;
},{"../core":13}],23:[function(_dereq_,module,exports){
module.exports = {
DotProgress: _dereq_('./dotprogress')
}
},{"./dotprogress":22}],24:[function(_dereq_,module,exports){
/*
* classList.js: Cross-browser full element.classList implementation.
* 2014-01-31
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*/
/*global self, document, DOMException */
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
if ("document" in self && !("classList" in document.createElement("_"))) {
(function (view) {
"use strict";
if (!('Element' in view)) return;
var
classListProp = "classList"
, protoProp = "prototype"
, elemCtrProto = view.Element[protoProp]
, objCtr = Object
, strTrim = String[protoProp].trim || function () {
return this.replace(/^\s+|\s+$/g, "");
}
, arrIndexOf = Array[protoProp].indexOf || function (item) {
var
i = 0
, len = this.length
;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
}
// Vendors: please allow content code to instantiate DOMExceptions
, DOMEx = function (type, message) {
this.name = type;
this.code = DOMException[type];
this.message = message;
}
, checkTokenAndGetIndex = function (classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR"
, "An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR"
, "String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
}
, ClassList = function (elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
, i = 0
, len = classes.length
;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function () {
elem.setAttribute("class", this.toString());
};
}
, classListProto = ClassList[protoProp] = []
, classListGetter = function () {
return new ClassList(this);
}
;
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function (i) {
return this[i] || null;
};
classListProto.contains = function (token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
;
do {
token = tokens[i] + "";
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
;
do {
token = tokens[i] + "";
var index = checkTokenAndGetIndex(this, token);
if (index !== -1) {
this.splice(index, 1);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function (token, force) {
token += "";
var
result = this.contains(token)
, method = result ?
force !== true && "remove"
:
force !== false && "add"
;
if (method) {
this[method](token);
}
return !result;
};
classListProto.toString = function () {
return this.join(" ");
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter
, enumerable: true
, configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
if (ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(self));
}
},{}],25:[function(_dereq_,module,exports){
/**
* new L.CrossHair('http://image.com/image', { x: 10, y :10 }).addTo(map)
*/
if (typeof window.L !== 'undefined') {
L.CrossHair = L.Control.extend({
initialize: function(img, size) {
this.image = new Image();
this.image.onload = L.bind(this._updatePos, this);
this.image.src = img;
if (size) {
this.image.width = size.x;
this.image.height = size.y;
}
},
_updatePos: function() {
if (this._map) {
var w = this.image.width >> 1;
var h = this.image.height >> 1;
w = this._map.getSize().x/2.0 - w;
h = this._map.getSize().y/2.0 - h;
L.DomUtil.setPosition(this.image, { x: w, y: h });
}
},
addTo: function(map) {
var r = L.Control.prototype.addTo.call(this, map);
// remove leaflet-top and leaflet-right classes
this.image.parentNode.setAttribute('class', '');
map.on('resize', L.bind(this._updatePos, this));
return r;
},
onRemove: function(map) {
map.off('resize', null, this);
},
onAdd: function(map) {
this._updatePos();
return this.image;
}
});
}
},{}],26:[function(_dereq_,module,exports){
module.exports = {
CrossHair: _dereq_('./crosshair')
}
},{"./crosshair":25}],27:[function(_dereq_,module,exports){
if (typeof Object.create === 'function') {
// implementation from standard node.js 'util' module
module.exports = function inherits(ctor, superCtor) {
ctor.super_ = superCtor
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
} else {
// old school shim for old browsers
module.exports = function inherits(ctor, superCtor) {
ctor.super_ = superCtor
var TempCtor = function () {}
TempCtor.prototype = superCtor.prototype
ctor.prototype = new TempCtor()
ctor.prototype.constructor = ctor
}
}
},{}],28:[function(_dereq_,module,exports){
// shim for using process in browser
var process = module.exports = {};
process.nextTick = (function () {
var canSetImmediate = typeof window !== 'undefined'
&& window.setImmediate;
var canPost = typeof window !== 'undefined'
&& window.postMessage && window.addEventListener
;
if (canSetImmediate) {
return function (f) { return window.setImmediate(f) };
}
if (canPost) {
var queue = [];
window.addEventListener('message', function (ev) {
var source = ev.source;
if ((source === window || source === null) && ev.data === 'process-tick') {
ev.stopPropagation();
if (queue.length > 0) {
var fn = queue.shift();
fn();
}
}
}, true);
return function nextTick(fn) {
queue.push(fn);
window.postMessage('process-tick', '*');
};
}
return function nextTick(fn) {
setTimeout(fn, 0);
};
})();
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
function noop() {}
process.on = noop;
process.addListener = noop;
process.once = noop;
process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.binding = function (name) {
throw new Error('process.binding is not supported');
}
// TODO(shtylman)
process.cwd = function () { return '/' };
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
},{}],29:[function(_dereq_,module,exports){
module.exports = function isBuffer(arg) {
return arg && typeof arg === 'object'
&& typeof arg.copy === 'function'
&& typeof arg.fill === 'function'
&& typeof arg.readUInt8 === 'function';
}
},{}],30:[function(_dereq_,module,exports){
(function (process,global){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var formatRegExp = /%[sdj%]/g;
exports.format = function(f) {
if (!isString(f)) {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function(x) {
if (x === '%%') return '%';
if (i >= len) return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j':
try {
return JSON.stringify(args[i++]);
} catch (_) {
return '[Circular]';
}
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (isNull(x) || !isObject(x)) {
str += ' ' + x;
} else {
str += ' ' + inspect(x);
}
}
return str;
};
// Mark that a method should not be used.
// Returns a modified function which warns once by default.
// If --no-deprecation is set, then it is a no-op.
exports.deprecate = function(fn, msg) {
// Allow for deprecating things in the process of starting up.
if (isUndefined(global.process)) {
return function() {
return exports.deprecate(fn, msg).apply(this, arguments);
};
}
if (process.noDeprecation === true) {
return fn;
}
var warned = false;
function deprecated() {
if (!warned) {
if (process.throwDeprecation) {
throw new Error(msg);
} else if (process.traceDeprecation) {
console.trace(msg);
} else {
console.error(msg);
}
warned = true;
}
return fn.apply(this, arguments);
}
return deprecated;
};
var debugs = {};
var debugEnviron;
exports.debuglog = function(set) {
if (isUndefined(debugEnviron))
debugEnviron = process.env.NODE_DEBUG || '';
set = set.toUpperCase();
if (!debugs[set]) {
if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
var pid = process.pid;
debugs[set] = function() {
var msg = exports.format.apply(exports, arguments);
console.error('%s %d: %s', set, pid, msg);
};
} else {
debugs[set] = function() {};
}
}
return debugs[set];
};
/**
* Echos the value of a value. Trys to print the value out
* in the best way possible given the different types.
*
* @param {Object} obj The object to print out.
* @param {Object} opts Optional options object that alters the output.
*/
/* legacy: obj, showHidden, depth, colors*/
function inspect(obj, opts) {
// default options
var ctx = {
seen: [],
stylize: stylizeNoColor
};
// legacy...
if (arguments.length >= 3) ctx.depth = arguments[2];
if (arguments.length >= 4) ctx.colors = arguments[3];
if (isBoolean(opts)) {
// legacy...
ctx.showHidden = opts;
} else if (opts) {
// got an "options" object
exports._extend(ctx, opts);
}
// set default options
if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
if (isUndefined(ctx.depth)) ctx.depth = 2;
if (isUndefined(ctx.colors)) ctx.colors = false;
if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
if (ctx.colors) ctx.stylize = stylizeWithColor;
return formatValue(ctx, obj, ctx.depth);
}
exports.inspect = inspect;
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
inspect.colors = {
'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39]
};
// Don't use 'blue' not visible on cmd.exe
inspect.styles = {
'special': 'cyan',
'number': 'yellow',
'boolean': 'yellow',
'undefined': 'grey',
'null': 'bold',
'string': 'green',
'date': 'magenta',
// "name": intentionally not styling
'regexp': 'red'
};
function stylizeWithColor(str, styleType) {
var style = inspect.styles[styleType];
if (style) {
return '\u001b[' + inspect.colors[style][0] + 'm' + str +
'\u001b[' + inspect.colors[style][1] + 'm';
} else {
return str;
}
}
function stylizeNoColor(str, styleType) {
return str;
}
function arrayToHash(array) {
var hash = {};
array.forEach(function(val, idx) {
hash[val] = true;
});
return hash;
}
function formatValue(ctx, value, recurseTimes) {
// Provide a hook for user-specified inspect functions.
// Check that value is an object with an inspect function on it
if (ctx.customInspect &&
value &&
isFunction(value.inspect) &&
// Filter out the util module, it's inspect function is special
value.inspect !== exports.inspect &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
var ret = value.inspect(recurseTimes, ctx);
if (!isString(ret)) {
ret = formatValue(ctx, ret, recurseTimes);
}
return ret;
}
// Primitive types cannot have properties
var primitive = formatPrimitive(ctx, value);
if (primitive) {
return primitive;
}
// Look up the keys of the object.
var keys = Object.keys(value);
var visibleKeys = arrayToHash(keys);
if (ctx.showHidden) {
keys = Object.getOwnPropertyNames(value);
}
// IE doesn't make error fields non-enumerable
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
if (isError(value)
&& (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
return formatError(value);
}
// Some type of object without properties can be shortcutted.
if (keys.length === 0) {
if (isFunction(value)) {
var name = value.name ? ': ' + value.name : '';
return ctx.stylize('[Function' + name + ']', 'special');
}
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
}
if (isDate(value)) {
return ctx.stylize(Date.prototype.toString.call(value), 'date');
}
if (isError(value)) {
return formatError(value);
}
}
var base = '', array = false, braces = ['{', '}'];
// Make Array say that they are Array
if (isArray(value)) {
array = true;
braces = ['[', ']'];
}
// Make functions say that they are functions
if (isFunction(value)) {
var n = value.name ? ': ' + value.name : '';
base = ' [Function' + n + ']';
}
// Make RegExps say that they are RegExps
if (isRegExp(value)) {
base = ' ' + RegExp.prototype.toString.call(value);
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + Date.prototype.toUTCString.call(value);
}
// Make error with message first say the error
if (isError(value)) {
base = ' ' + formatError(value);
}
if (keys.length === 0 && (!array || value.length == 0)) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
} else {
return ctx.stylize('[Object]', 'special');
}
}
ctx.seen.push(value);
var output;
if (array) {
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
} else {
output = keys.map(function(key) {
return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
});
}
ctx.seen.pop();
return reduceToSingleString(output, base, braces);
}
function formatPrimitive(ctx, value) {
if (isUndefined(value))
return ctx.stylize('undefined', 'undefined');
if (isString(value)) {
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') + '\'';
return ctx.stylize(simple, 'string');
}
if (isNumber(value))
return ctx.stylize('' + value, 'number');
if (isBoolean(value))
return ctx.stylize('' + value, 'boolean');
// For some reason typeof null is "object", so special case here.
if (isNull(value))
return ctx.stylize('null', 'null');
}
function formatError(value) {
return '[' + Error.prototype.toString.call(value) + ']';
}
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
var output = [];
for (var i = 0, l = value.length; i < l; ++i) {
if (hasOwnProperty(value, String(i))) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
String(i), true));
} else {
output.push('');
}
}
keys.forEach(function(key) {
if (!key.match(/^\d+$/)) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
key, true));
}
});
return output;
}
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
var name, str, desc;
desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
if (desc.get) {
if (desc.set) {
str = ctx.stylize('[Getter/Setter]', 'special');
} else {
str = ctx.stylize('[Getter]', 'special');
}
} else {
if (desc.set) {
str = ctx.stylize('[Setter]', 'special');
}
}
if (!hasOwnProperty(visibleKeys, key)) {
name = '[' + key + ']';
}
if (!str) {
if (ctx.seen.indexOf(desc.value) < 0) {
if (isNull(recurseTimes)) {
str = formatValue(ctx, desc.value, null);
} else {
str = formatValue(ctx, desc.value, recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (array) {
str = str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n').substr(2);
} else {
str = '\n' + str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n');
}
}
} else {
str = ctx.stylize('[Circular]', 'special');
}
}
if (isUndefined(name)) {
if (array && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.substr(1, name.length - 2);
name = ctx.stylize(name, 'name');
} else {
name = name.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = ctx.stylize(name, 'string');
}
}
return name + ': ' + str;
}
function reduceToSingleString(output, base, braces) {
var numLinesEst = 0;
var length = output.reduce(function(prev, cur) {
numLinesEst++;
if (cur.indexOf('\n') >= 0) numLinesEst++;
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
}, 0);
if (length > 60) {
return braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1];
}
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(ar) {
return Array.isArray(ar);
}
exports.isArray = isArray;
function isBoolean(arg) {
return typeof arg === 'boolean';
}
exports.isBoolean = isBoolean;
function isNull(arg) {
return arg === null;
}
exports.isNull = isNull;
function isNullOrUndefined(arg) {
return arg == null;
}
exports.isNullOrUndefined = isNullOrUndefined;
function isNumber(arg) {
return typeof arg === 'number';
}
exports.isNumber = isNumber;
function isString(arg) {
return typeof arg === 'string';
}
exports.isString = isString;
function isSymbol(arg) {
return typeof arg === 'symbol';
}
exports.isSymbol = isSymbol;
function isUndefined(arg) {
return arg === void 0;
}
exports.isUndefined = isUndefined;
function isRegExp(re) {
return isObject(re) && objectToString(re) === '[object RegExp]';
}
exports.isRegExp = isRegExp;
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
exports.isObject = isObject;
function isDate(d) {
return isObject(d) && objectToString(d) === '[object Date]';
}
exports.isDate = isDate;
function isError(e) {
return isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error);
}
exports.isError = isError;
function isFunction(arg) {
return typeof arg === 'function';
}
exports.isFunction = isFunction;
function isPrimitive(arg) {
return arg === null ||
typeof arg === 'boolean' ||
typeof arg === 'number' ||
typeof arg === 'string' ||
typeof arg === 'symbol' || // ES6 symbol
typeof arg === 'undefined';
}
exports.isPrimitive = isPrimitive;
exports.isBuffer = _dereq_('./support/isBuffer');
function objectToString(o) {
return Object.prototype.toString.call(o);
}
function pad(n) {
return n < 10 ? '0' + n.toString(10) : n.toString(10);
}
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec'];
// 26 Feb 16:19:34
function timestamp() {
var d = new Date();
var time = [pad(d.getHours()),
pad(d.getMinutes()),
pad(d.getSeconds())].join(':');
return [d.getDate(), months[d.getMonth()], time].join(' ');
}
// log is just a thin wrapper to console.log that prepends a timestamp
exports.log = function() {
console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
};
/**
* Inherit the prototype methods from one constructor into another.
*
* The Function.prototype.inherits from lang.js rewritten as a standalone
* function (not on Function.prototype). NOTE: If this file is to be loaded
* during bootstrapping this function needs to be rewritten using some native
* functions as prototype setup using normal JavaScript does not work as
* expected during bootstrapping (see mirror.js in r114903).
*
* @param {function} ctor Constructor function which needs to inherit the
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
exports.inherits = _dereq_('inherits');
exports._extend = function(origin, add) {
// Don't do anything if add isn't an object
if (!add || !isObject(add)) return origin;
var keys = Object.keys(add);
var i = keys.length;
while (i--) {
origin[keys[i]] = add[keys[i]];
}
return origin;
};
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}).call(this,_dereq_("FWaASH"),typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./support/isBuffer":29,"FWaASH":28,"inherits":27}],31:[function(_dereq_,module,exports){
/*! Hammer.JS - v1.1.3 - 2014-05-20
* http://eightmedia.github.io/hammer.js
*
* Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
* Licensed under the MIT license */
(function(window, undefined) {
'use strict';
/**
* @main
* @module hammer
*
* @class Hammer
* @static
*/
/**
* Hammer, use this to create instances
* ````
* var hammertime = new Hammer(myElement);
* ````
*
* @method Hammer
* @param {HTMLElement} element
* @param {Object} [options={}]
* @return {Hammer.Instance}
*/
var Hammer = function Hammer(element, options) {
return new Hammer.Instance(element, options || {});
};
/**
* version, as defined in package.json
* the value will be set at each build
* @property VERSION
* @final
* @type {String}
*/
Hammer.VERSION = '1.1.3';
/**
* default settings.
* more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled
* by setting it's name (like `swipe`) to false.
* You can set the defaults for all instances by changing this object before creating an instance.
* @example
* ````
* Hammer.defaults.drag = false;
* Hammer.defaults.behavior.touchAction = 'pan-y';
* delete Hammer.defaults.behavior.userSelect;
* ````
* @property defaults
* @type {Object}
*/
Hammer.defaults = {
/**
* this setting object adds styles and attributes to the element to prevent the browser from doing
* its native behavior. The css properties are auto prefixed for the browsers when needed.
* @property defaults.behavior
* @type {Object}
*/
behavior: {
/**
* Disables text selection to improve the dragging gesture. When the value is `none` it also sets
* `onselectstart=false` for IE on the element. Mainly for desktop browsers.
* @property defaults.behavior.userSelect
* @type {String}
* @default 'none'
*/
userSelect: 'none',
/**
* Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming).
* Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event.
* @property defaults.behavior.touchAction
* @type {String}
* @default: 'pan-y'
*/
touchAction: 'pan-y',
/**
* Disables the default callout shown when you touch and hold a touch target.
* On iOS, when you touch and hold a touch target such as a link, Safari displays
* a callout containing information about the link. This property allows you to disable that callout.
* @property defaults.behavior.touchCallout
* @type {String}
* @default 'none'
*/
touchCallout: 'none',
/**
* Specifies whether zooming is enabled. Used by IE10>
* @property defaults.behavior.contentZooming
* @type {String}
* @default 'none'
*/
contentZooming: 'none',
/**
* Specifies that an entire element should be draggable instead of its contents.
* Mainly for desktop browsers.
* @property defaults.behavior.userDrag
* @type {String}
* @default 'none'
*/
userDrag: 'none',
/**
* Overrides the highlight color shown when the user taps a link or a JavaScript
* clickable element in Safari on iPhone. This property obeys the alpha value, if specified.
*
* If you don't specify an alpha value, Safari on iPhone applies a default alpha value
* to the color. To disable tap highlighting, set the alpha value to 0 (invisible).
* If you set the alpha value to 1.0 (opaque), the element is not visible when tapped.
* @property defaults.behavior.tapHighlightColor
* @type {String}
* @default 'rgba(0,0,0,0)'
*/
tapHighlightColor: 'rgba(0,0,0,0)'
}
};
/**
* hammer document where the base events are added at
* @property DOCUMENT
* @type {HTMLElement}
* @default window.document
*/
Hammer.DOCUMENT = document;
/**
* detect support for pointer events
* @property HAS_POINTEREVENTS
* @type {Boolean}
*/
Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
/**
* detect support for touch events
* @property HAS_TOUCHEVENTS
* @type {Boolean}
*/
Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
/**
* detect mobile browsers
* @property IS_MOBILE
* @type {Boolean}
*/
Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent);
/**
* detect if we want to support mouseevents at all
* @property NO_MOUSEEVENTS
* @type {Boolean}
*/
Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS;
/**
* interval in which Hammer recalculates current velocity/direction/angle in ms
* @property CALCULATE_INTERVAL
* @type {Number}
* @default 25
*/
Hammer.CALCULATE_INTERVAL = 25;
/**
* eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup`
* the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`)
* @property EVENT_TYPES
* @private
* @writeOnce
* @type {Object}
*/
var EVENT_TYPES = {};
/**
* direction strings, for safe comparisons
* @property DIRECTION_DOWN|LEFT|UP|RIGHT
* @final
* @type {String}
* @default 'down' 'left' 'up' 'right'
*/
var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
/**
* pointertype strings, for safe comparisons
* @property POINTER_MOUSE|TOUCH|PEN
* @final
* @type {String}
* @default 'mouse' 'touch' 'pen'
*/
var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
/**
* eventtypes
* @property EVENT_START|MOVE|END|RELEASE|TOUCH
* @final
* @type {String}
* @default 'start' 'change' 'move' 'end' 'release' 'touch'
*/
var EVENT_START = Hammer.EVENT_START = 'start';
var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
var EVENT_END = Hammer.EVENT_END = 'end';
var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release';
var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch';
/**
* if the window events are set...
* @property READY
* @writeOnce
* @type {Boolean}
* @default false
*/
Hammer.READY = false;
/**
* plugins namespace
* @property plugins
* @type {Object}
*/
Hammer.plugins = Hammer.plugins || {};
/**
* gestures namespace
* see `/gestures` for the definitions
* @property gestures
* @type {Object}
*/
Hammer.gestures = Hammer.gestures || {};
/**
* setup events to detect gestures on the document
* this function is called when creating an new instance
* @private
*/
function setup() {
if(Hammer.READY) {
return;
}
// find what eventtypes we add listeners to
Event.determineEventTypes();
// Register all gestures inside Hammer.gestures
Utils.each(Hammer.gestures, function(gesture) {
Detection.register(gesture);
});
// Add touch events on the document
Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
// Hammer is ready...!
Hammer.READY = true;
}
/**
* @module hammer
*
* @class Utils
* @static
*/
var Utils = Hammer.utils = {
/**
* extend method, could also be used for cloning when `dest` is an empty object.
* changes the dest object
* @method extend
* @param {Object} dest
* @param {Object} src
* @param {Boolean} [merge=false] do a merge
* @return {Object} dest
*/
extend: function extend(dest, src, merge) {
for(var key in src) {
if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) {
continue;
}
dest[key] = src[key];
}
return dest;
},
/**
* simple addEventListener wrapper
* @method on
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
*/
on: function on(element, type, handler) {
element.addEventListener(type, handler, false);
},
/**
* simple removeEventListener wrapper
* @method off
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
*/
off: function off(element, type, handler) {
element.removeEventListener(type, handler, false);
},
/**
* forEach over arrays and objects
* @method each
* @param {Object|Array} obj
* @param {Function} iterator
* @param {any} iterator.item
* @param {Number} iterator.index
* @param {Object|Array} iterator.obj the source object
* @param {Object} context value to use as `this` in the iterator
*/
each: function each(obj, iterator, context) {
var i, len;
// native forEach on arrays
if('forEach' in obj) {
obj.forEach(iterator, context);
// arrays
} else if(obj.length !== undefined) {
for(i = 0, len = obj.length; i < len; i++) {
if(iterator.call(context, obj[i], i, obj) === false) {
return;
}
}
// objects
} else {
for(i in obj) {
if(obj.hasOwnProperty(i) &&
iterator.call(context, obj[i], i, obj) === false) {
return;
}
}
}
},
/**
* find if a string contains the string using indexOf
* @method inStr
* @param {String} src
* @param {String} find
* @return {Boolean} found
*/
inStr: function inStr(src, find) {
return src.indexOf(find) > -1;
},
/**
* find if a array contains the object using indexOf or a simple polyfill
* @method inArray
* @param {String} src
* @param {String} find
* @return {Boolean|Number} false when not found, or the index
*/
inArray: function inArray(src, find) {
if(src.indexOf) {
var index = src.indexOf(find);
return (index === -1) ? false : index;
} else {
for(var i = 0, len = src.length; i < len; i++) {
if(src[i] === find) {
return i;
}
}
return false;
}
},
/**
* convert an array-like object (`arguments`, `touchlist`) to an array
* @method toArray
* @param {Object} obj
* @return {Array}
*/
toArray: function toArray(obj) {
return Array.prototype.slice.call(obj, 0);
},
/**
* find if a node is in the given parent
* @method hasParent
* @param {HTMLElement} node
* @param {HTMLElement} parent
* @return {Boolean} found
*/
hasParent: function hasParent(node, parent) {
while(node) {
if(node == parent) {
return true;
}
node = node.parentNode;
}
return false;
},
/**
* get the center of all the touches
* @method getCenter
* @param {Array} touches
* @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties
*/
getCenter: function getCenter(touches) {
var pageX = [],
pageY = [],
clientX = [],
clientY = [],
min = Math.min,
max = Math.max;
// no need to loop when only one touch
if(touches.length === 1) {
return {
pageX: touches[0].pageX,
pageY: touches[0].pageY,
clientX: touches[0].clientX,
clientY: touches[0].clientY
};
}
Utils.each(touches, function(touch) {
pageX.push(touch.pageX);
pageY.push(touch.pageY);
clientX.push(touch.clientX);
clientY.push(touch.clientY);
});
return {
pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
};
},
/**
* calculate the velocity between two points. unit is in px per ms.
* @method getVelocity
* @param {Number} deltaTime
* @param {Number} deltaX
* @param {Number} deltaY
* @return {Object} velocity `x` and `y`
*/
getVelocity: function getVelocity(deltaTime, deltaX, deltaY) {
return {
x: Math.abs(deltaX / deltaTime) || 0,
y: Math.abs(deltaY / deltaTime) || 0
};
},
/**
* calculate the angle between two coordinates
* @method getAngle
* @param {Touch} touch1
* @param {Touch} touch2
* @return {Number} angle
*/
getAngle: function getAngle(touch1, touch2) {
var x = touch2.clientX - touch1.clientX,
y = touch2.clientY - touch1.clientY;
return Math.atan2(y, x) * 180 / Math.PI;
},
/**
* do a small comparision to get the direction between two touches.
* @method getDirection
* @param {Touch} touch1
* @param {Touch} touch2
* @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN`
*/
getDirection: function getDirection(touch1, touch2) {
var x = Math.abs(touch1.clientX - touch2.clientX),
y = Math.abs(touch1.clientY - touch2.clientY);
if(x >= y) {
return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
},
/**
* calculate the distance between two touches
* @method getDistance
* @param {Touch}touch1
* @param {Touch} touch2
* @return {Number} distance
*/
getDistance: function getDistance(touch1, touch2) {
var x = touch2.clientX - touch1.clientX,
y = touch2.clientY - touch1.clientY;
return Math.sqrt((x * x) + (y * y));
},
/**
* calculate the scale factor between two touchLists
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
* @method getScale
* @param {Array} start array of touches
* @param {Array} end array of touches
* @return {Number} scale
*/
getScale: function getScale(start, end) {
// need two fingers...
if(start.length >= 2 && end.length >= 2) {
return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
}
return 1;
},
/**
* calculate the rotation degrees between two touchLists
* @method getRotation
* @param {Array} start array of touches
* @param {Array} end array of touches
* @return {Number} rotation
*/
getRotation: function getRotation(start, end) {
// need two fingers
if(start.length >= 2 && end.length >= 2) {
return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
}
return 0;
},
/**
* find out if the direction is vertical *
* @method isVertical
* @param {String} direction matches `DIRECTION_UP|DOWN`
* @return {Boolean} is_vertical
*/
isVertical: function isVertical(direction) {
return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
},
/**
* set css properties with their prefixes
* @param {HTMLElement} element
* @param {String} prop
* @param {String} value
* @param {Boolean} [toggle=true]
* @return {Boolean}
*/
setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) {
var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms'];
prop = Utils.toCamelCase(prop);
for(var i = 0; i < prefixes.length; i++) {
var p = prop;
// prefixes
if(prefixes[i]) {
p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1);
}
// test the style
if(p in element.style) {
element.style[p] = (toggle == null || toggle) && value || '';
break;
}
}
},
/**
* toggle browser default behavior by setting css properties.
* `userSelect='none'` also sets `element.onselectstart` to false
* `userDrag='none'` also sets `element.ondragstart` to false
*
* @method toggleBehavior
* @param {HtmlElement} element
* @param {Object} props
* @param {Boolean} [toggle=true]
*/
toggleBehavior: function toggleBehavior(element, props, toggle) {
if(!props || !element || !element.style) {
return;
}
// set the css properties
Utils.each(props, function(value, prop) {
Utils.setPrefixedCss(element, prop, value, toggle);
});
var falseFn = toggle && function() {
return false;
};
// also the disable onselectstart
if(props.userSelect == 'none') {
element.onselectstart = falseFn;
}
// and disable ondragstart
if(props.userDrag == 'none') {
element.ondragstart = falseFn;
}
},
/**
* convert a string with underscores to camelCase
* so prevent_default becomes preventDefault
* @param {String} str
* @return {String} camelCaseStr
*/
toCamelCase: function toCamelCase(str) {
return str.replace(/[_-]([a-z])/g, function(s) {
return s[1].toUpperCase();
});
}
};
/**
* @module hammer
*/
/**
* @class Event
* @static
*/
var Event = Hammer.event = {
/**
* when touch events have been fired, this is true
* this is used to stop mouse events
* @property prevent_mouseevents
* @private
* @type {Boolean}
*/
preventMouseEvents: false,
/**
* if EVENT_START has been fired
* @property started
* @private
* @type {Boolean}
*/
started: false,
/**
* when the mouse is hold down, this is true
* @property should_detect
* @private
* @type {Boolean}
*/
shouldDetect: false,
/**
* simple event binder with a hook and support for multiple types
* @method on
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
* @param {Function} [hook]
* @param {Object} hook.type
*/
on: function on(element, type, handler, hook) {
var types = type.split(' ');
Utils.each(types, function(type) {
Utils.on(element, type, handler);
hook && hook(type);
});
},
/**
* simple event unbinder with a hook and support for multiple types
* @method off
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
* @param {Function} [hook]
* @param {Object} hook.type
*/
off: function off(element, type, handler, hook) {
var types = type.split(' ');
Utils.each(types, function(type) {
Utils.off(element, type, handler);
hook && hook(type);
});
},
/**
* the core touch event handler.
* this finds out if we should to detect gestures
* @method onTouch
* @param {HTMLElement} element
* @param {String} eventType matches `EVENT_START|MOVE|END`
* @param {Function} handler
* @return onTouchHandler {Function} the core event handler
*/
onTouch: function onTouch(element, eventType, handler) {
var self = this;
var onTouchHandler = function onTouchHandler(ev) {
var srcType = ev.type.toLowerCase(),
isPointer = Hammer.HAS_POINTEREVENTS,
isMouse = Utils.inStr(srcType, 'mouse'),
triggerType;
// if we are in a mouseevent, but there has been a touchevent triggered in this session
// we want to do nothing. simply break out of the event.
if(isMouse && self.preventMouseEvents) {
return;
// mousebutton must be down
} else if(isMouse && eventType == EVENT_START && ev.button === 0) {
self.preventMouseEvents = false;
self.shouldDetect = true;
} else if(isPointer && eventType == EVENT_START) {
self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev));
// just a valid start event, but no mouse
} else if(!isMouse && eventType == EVENT_START) {
self.preventMouseEvents = true;
self.shouldDetect = true;
}
// update the pointer event before entering the detection
if(isPointer && eventType != EVENT_END) {
PointerEvent.updatePointer(eventType, ev);
}
// we are in a touch/down state, so allowed detection of gestures
if(self.shouldDetect) {
triggerType = self.doDetect.call(self, ev, eventType, element, handler);
}
// ...and we are done with the detection
// so reset everything to start each detection totally fresh
if(triggerType == EVENT_END) {
self.preventMouseEvents = false;
self.shouldDetect = false;
PointerEvent.reset();
// update the pointerevent object after the detection
}
if(isPointer && eventType == EVENT_END) {
PointerEvent.updatePointer(eventType, ev);
}
};
this.on(element, EVENT_TYPES[eventType], onTouchHandler);
return onTouchHandler;
},
/**
* the core detection method
* this finds out what hammer-touch-events to trigger
* @method doDetect
* @param {Object} ev
* @param {String} eventType matches `EVENT_START|MOVE|END`
* @param {HTMLElement} element
* @param {Function} handler
* @return {String} triggerType matches `EVENT_START|MOVE|END`
*/
doDetect: function doDetect(ev, eventType, element, handler) {
var touchList = this.getTouchList(ev, eventType);
var touchListLength = touchList.length;
var triggerType = eventType;
var triggerChange = touchList.trigger; // used by fakeMultitouch plugin
var changedLength = touchListLength;
// at each touchstart-like event we want also want to trigger a TOUCH event...
if(eventType == EVENT_START) {
triggerChange = EVENT_TOUCH;
// ...the same for a touchend-like event
} else if(eventType == EVENT_END) {
triggerChange = EVENT_RELEASE;
// keep track of how many touches have been removed
changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1);
}
// after there are still touches on the screen,
// we just want to trigger a MOVE event. so change the START or END to a MOVE
// but only after detection has been started, the first time we actualy want a START
if(changedLength > 0 && this.started) {
triggerType = EVENT_MOVE;
}
// detection has been started, we keep track of this, see above
this.started = true;
// generate some event data, some basic information
var evData = this.collectEventData(element, triggerType, touchList, ev);
// trigger the triggerType event before the change (TOUCH, RELEASE) events
// but the END event should be at last
if(eventType != EVENT_END) {
handler.call(Detection, evData);
}
// trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed
if(triggerChange) {
evData.changedLength = changedLength;
evData.eventType = triggerChange;
handler.call(Detection, evData);
evData.eventType = triggerType;
delete evData.changedLength;
}
// trigger the END event
if(triggerType == EVENT_END) {
handler.call(Detection, evData);
// ...and we are done with the detection
// so reset everything to start each detection totally fresh
this.started = false;
}
return triggerType;
},
/**
* we have different events for each device/browser
* determine what we need and set them in the EVENT_TYPES constant
* the `onTouch` method is bind to these properties.
* @method determineEventTypes
* @return {Object} events
*/
determineEventTypes: function determineEventTypes() {
var types;
if(Hammer.HAS_POINTEREVENTS) {
if(window.PointerEvent) {
types = [
'pointerdown',
'pointermove',
'pointerup pointercancel lostpointercapture'
];
} else {
types = [
'MSPointerDown',
'MSPointerMove',
'MSPointerUp MSPointerCancel MSLostPointerCapture'
];
}
} else if(Hammer.NO_MOUSEEVENTS) {
types = [
'touchstart',
'touchmove',
'touchend touchcancel'
];
} else {
types = [
'touchstart mousedown',
'touchmove mousemove',
'touchend touchcancel mouseup'
];
}
EVENT_TYPES[EVENT_START] = types[0];
EVENT_TYPES[EVENT_MOVE] = types[1];
EVENT_TYPES[EVENT_END] = types[2];
return EVENT_TYPES;
},
/**
* create touchList depending on the event
* @method getTouchList
* @param {Object} ev
* @param {String} eventType
* @return {Array} touches
*/
getTouchList: function getTouchList(ev, eventType) {
// get the fake pointerEvent touchlist
if(Hammer.HAS_POINTEREVENTS) {
return PointerEvent.getTouchList();
}
// get the touchlist
if(ev.touches) {
if(eventType == EVENT_MOVE) {
return ev.touches;
}
var identifiers = [];
var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches));
var touchList = [];
Utils.each(concat, function(touch) {
if(Utils.inArray(identifiers, touch.identifier) === false) {
touchList.push(touch);
}
identifiers.push(touch.identifier);
});
return touchList;
}
// make fake touchList from mouse position
ev.identifier = 1;
return [ev];
},
/**
* collect basic event data
* @method collectEventData
* @param {HTMLElement} element
* @param {String} eventType matches `EVENT_START|MOVE|END`
* @param {Array} touches
* @param {Object} ev
* @return {Object} ev
*/
collectEventData: function collectEventData(element, eventType, touches, ev) {
// find out pointerType
var pointerType = POINTER_TOUCH;
if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
pointerType = POINTER_MOUSE;
} else if(PointerEvent.matchType(POINTER_PEN, ev)) {
pointerType = POINTER_PEN;
}
return {
center: Utils.getCenter(touches),
timeStamp: Date.now(),
target: ev.target,
touches: touches,
eventType: eventType,
pointerType: pointerType,
srcEvent: ev,
/**
* prevent the browser default actions
* mostly used to disable scrolling of the browser
*/
preventDefault: function() {
var srcEvent = this.srcEvent;
srcEvent.preventManipulation && srcEvent.preventManipulation();
srcEvent.preventDefault && srcEvent.preventDefault();
},
/**
* stop bubbling the event up to its parents
*/
stopPropagation: function() {
this.srcEvent.stopPropagation();
},
/**
* immediately stop gesture detection
* might be useful after a swipe was detected
* @return {*}
*/
stopDetect: function() {
return Detection.stopDetect();
}
};
}
};
/**
* @module hammer
*
* @class PointerEvent
* @static
*/
var PointerEvent = Hammer.PointerEvent = {
/**
* holds all pointers, by `identifier`
* @property pointers
* @type {Object}
*/
pointers: {},
/**
* get the pointers as an array
* @method getTouchList
* @return {Array} touchlist
*/
getTouchList: function getTouchList() {
var touchlist = [];
// we can use forEach since pointerEvents only is in IE10
Utils.each(this.pointers, function(pointer) {
touchlist.push(pointer);
});
return touchlist;
},
/**
* update the position of a pointer
* @method updatePointer
* @param {String} eventType matches `EVENT_START|MOVE|END`
* @param {Object} pointerEvent
*/
updatePointer: function updatePointer(eventType, pointerEvent) {
if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) {
delete this.pointers[pointerEvent.pointerId];
} else {
pointerEvent.identifier = pointerEvent.pointerId;
this.pointers[pointerEvent.pointerId] = pointerEvent;
}
},
/**
* check if ev matches pointertype
* @method matchType
* @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN`
* @param {PointerEvent} ev
*/
matchType: function matchType(pointerType, ev) {
if(!ev.pointerType) {
return false;
}
var pt = ev.pointerType,
types = {};
types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE));
types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH));
types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN));
return types[pointerType];
},
/**
* reset the stored pointers
* @method reset
*/
reset: function resetList() {
this.pointers = {};
}
};
/**
* @module hammer
*
* @class Detection
* @static
*/
var Detection = Hammer.detection = {
// contains all registred Hammer.gestures in the correct order
gestures: [],
// data of the current Hammer.gesture detection session
current: null,
// the previous Hammer.gesture session data
// is a full clone of the previous gesture.current object
previous: null,
// when this becomes true, no gestures are fired
stopped: false,
/**
* start Hammer.gesture detection
* @method startDetect
* @param {Hammer.Instance} inst
* @param {Object} eventData
*/
startDetect: function startDetect(inst, eventData) {
// already busy with a Hammer.gesture detection on an element
if(this.current) {
return;
}
this.stopped = false;
// holds current session
this.current = {
inst: inst, // reference to HammerInstance we're working for
startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc
lastEvent: false, // last eventData
lastCalcEvent: false, // last eventData for calculations.
futureCalcEvent: false, // last eventData for calculations.
lastCalcData: {}, // last lastCalcData
name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
};
this.detect(eventData);
},
/**
* Hammer.gesture detection
* @method detect
* @param {Object} eventData
* @return {any}
*/
detect: function detect(eventData) {
if(!this.current || this.stopped) {
return;
}
// extend event data with calculations about scale, distance etc
eventData = this.extendEventData(eventData);
// hammer instance and instance options
var inst = this.current.inst,
instOptions = inst.options;
// call Hammer.gesture handlers
Utils.each(this.gestures, function triggerGesture(gesture) {
// only when the instance options have enabled this gesture
if(!this.stopped && inst.enabled && instOptions[gesture.name]) {
gesture.handler.call(gesture, eventData, inst);
}
}, this);
// store as previous event event
if(this.current) {
this.current.lastEvent = eventData;
}
if(eventData.eventType == EVENT_END) {
this.stopDetect();
}
return eventData;
},
/**
* clear the Hammer.gesture vars
* this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
* to stop other Hammer.gestures from being fired
* @method stopDetect
*/
stopDetect: function stopDetect() {
// clone current data to the store as the previous gesture
// used for the double tap gesture, since this is an other gesture detect session
this.previous = Utils.extend({}, this.current);
// reset the current
this.current = null;
this.stopped = true;
},
/**
* calculate velocity, angle and direction
* @method getVelocityData
* @param {Object} ev
* @param {Object} center
* @param {Number} deltaTime
* @param {Number} deltaX
* @param {Number} deltaY
*/
getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) {
var cur = this.current,
recalc = false,
calcEv = cur.lastCalcEvent,
calcData = cur.lastCalcData;
if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) {
center = calcEv.center;
deltaTime = ev.timeStamp - calcEv.timeStamp;
deltaX = ev.center.clientX - calcEv.center.clientX;
deltaY = ev.center.clientY - calcEv.center.clientY;
recalc = true;
}
if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
cur.futureCalcEvent = ev;
}
if(!cur.lastCalcEvent || recalc) {
calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY);
calcData.angle = Utils.getAngle(center, ev.center);
calcData.direction = Utils.getDirection(center, ev.center);
cur.lastCalcEvent = cur.futureCalcEvent || ev;
cur.futureCalcEvent = ev;
}
ev.velocityX = calcData.velocity.x;
ev.velocityY = calcData.velocity.y;
ev.interimAngle = calcData.angle;
ev.interimDirection = calcData.direction;
},
/**
* extend eventData for Hammer.gestures
* @method extendEventData
* @param {Object} ev
* @return {Object} ev
*/
extendEventData: function extendEventData(ev) {
var cur = this.current,
startEv = cur.startEvent,
lastEv = cur.lastEvent || startEv;
// update the start touchlist to calculate the scale/rotation
if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
startEv.touches = [];
Utils.each(ev.touches, function(touch) {
startEv.touches.push({
clientX: touch.clientX,
clientY: touch.clientY
});
});
}
var deltaTime = ev.timeStamp - startEv.timeStamp,
deltaX = ev.center.clientX - startEv.center.clientX,
deltaY = ev.center.clientY - startEv.center.clientY;
this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY);
Utils.extend(ev, {
startEvent: startEv,
deltaTime: deltaTime,
deltaX: deltaX,
deltaY: deltaY,
distance: Utils.getDistance(startEv.center, ev.center),
angle: Utils.getAngle(startEv.center, ev.center),
direction: Utils.getDirection(startEv.center, ev.center),
scale: Utils.getScale(startEv.touches, ev.touches),
rotation: Utils.getRotation(startEv.touches, ev.touches)
});
return ev;
},
/**
* register new gesture
* @method register
* @param {Object} gesture object, see `gestures/` for documentation
* @return {Array} gestures
*/
register: function register(gesture) {
// add an enable gesture options if there is no given
var options = gesture.defaults || {};
if(options[gesture.name] === undefined) {
options[gesture.name] = true;
}
// extend Hammer default options with the Hammer.gesture options
Utils.extend(Hammer.defaults, options, true);
// set its index
gesture.index = gesture.index || 1000;
// add Hammer.gesture to the list
this.gestures.push(gesture);
// sort the list by index
this.gestures.sort(function(a, b) {
if(a.index < b.index) {
return -1;
}
if(a.index > b.index) {
return 1;
}
return 0;
});
return this.gestures;
}
};
/**
* @module hammer
*/
/**
* create new hammer instance
* all methods should return the instance itself, so it is chainable.
*
* @class Instance
* @constructor
* @param {HTMLElement} element
* @param {Object} [options={}] options are merged with `Hammer.defaults`
* @return {Hammer.Instance}
*/
Hammer.Instance = function(element, options) {
var self = this;
// setup HammerJS window events and register all gestures
// this also sets up the default options
setup();
/**
* @property element
* @type {HTMLElement}
*/
this.element = element;
/**
* @property enabled
* @type {Boolean}
* @protected
*/
this.enabled = true;
/**
* options, merged with the defaults
* options with an _ are converted to camelCase
* @property options
* @type {Object}
*/
Utils.each(options, function(value, name) {
delete options[name];
options[Utils.toCamelCase(name)] = value;
});
this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {});
// add some css to the element to prevent the browser from doing its native behavoir
if(this.options.behavior) {
Utils.toggleBehavior(this.element, this.options.behavior, true);
}
/**
* event start handler on the element to start the detection
* @property eventStartHandler
* @type {Object}
*/
this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
if(self.enabled && ev.eventType == EVENT_START) {
Detection.startDetect(self, ev);
} else if(ev.eventType == EVENT_TOUCH) {
Detection.detect(ev);
}
});
/**
* keep a list of user event handlers which needs to be removed when calling 'dispose'
* @property eventHandlers
* @type {Array}
*/
this.eventHandlers = [];
};
Hammer.Instance.prototype = {
/**
* bind events to the instance
* @method on
* @chainable
* @param {String} gestures multiple gestures by splitting with a space
* @param {Function} handler
* @param {Object} handler.ev event object
*/
on: function onEvent(gestures, handler) {
var self = this;
Event.on(self.element, gestures, handler, function(type) {
self.eventHandlers.push({ gesture: type, handler: handler });
});
return self;
},
/**
* unbind events to the instance
* @method off
* @chainable
* @param {String} gestures
* @param {Function} handler
*/
off: function offEvent(gestures, handler) {
var self = this;
Event.off(self.element, gestures, handler, function(type) {
var index = Utils.inArray({ gesture: type, handler: handler });
if(index !== false) {
self.eventHandlers.splice(index, 1);
}
});
return self;
},
/**
* trigger gesture event
* @method trigger
* @chainable
* @param {String} gesture
* @param {Object} [eventData]
*/
trigger: function triggerEvent(gesture, eventData) {
// optional
if(!eventData) {
eventData = {};
}
// create DOM event
var event = Hammer.DOCUMENT.createEvent('Event');
event.initEvent(gesture, true, true);
event.gesture = eventData;
// trigger on the target if it is in the instance element,
// this is for event delegation tricks
var element = this.element;
if(Utils.hasParent(eventData.target, element)) {
element = eventData.target;
}
element.dispatchEvent(event);
return this;
},
/**
* enable of disable hammer.js detection
* @method enable
* @chainable
* @param {Boolean} state
*/
enable: function enable(state) {
this.enabled = state;
return this;
},
/**
* dispose this hammer instance
* @method dispose
* @return {Null}
*/
dispose: function dispose() {
var i, eh;
// undo all changes made by stop_browser_behavior
Utils.toggleBehavior(this.element, this.options.behavior, false);
// unbind all custom event handlers
for(i = -1; (eh = this.eventHandlers[++i]);) {
Utils.off(this.element, eh.gesture, eh.handler);
}
this.eventHandlers = [];
// unbind the start event listener
Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler);
return null;
}
};
/**
* @module gestures
*/
/**
* Move with x fingers (default 1) around on the page.
* Preventing the default browser behavior is a good way to improve feel and working.
* ````
* hammertime.on("drag", function(ev) {
* console.log(ev);
* ev.gesture.preventDefault();
* });
* ````
*
* @class Drag
* @static
*/
/**
* @event drag
* @param {Object} ev
*/
/**
* @event dragstart
* @param {Object} ev
*/
/**
* @event dragend
* @param {Object} ev
*/
/**
* @event drapleft
* @param {Object} ev
*/
/**
* @event dragright
* @param {Object} ev
*/
/**
* @event dragup
* @param {Object} ev
*/
/**
* @event dragdown
* @param {Object} ev
*/
/**
* @param {String} name
*/
(function(name) {
var triggered = false;
function dragGesture(ev, inst) {
var cur = Detection.current;
// max touches
if(inst.options.dragMaxTouches > 0 &&
ev.touches.length > inst.options.dragMaxTouches) {
return;
}
switch(ev.eventType) {
case EVENT_START:
triggered = false;
break;
case EVENT_MOVE:
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.distance < inst.options.dragMinDistance &&
cur.name != name) {
return;
}
var startCenter = cur.startEvent.center;
// we are dragging!
if(cur.name != name) {
cur.name = name;
if(inst.options.dragDistanceCorrection && ev.distance > 0) {
// When a drag is triggered, set the event center to dragMinDistance pixels from the original event center.
// Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0.
// It might be useful to save the original start point somewhere
var factor = Math.abs(inst.options.dragMinDistance / ev.distance);
startCenter.pageX += ev.deltaX * factor;
startCenter.pageY += ev.deltaY * factor;
startCenter.clientX += ev.deltaX * factor;
startCenter.clientY += ev.deltaY * factor;
// recalculate event data using new start point
ev = Detection.extendEventData(ev);
}
}
// lock drag to axis?
if(cur.lastEvent.dragLockToAxis ||
( inst.options.dragLockToAxis &&
inst.options.dragLockMinDistance <= ev.distance
)) {
ev.dragLockToAxis = true;
}
// keep direction on the axis that the drag gesture started on
var lastDirection = cur.lastEvent.direction;
if(ev.dragLockToAxis && lastDirection !== ev.direction) {
if(Utils.isVertical(lastDirection)) {
ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
} else {
ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
}
// first time, trigger dragstart event
if(!triggered) {
inst.trigger(name + 'start', ev);
triggered = true;
}
// trigger events
inst.trigger(name, ev);
inst.trigger(name + ev.direction, ev);
var isVertical = Utils.isVertical(ev.direction);
// block the browser events
if((inst.options.dragBlockVertical && isVertical) ||
(inst.options.dragBlockHorizontal && !isVertical)) {
ev.preventDefault();
}
break;
case EVENT_RELEASE:
if(triggered && ev.changedLength <= inst.options.dragMaxTouches) {
inst.trigger(name + 'end', ev);
triggered = false;
}
break;
case EVENT_END:
triggered = false;
break;
}
}
Hammer.gestures.Drag = {
name: name,
index: 50,
handler: dragGesture,
defaults: {
/**
* minimal movement that have to be made before the drag event gets triggered
* @property dragMinDistance
* @type {Number}
* @default 10
*/
dragMinDistance: 10,
/**
* Set dragDistanceCorrection to true to make the starting point of the drag
* be calculated from where the drag was triggered, not from where the touch started.
* Useful to avoid a jerk-starting drag, which can make fine-adjustments
* through dragging difficult, and be visually unappealing.
* @property dragDistanceCorrection
* @type {Boolean}
* @default true
*/
dragDistanceCorrection: true,
/**
* set 0 for unlimited, but this can conflict with transform
* @property dragMaxTouches
* @type {Number}
* @default 1
*/
dragMaxTouches: 1,
/**
* prevent default browser behavior when dragging occurs
* be careful with it, it makes the element a blocking element
* when you are using the drag gesture, it is a good practice to set this true
* @property dragBlockHorizontal
* @type {Boolean}
* @default false
*/
dragBlockHorizontal: false,
/**
* same as `dragBlockHorizontal`, but for vertical movement
* @property dragBlockVertical
* @type {Boolean}
* @default false
*/
dragBlockVertical: false,
/**
* dragLockToAxis keeps the drag gesture on the axis that it started on,
* It disallows vertical directions if the initial direction was horizontal, and vice versa.
* @property dragLockToAxis
* @type {Boolean}
* @default false
*/
dragLockToAxis: false,
/**
* drag lock only kicks in when distance > dragLockMinDistance
* This way, locking occurs only when the distance has become large enough to reliably determine the direction
* @property dragLockMinDistance
* @type {Number}
* @default 25
*/
dragLockMinDistance: 25
}
};
})('drag');
/**
* @module gestures
*/
/**
* trigger a simple gesture event, so you can do anything in your handler.
* only usable if you know what your doing...
*
* @class Gesture
* @static
*/
/**
* @event gesture
* @param {Object} ev
*/
Hammer.gestures.Gesture = {
name: 'gesture',
index: 1337,
handler: function releaseGesture(ev, inst) {
inst.trigger(this.name, ev);
}
};
/**
* @module gestures
*/
/**
* Touch stays at the same place for x time
*
* @class Hold
* @static
*/
/**
* @event hold
* @param {Object} ev
*/
/**
* @param {String} name
*/
(function(name) {
var timer;
function holdGesture(ev, inst) {
var options = inst.options,
current = Detection.current;
switch(ev.eventType) {
case EVENT_START:
clearTimeout(timer);
// set the gesture so we can check in the timeout if it still is
current.name = name;
// set timer and if after the timeout it still is hold,
// we trigger the hold event
timer = setTimeout(function() {
if(current && current.name == name) {
inst.trigger(name, ev);
}
}, options.holdTimeout);
break;
case EVENT_MOVE:
if(ev.distance > options.holdThreshold) {
clearTimeout(timer);
}
break;
case EVENT_RELEASE:
clearTimeout(timer);
break;
}
}
Hammer.gestures.Hold = {
name: name,
index: 10,
defaults: {
/**
* @property holdTimeout
* @type {Number}
* @default 500
*/
holdTimeout: 500,
/**
* movement allowed while holding
* @property holdThreshold
* @type {Number}
* @default 2
*/
holdThreshold: 2
},
handler: holdGesture
};
})('hold');
/**
* @module gestures
*/
/**
* when a touch is being released from the page
*
* @class Release
* @static
*/
/**
* @event release
* @param {Object} ev
*/
Hammer.gestures.Release = {
name: 'release',
index: Infinity,
handler: function releaseGesture(ev, inst) {
if(ev.eventType == EVENT_RELEASE) {
inst.trigger(this.name, ev);
}
}
};
/**
* @module gestures
*/
/**
* triggers swipe events when the end velocity is above the threshold
* for best usage, set `preventDefault` (on the drag gesture) to `true`
* ````
* hammertime.on("dragleft swipeleft", function(ev) {
* console.log(ev);
* ev.gesture.preventDefault();
* });
* ````
*
* @class Swipe
* @static
*/
/**
* @event swipe
* @param {Object} ev
*/
/**
* @event swipeleft
* @param {Object} ev
*/
/**
* @event swiperight
* @param {Object} ev
*/
/**
* @event swipeup
* @param {Object} ev
*/
/**
* @event swipedown
* @param {Object} ev
*/
Hammer.gestures.Swipe = {
name: 'swipe',
index: 40,
defaults: {
/**
* @property swipeMinTouches
* @type {Number}
* @default 1
*/
swipeMinTouches: 1,
/**
* @property swipeMaxTouches
* @type {Number}
* @default 1
*/
swipeMaxTouches: 1,
/**
* horizontal swipe velocity
* @property swipeVelocityX
* @type {Number}
* @default 0.6
*/
swipeVelocityX: 0.6,
/**
* vertical swipe velocity
* @property swipeVelocityY
* @type {Number}
* @default 0.6
*/
swipeVelocityY: 0.6
},
handler: function swipeGesture(ev, inst) {
if(ev.eventType == EVENT_RELEASE) {
var touches = ev.touches.length,
options = inst.options;
// max touches
if(touches < options.swipeMinTouches ||
touches > options.swipeMaxTouches) {
return;
}
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.velocityX > options.swipeVelocityX ||
ev.velocityY > options.swipeVelocityY) {
// trigger swipe events
inst.trigger(this.name, ev);
inst.trigger(this.name + ev.direction, ev);
}
}
}
};
/**
* @module gestures
*/
/**
* Single tap and a double tap on a place
*
* @class Tap
* @static
*/
/**
* @event tap
* @param {Object} ev
*/
/**
* @event doubletap
* @param {Object} ev
*/
/**
* @param {String} name
*/
(function(name) {
var hasMoved = false;
function tapGesture(ev, inst) {
var options = inst.options,
current = Detection.current,
prev = Detection.previous,
sincePrev,
didDoubleTap;
switch(ev.eventType) {
case EVENT_START:
hasMoved = false;
break;
case EVENT_MOVE:
hasMoved = hasMoved || (ev.distance > options.tapMaxDistance);
break;
case EVENT_END:
if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) {
// previous gesture, for the double tap since these are two different gesture detections
sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
didDoubleTap = false;
// check if double tap
if(prev && prev.name == name &&
(sincePrev && sincePrev < options.doubleTapInterval) &&
ev.distance < options.doubleTapDistance) {
inst.trigger('doubletap', ev);
didDoubleTap = true;
}
// do a single tap
if(!didDoubleTap || options.tapAlways) {
current.name = name;
inst.trigger(current.name, ev);
}
}
break;
}
}
Hammer.gestures.Tap = {
name: name,
index: 100,
handler: tapGesture,
defaults: {
/**
* max time of a tap, this is for the slow tappers
* @property tapMaxTime
* @type {Number}
* @default 250
*/
tapMaxTime: 250,
/**
* max distance of movement of a tap, this is for the slow tappers
* @property tapMaxDistance
* @type {Number}
* @default 10
*/
tapMaxDistance: 10,
/**
* always trigger the `tap` event, even while double-tapping
* @property tapAlways
* @type {Boolean}
* @default true
*/
tapAlways: true,
/**
* max distance between two taps
* @property doubleTapDistance
* @type {Number}
* @default 20
*/
doubleTapDistance: 20,
/**
* max time between two taps
* @property doubleTapInterval
* @type {Number}
* @default 300
*/
doubleTapInterval: 300
}
};
})('tap');
/**
* @module gestures
*/
/**
* when a touch is being touched at the page
*
* @class Touch
* @static
*/
/**
* @event touch
* @param {Object} ev
*/
Hammer.gestures.Touch = {
name: 'touch',
index: -Infinity,
defaults: {
/**
* call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page,
* but it improves gestures like transforming and dragging.
* be careful with using this, it can be very annoying for users to be stuck on the page
* @property preventDefault
* @type {Boolean}
* @default false
*/
preventDefault: false,
/**
* disable mouse events, so only touch (or pen!) input triggers events
* @property preventMouse
* @type {Boolean}
* @default false
*/
preventMouse: false
},
handler: function touchGesture(ev, inst) {
if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) {
ev.stopDetect();
return;
}
if(inst.options.preventDefault) {
ev.preventDefault();
}
if(ev.eventType == EVENT_TOUCH) {
inst.trigger('touch', ev);
}
}
};
/**
* @module gestures
*/
/**
* User want to scale or rotate with 2 fingers
* Preventing the default browser behavior is a good way to improve feel and working. This can be done with the
* `preventDefault` option.
*
* @class Transform
* @static
*/
/**
* @event transform
* @param {Object} ev
*/
/**
* @event transformstart
* @param {Object} ev
*/
/**
* @event transformend
* @param {Object} ev
*/
/**
* @event pinchin
* @param {Object} ev
*/
/**
* @event pinchout
* @param {Object} ev
*/
/**
* @event rotate
* @param {Object} ev
*/
/**
* @param {String} name
*/
(function(name) {
var triggered = false;
function transformGesture(ev, inst) {
switch(ev.eventType) {
case EVENT_START:
triggered = false;
break;
case EVENT_MOVE:
// at least multitouch
if(ev.touches.length < 2) {
return;
}
var scaleThreshold = Math.abs(1 - ev.scale);
var rotationThreshold = Math.abs(ev.rotation);
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(scaleThreshold < inst.options.transformMinScale &&
rotationThreshold < inst.options.transformMinRotation) {
return;
}
// we are transforming!
Detection.current.name = name;
// first time, trigger dragstart event
if(!triggered) {
inst.trigger(name + 'start', ev);
triggered = true;
}
inst.trigger(name, ev); // basic transform event
// trigger rotate event
if(rotationThreshold > inst.options.transformMinRotation) {
inst.trigger('rotate', ev);
}
// trigger pinch event
if(scaleThreshold > inst.options.transformMinScale) {
inst.trigger('pinch', ev);
inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev);
}
break;
case EVENT_RELEASE:
if(triggered && ev.changedLength < 2) {
inst.trigger(name + 'end', ev);
triggered = false;
}
break;
}
}
Hammer.gestures.Transform = {
name: name,
index: 45,
defaults: {
/**
* minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
* @property transformMinScale
* @type {Number}
* @default 0.01
*/
transformMinScale: 0.01,
/**
* rotation in degrees
* @property transformMinRotation
* @type {Number}
* @default 1
*/
transformMinRotation: 1
},
handler: transformGesture
};
})('transform');
/**
* @module hammer
*/
// AMD export
if(typeof define == 'function' && define.amd) {
define(function() {
return Hammer;
});
// commonjs export
} else if(typeof module !== 'undefined' && module.exports) {
module.exports = Hammer;
// browser export
} else {
window.Hammer = Hammer;
}
})(window);
},{}],32:[function(_dereq_,module,exports){
if (!('indexOf' in Array.prototype)) {
Array.prototype.indexOf= function(find, i /*opt*/) {
if (i===undefined) i= 0;
if (i<0) i+= this.length;
if (i<0) i= 0;
for (var n= this.length; i<n; i++)
if (i in this && this[i]===find)
return i;
return -1;
};
}
d3 = (function(){
var d3 = {version: "3.3.10"}; // semver
function d3_class(ctor, properties) {
try {
for (var key in properties) {
Object.defineProperty(ctor.prototype, key, {
value: properties[key],
enumerable: false
});
}
} catch (e) {
ctor.prototype = properties;
}
}
d3.map = function(object) {
var map = new d3_Map;
if (object instanceof d3_Map) object.forEach(function(key, value) { map.set(key, value); });
else for (var key in object) map.set(key, object[key]);
return map;
};
function d3_Map() {}
d3_class(d3_Map, {
has: function(key) {
return d3_map_prefix + key in this;
},
get: function(key) {
return this[d3_map_prefix + key];
},
set: function(key, value) {
return this[d3_map_prefix + key] = value;
},
remove: function(key) {
key = d3_map_prefix + key;
return key in this && delete this[key];
},
keys: function() {
var keys = [];
this.forEach(function(key) { keys.push(key); });
return keys;
},
values: function() {
var values = [];
this.forEach(function(key, value) { values.push(value); });
return values;
},
entries: function() {
var entries = [];
this.forEach(function(key, value) { entries.push({key: key, value: value}); });
return entries;
},
forEach: function(f) {
for (var key in this) {
if (key.charCodeAt(0) === d3_map_prefixCode) {
f.call(this, key.substring(1), this[key]);
}
}
}
});
var d3_map_prefix = "\0", // prevent collision with built-ins
d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
d3.dispatch = function() {
var dispatch = new d3_dispatch,
i = -1,
n = arguments.length;
while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
return dispatch;
};
function d3_dispatch() {}
d3_dispatch.prototype.on = function(type, listener) {
var i = type.indexOf("."),
name = "";
// Extract optional namespace, e.g., "click.foo"
if (i >= 0) {
name = type.substring(i + 1);
type = type.substring(0, i);
}
if (type) return arguments.length < 2
? this[type].on(name)
: this[type].on(name, listener);
if (arguments.length === 2) {
if (listener == null) for (type in this) {
if (this.hasOwnProperty(type)) this[type].on(name, null);
}
return this;
}
};
function d3_dispatch_event(dispatch) {
var listeners = [],
listenerByName = new d3_Map;
function event() {
var z = listeners, // defensive reference
i = -1,
n = z.length,
l;
while (++i < n) if (l = z[i].on) l.apply(this, arguments);
return dispatch;
}
event.on = function(name, listener) {
var l = listenerByName.get(name),
i;
// return the current listener, if any
if (arguments.length < 2) return l && l.on;
// remove the old listener, if any (with copy-on-write)
if (l) {
l.on = null;
listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));
listenerByName.remove(name);
}
// add the new listener, if any
if (listener) listeners.push(listenerByName.set(name, {on: listener}));
return dispatch;
};
return event;
}
// Copies a variable number of methods from source to target.
d3.rebind = function(target, source) {
var i = 1, n = arguments.length, method;
while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);
return target;
};
// Method is assumed to be a standard D3 getter-setter:
// If passed with no arguments, gets the value.
// If passed with arguments, sets the value and returns the target.
function d3_rebind(target, source, method) {
return function() {
var value = method.apply(source, arguments);
return value === source ? target : value;
};
}
return d3;
})();
},{}],33:[function(_dereq_,module,exports){
// Released under MIT license
// Copyright (c) 2009-2010 Dominic Baggott
// Copyright (c) 2009-2010 Ash Berlin
// Copyright (c) 2011 Christoph Dorn <christoph@christophdorn.com> (http://www.christophdorn.com)
// Date: 2013-09-15T16:12Z
(function(expose) {
var MarkdownHelpers = {};
// For Spidermonkey based engines
function mk_block_toSource() {
return "Markdown.mk_block( " +
uneval(this.toString()) +
", " +
uneval(this.trailing) +
", " +
uneval(this.lineNumber) +
" )";
}
// node
function mk_block_inspect() {
var util = _dereq_("util");
return "Markdown.mk_block( " +
util.inspect(this.toString()) +
", " +
util.inspect(this.trailing) +
", " +
util.inspect(this.lineNumber) +
" )";
}
MarkdownHelpers.mk_block = function(block, trail, line) {
// Be helpful for default case in tests.
if ( arguments.length === 1 )
trail = "\n\n";
// We actually need a String object, not a string primitive
/* jshint -W053 */
var s = new String(block);
s.trailing = trail;
// To make it clear its not just a string
s.inspect = mk_block_inspect;
s.toSource = mk_block_toSource;
if ( line !== undefined )
s.lineNumber = line;
return s;
};
var isArray = MarkdownHelpers.isArray = Array.isArray || function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
// Don't mess with Array.prototype. Its not friendly
if ( Array.prototype.forEach ) {
MarkdownHelpers.forEach = function forEach( arr, cb, thisp ) {
return arr.forEach( cb, thisp );
};
}
else {
MarkdownHelpers.forEach = function forEach(arr, cb, thisp) {
for (var i = 0; i < arr.length; i++)
cb.call(thisp || arr, arr[i], i, arr);
};
}
MarkdownHelpers.isEmpty = function isEmpty( obj ) {
for ( var key in obj ) {
if ( hasOwnProperty.call( obj, key ) )
return false;
}
return true;
};
MarkdownHelpers.extract_attr = function extract_attr( jsonml ) {
return isArray(jsonml)
&& jsonml.length > 1
&& typeof jsonml[ 1 ] === "object"
&& !( isArray(jsonml[ 1 ]) )
? jsonml[ 1 ]
: undefined;
};
/**
* class Markdown
*
* Markdown processing in Javascript done right. We have very particular views
* on what constitutes 'right' which include:
*
* - produces well-formed HTML (this means that em and strong nesting is
* important)
*
* - has an intermediate representation to allow processing of parsed data (We
* in fact have two, both as [JsonML]: a markdown tree and an HTML tree).
*
* - is easily extensible to add new dialects without having to rewrite the
* entire parsing mechanics
*
* - has a good test suite
*
* This implementation fulfills all of these (except that the test suite could
* do with expanding to automatically run all the fixtures from other Markdown
* implementations.)
*
* ##### Intermediate Representation
*
* *TODO* Talk about this :) Its JsonML, but document the node names we use.
*
* [JsonML]: http://jsonml.org/ "JSON Markup Language"
**/
var Markdown = function(dialect) {
switch (typeof dialect) {
case "undefined":
this.dialect = Markdown.dialects.Gruber;
break;
case "object":
this.dialect = dialect;
break;
default:
if ( dialect in Markdown.dialects )
this.dialect = Markdown.dialects[dialect];
else
throw new Error("Unknown Markdown dialect '" + String(dialect) + "'");
break;
}
this.em_state = [];
this.strong_state = [];
this.debug_indent = "";
};
/**
* Markdown.dialects
*
* Namespace of built-in dialects.
**/
Markdown.dialects = {};
// Imported functions
var mk_block = Markdown.mk_block = MarkdownHelpers.mk_block,
isArray = MarkdownHelpers.isArray;
/**
* parse( markdown, [dialect] ) -> JsonML
* - markdown (String): markdown string to parse
* - dialect (String | Dialect): the dialect to use, defaults to gruber
*
* Parse `markdown` and return a markdown document as a Markdown.JsonML tree.
**/
Markdown.parse = function( source, dialect ) {
// dialect will default if undefined
var md = new Markdown( dialect );
return md.toTree( source );
};
function count_lines( str ) {
var n = 0,
i = -1;
while ( ( i = str.indexOf("\n", i + 1) ) !== -1 )
n++;
return n;
}
// Internal - split source into rough blocks
Markdown.prototype.split_blocks = function splitBlocks( input ) {
input = input.replace(/(\r\n|\n|\r)/g, "\n");
// [\s\S] matches _anything_ (newline or space)
// [^] is equivalent but doesn't work in IEs.
var re = /([\s\S]+?)($|\n#|\n(?:\s*\n|$)+)/g,
blocks = [],
m;
var line_no = 1;
if ( ( m = /^(\s*\n)/.exec(input) ) !== null ) {
// skip (but count) leading blank lines
line_no += count_lines( m[0] );
re.lastIndex = m[0].length;
}
while ( ( m = re.exec(input) ) !== null ) {
if (m[2] === "\n#") {
m[2] = "\n";
re.lastIndex--;
}
blocks.push( mk_block( m[1], m[2], line_no ) );
line_no += count_lines( m[0] );
}
return blocks;
};
/**
* Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ]
* - block (String): the block to process
* - next (Array): the following blocks
*
* Process `block` and return an array of JsonML nodes representing `block`.
*
* It does this by asking each block level function in the dialect to process
* the block until one can. Succesful handling is indicated by returning an
* array (with zero or more JsonML nodes), failure by a false value.
*
* Blocks handlers are responsible for calling [[Markdown#processInline]]
* themselves as appropriate.
*
* If the blocks were split incorrectly or adjacent blocks need collapsing you
* can adjust `next` in place using shift/splice etc.
*
* If any of this default behaviour is not right for the dialect, you can
* define a `__call__` method on the dialect that will get invoked to handle
* the block processing.
*/
Markdown.prototype.processBlock = function processBlock( block, next ) {
var cbs = this.dialect.block,
ord = cbs.__order__;
if ( "__call__" in cbs )
return cbs.__call__.call(this, block, next);
for ( var i = 0; i < ord.length; i++ ) {
//D:this.debug( "Testing", ord[i] );
var res = cbs[ ord[i] ].call( this, block, next );
if ( res ) {
//D:this.debug(" matched");
if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) )
this.debug(ord[i], "didn't return a proper array");
//D:this.debug( "" );
return res;
}
}
// Uhoh! no match! Should we throw an error?
return [];
};
Markdown.prototype.processInline = function processInline( block ) {
return this.dialect.inline.__call__.call( this, String( block ) );
};
/**
* Markdown#toTree( source ) -> JsonML
* - source (String): markdown source to parse
*
* Parse `source` into a JsonML tree representing the markdown document.
**/
// custom_tree means set this.tree to `custom_tree` and restore old value on return
Markdown.prototype.toTree = function toTree( source, custom_root ) {
var blocks = source instanceof Array ? source : this.split_blocks( source );
// Make tree a member variable so its easier to mess with in extensions
var old_tree = this.tree;
try {
this.tree = custom_root || this.tree || [ "markdown" ];
blocks_loop:
while ( blocks.length ) {
var b = this.processBlock( blocks.shift(), blocks );
// Reference blocks and the like won't return any content
if ( !b.length )
continue blocks_loop;
this.tree.push.apply( this.tree, b );
}
return this.tree;
}
finally {
if ( custom_root )
this.tree = old_tree;
}
};
// Noop by default
Markdown.prototype.debug = function () {
var args = Array.prototype.slice.call( arguments);
args.unshift(this.debug_indent);
if ( typeof print !== "undefined" )
print.apply( print, args );
if ( typeof console !== "undefined" && typeof console.log !== "undefined" )
console.log.apply( null, args );
};
Markdown.prototype.loop_re_over_block = function( re, block, cb ) {
// Dont use /g regexps with this
var m,
b = block.valueOf();
while ( b.length && (m = re.exec(b) ) !== null ) {
b = b.substr( m[0].length );
cb.call(this, m);
}
return b;
};
// Build default order from insertion order.
Markdown.buildBlockOrder = function(d) {
var ord = [];
for ( var i in d ) {
if ( i === "__order__" || i === "__call__" )
continue;
ord.push( i );
}
d.__order__ = ord;
};
// Build patterns for inline matcher
Markdown.buildInlinePatterns = function(d) {
var patterns = [];
for ( var i in d ) {
// __foo__ is reserved and not a pattern
if ( i.match( /^__.*__$/) )
continue;
var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
.replace( /\n/, "\\n" );
patterns.push( i.length === 1 ? l : "(?:" + l + ")" );
}
patterns = patterns.join("|");
d.__patterns__ = patterns;
//print("patterns:", uneval( patterns ) );
var fn = d.__call__;
d.__call__ = function(text, pattern) {
if ( pattern !== undefined )
return fn.call(this, text, pattern);
else
return fn.call(this, text, patterns);
};
};
var extract_attr = MarkdownHelpers.extract_attr;
/**
* renderJsonML( jsonml[, options] ) -> String
* - jsonml (Array): JsonML array to render to XML
* - options (Object): options
*
* Converts the given JsonML into well-formed XML.
*
* The options currently understood are:
*
* - root (Boolean): wether or not the root node should be included in the
* output, or just its children. The default `false` is to not include the
* root itself.
*/
Markdown.renderJsonML = function( jsonml, options ) {
options = options || {};
// include the root element in the rendered output?
options.root = options.root || false;
var content = [];
if ( options.root ) {
content.push( render_tree( jsonml ) );
}
else {
jsonml.shift(); // get rid of the tag
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) )
jsonml.shift(); // get rid of the attributes
while ( jsonml.length )
content.push( render_tree( jsonml.shift() ) );
}
return content.join( "\n\n" );
};
/**
* toHTMLTree( markdown, [dialect] ) -> JsonML
* toHTMLTree( md_tree ) -> JsonML
* - markdown (String): markdown string to parse
* - dialect (String | Dialect): the dialect to use, defaults to gruber
* - md_tree (Markdown.JsonML): parsed markdown tree
*
* Turn markdown into HTML, represented as a JsonML tree. If a string is given
* to this function, it is first parsed into a markdown tree by calling
* [[parse]].
**/
Markdown.toHTMLTree = function toHTMLTree( input, dialect , options ) {
// convert string input to an MD tree
if ( typeof input === "string" )
input = this.parse( input, dialect );
// Now convert the MD tree to an HTML tree
// remove references from the tree
var attrs = extract_attr( input ),
refs = {};
if ( attrs && attrs.references )
refs = attrs.references;
var html = convert_tree_to_html( input, refs , options );
merge_text_nodes( html );
return html;
};
/**
* toHTML( markdown, [dialect] ) -> String
* toHTML( md_tree ) -> String
* - markdown (String): markdown string to parse
* - md_tree (Markdown.JsonML): parsed markdown tree
*
* Take markdown (either as a string or as a JsonML tree) and run it through
* [[toHTMLTree]] then turn it into a well-formated HTML fragment.
**/
Markdown.toHTML = function toHTML( source , dialect , options ) {
var input = this.toHTMLTree( source , dialect , options );
return this.renderJsonML( input );
};
function escapeHTML( text ) {
return text.replace( /&/g, "&amp;" )
.replace( /</g, "&lt;" )
.replace( />/g, "&gt;" )
.replace( /"/g, "&quot;" )
.replace( /'/g, "&#39;" );
}
function render_tree( jsonml ) {
// basic case
if ( typeof jsonml === "string" )
return escapeHTML( jsonml );
var tag = jsonml.shift(),
attributes = {},
content = [];
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) )
attributes = jsonml.shift();
while ( jsonml.length )
content.push( render_tree( jsonml.shift() ) );
var tag_attrs = "";
for ( var a in attributes )
tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"';
// be careful about adding whitespace here for inline elements
if ( tag === "img" || tag === "br" || tag === "hr" )
return "<"+ tag + tag_attrs + "/>";
else
return "<"+ tag + tag_attrs + ">" + content.join( "" ) + "</" + tag + ">";
}
function convert_tree_to_html( tree, references, options ) {
var i;
options = options || {};
// shallow clone
var jsonml = tree.slice( 0 );
if ( typeof options.preprocessTreeNode === "function" )
jsonml = options.preprocessTreeNode(jsonml, references);
// Clone attributes if they exist
var attrs = extract_attr( jsonml );
if ( attrs ) {
jsonml[ 1 ] = {};
for ( i in attrs ) {
jsonml[ 1 ][ i ] = attrs[ i ];
}
attrs = jsonml[ 1 ];
}
// basic case
if ( typeof jsonml === "string" )
return jsonml;
// convert this node
switch ( jsonml[ 0 ] ) {
case "header":
jsonml[ 0 ] = "h" + jsonml[ 1 ].level;
delete jsonml[ 1 ].level;
break;
case "bulletlist":
jsonml[ 0 ] = "ul";
break;
case "numberlist":
jsonml[ 0 ] = "ol";
break;
case "listitem":
jsonml[ 0 ] = "li";
break;
case "para":
jsonml[ 0 ] = "p";
break;
case "markdown":
jsonml[ 0 ] = "html";
if ( attrs )
delete attrs.references;
break;
case "code_block":
jsonml[ 0 ] = "pre";
i = attrs ? 2 : 1;
var code = [ "code" ];
code.push.apply( code, jsonml.splice( i, jsonml.length - i ) );
jsonml[ i ] = code;
break;
case "inlinecode":
jsonml[ 0 ] = "code";
break;
case "img":
jsonml[ 1 ].src = jsonml[ 1 ].href;
delete jsonml[ 1 ].href;
break;
case "linebreak":
jsonml[ 0 ] = "br";
break;
case "link":
jsonml[ 0 ] = "a";
break;
case "link_ref":
jsonml[ 0 ] = "a";
// grab this ref and clean up the attribute node
var ref = references[ attrs.ref ];
// if the reference exists, make the link
if ( ref ) {
delete attrs.ref;
// add in the href and title, if present
attrs.href = ref.href;
if ( ref.title )
attrs.title = ref.title;
// get rid of the unneeded original text
delete attrs.original;
}
// the reference doesn't exist, so revert to plain text
else {
return attrs.original;
}
break;
case "img_ref":
jsonml[ 0 ] = "img";
// grab this ref and clean up the attribute node
var ref = references[ attrs.ref ];
// if the reference exists, make the link
if ( ref ) {
delete attrs.ref;
// add in the href and title, if present
attrs.src = ref.href;
if ( ref.title )
attrs.title = ref.title;
// get rid of the unneeded original text
delete attrs.original;
}
// the reference doesn't exist, so revert to plain text
else {
return attrs.original;
}
break;
}
// convert all the children
i = 1;
// deal with the attribute node, if it exists
if ( attrs ) {
// if there are keys, skip over it
for ( var key in jsonml[ 1 ] ) {
i = 2;
break;
}
// if there aren't, remove it
if ( i === 1 )
jsonml.splice( i, 1 );
}
for ( ; i < jsonml.length; ++i ) {
jsonml[ i ] = convert_tree_to_html( jsonml[ i ], references, options );
}
return jsonml;
}
// merges adjacent text nodes into a single node
function merge_text_nodes( jsonml ) {
// skip the tag name and attribute hash
var i = extract_attr( jsonml ) ? 2 : 1;
while ( i < jsonml.length ) {
// if it's a string check the next item too
if ( typeof jsonml[ i ] === "string" ) {
if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) {
// merge the second string into the first and remove it
jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ];
}
else {
++i;
}
}
// if it's not a string recurse
else {
merge_text_nodes( jsonml[ i ] );
++i;
}
}
};
var DialectHelpers = {};
DialectHelpers.inline_until_char = function( text, want ) {
var consumed = 0,
nodes = [];
while ( true ) {
if ( text.charAt( consumed ) === want ) {
// Found the character we were looking for
consumed++;
return [ consumed, nodes ];
}
if ( consumed >= text.length ) {
// No closing char found. Abort.
return null;
}
var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) );
consumed += res[ 0 ];
// Add any returned nodes.
nodes.push.apply( nodes, res.slice( 1 ) );
}
};
// Helper function to make sub-classing a dialect easier
DialectHelpers.subclassDialect = function( d ) {
function Block() {}
Block.prototype = d.block;
function Inline() {}
Inline.prototype = d.inline;
return { block: new Block(), inline: new Inline() };
};
var forEach = MarkdownHelpers.forEach,
extract_attr = MarkdownHelpers.extract_attr,
mk_block = MarkdownHelpers.mk_block,
isEmpty = MarkdownHelpers.isEmpty,
inline_until_char = DialectHelpers.inline_until_char;
/**
* Gruber dialect
*
* The default dialect that follows the rules set out by John Gruber's
* markdown.pl as closely as possible. Well actually we follow the behaviour of
* that script which in some places is not exactly what the syntax web page
* says.
**/
var Gruber = {
block: {
atxHeader: function atxHeader( block, next ) {
var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ );
if ( !m )
return undefined;
var header = [ "header", { level: m[ 1 ].length } ];
Array.prototype.push.apply(header, this.processInline(m[ 2 ]));
if ( m[0].length < block.length )
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
return [ header ];
},
setextHeader: function setextHeader( block, next ) {
var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ );
if ( !m )
return undefined;
var level = ( m[ 2 ] === "=" ) ? 1 : 2,
header = [ "header", { level : level }, m[ 1 ] ];
if ( m[0].length < block.length )
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
return [ header ];
},
code: function code( block, next ) {
// | Foo
// |bar
// should be a code block followed by a paragraph. Fun
//
// There might also be adjacent code block to merge.
var ret = [],
re = /^(?: {0,3}\t| {4})(.*)\n?/;
// 4 spaces + content
if ( !block.match( re ) )
return undefined;
block_search:
do {
// Now pull out the rest of the lines
var b = this.loop_re_over_block(
re, block.valueOf(), function( m ) { ret.push( m[1] ); } );
if ( b.length ) {
// Case alluded to in first comment. push it back on as a new block
next.unshift( mk_block(b, block.trailing) );
break block_search;
}
else if ( next.length ) {
// Check the next block - it might be code too
if ( !next[0].match( re ) )
break block_search;
// Pull how how many blanks lines follow - minus two to account for .join
ret.push ( block.trailing.replace(/[^\n]/g, "").substring(2) );
block = next.shift();
}
else {
break block_search;
}
} while ( true );
return [ [ "code_block", ret.join("\n") ] ];
},
horizRule: function horizRule( block, next ) {
// this needs to find any hr in the block to handle abutting blocks
var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ );
if ( !m )
return undefined;
var jsonml = [ [ "hr" ] ];
// if there's a leading abutting block, process it
if ( m[ 1 ] ) {
var contained = mk_block( m[ 1 ], "", block.lineNumber );
jsonml.unshift.apply( jsonml, this.toTree( contained, [] ) );
}
// if there's a trailing abutting block, stick it into next
if ( m[ 3 ] )
next.unshift( mk_block( m[ 3 ], block.trailing, block.lineNumber + 1 ) );
return jsonml;
},
// There are two types of lists. Tight and loose. Tight lists have no whitespace
// between the items (and result in text just in the <li>) and loose lists,
// which have an empty line between list items, resulting in (one or more)
// paragraphs inside the <li>.
//
// There are all sorts weird edge cases about the original markdown.pl's
// handling of lists:
//
// * Nested lists are supposed to be indented by four chars per level. But
// if they aren't, you can get a nested list by indenting by less than
// four so long as the indent doesn't match an indent of an existing list
// item in the 'nest stack'.
//
// * The type of the list (bullet or number) is controlled just by the
// first item at the indent. Subsequent changes are ignored unless they
// are for nested lists
//
lists: (function( ) {
// Use a closure to hide a few variables.
var any_list = "[*+-]|\\d+\\.",
bullet_list = /[*+-]/,
// Capture leading indent as it matters for determining nested lists.
is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ),
indent_re = "(?: {0,3}\\t| {4})";
// TODO: Cache this regexp for certain depths.
// Create a regexp suitable for matching an li for a given stack depth
function regex_for_depth( depth ) {
return new RegExp(
// m[1] = indent, m[2] = list_type
"(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" +
// m[3] = cont
"(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})"
);
}
function expand_tab( input ) {
return input.replace( / {0,3}\t/g, " " );
}
// Add inline content `inline` to `li`. inline comes from processInline
// so is an array of content
function add(li, loose, inline, nl) {
if ( loose ) {
li.push( [ "para" ].concat(inline) );
return;
}
// Hmmm, should this be any block level element or just paras?
var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] === "para"
? li[li.length -1]
: li;
// If there is already some content in this list, add the new line in
if ( nl && li.length > 1 )
inline.unshift(nl);
for ( var i = 0; i < inline.length; i++ ) {
var what = inline[i],
is_str = typeof what === "string";
if ( is_str && add_to.length > 1 && typeof add_to[add_to.length-1] === "string" )
add_to[ add_to.length-1 ] += what;
else
add_to.push( what );
}
}
// contained means have an indent greater than the current one. On
// *every* line in the block
function get_contained_blocks( depth, blocks ) {
var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ),
replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"),
ret = [];
while ( blocks.length > 0 ) {
if ( re.exec( blocks[0] ) ) {
var b = blocks.shift(),
// Now remove that indent
x = b.replace( replace, "");
ret.push( mk_block( x, b.trailing, b.lineNumber ) );
}
else
break;
}
return ret;
}
// passed to stack.forEach to turn list items up the stack into paras
function paragraphify(s, i, stack) {
var list = s.list;
var last_li = list[list.length-1];
if ( last_li[1] instanceof Array && last_li[1][0] === "para" )
return;
if ( i + 1 === stack.length ) {
// Last stack frame
// Keep the same array, but replace the contents
last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ) );
}
else {
var sublist = last_li.pop();
last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ), sublist );
}
}
// The matcher function
return function( block, next ) {
var m = block.match( is_list_re );
if ( !m )
return undefined;
function make_list( m ) {
var list = bullet_list.exec( m[2] )
? ["bulletlist"]
: ["numberlist"];
stack.push( { list: list, indent: m[1] } );
return list;
}
var stack = [], // Stack of lists for nesting.
list = make_list( m ),
last_li,
loose = false,
ret = [ stack[0].list ],
i;
// Loop to search over block looking for inner block elements and loose lists
loose_search:
while ( true ) {
// Split into lines preserving new lines at end of line
var lines = block.split( /(?=\n)/ );
// We have to grab all lines for a li and call processInline on them
// once as there are some inline things that can span lines.
var li_accumulate = "", nl = "";
// Loop over the lines in this block looking for tight lists.
tight_search:
for ( var line_no = 0; line_no < lines.length; line_no++ ) {
nl = "";
var l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; });
// TODO: really should cache this
var line_re = regex_for_depth( stack.length );
m = l.match( line_re );
//print( "line:", uneval(l), "\nline match:", uneval(m) );
// We have a list item
if ( m[1] !== undefined ) {
// Process the previous list item, if any
if ( li_accumulate.length ) {
add( last_li, loose, this.processInline( li_accumulate ), nl );
// Loose mode will have been dealt with. Reset it
loose = false;
li_accumulate = "";
}
m[1] = expand_tab( m[1] );
var wanted_depth = Math.floor(m[1].length/4)+1;
//print( "want:", wanted_depth, "stack:", stack.length);
if ( wanted_depth > stack.length ) {
// Deep enough for a nested list outright
//print ( "new nested list" );
list = make_list( m );
last_li.push( list );
last_li = list[1] = [ "listitem" ];
}
else {
// We aren't deep enough to be strictly a new level. This is
// where Md.pl goes nuts. If the indent matches a level in the
// stack, put it there, else put it one deeper then the
// wanted_depth deserves.
var found = false;
for ( i = 0; i < stack.length; i++ ) {
if ( stack[ i ].indent !== m[1] )
continue;
list = stack[ i ].list;
stack.splice( i+1, stack.length - (i+1) );
found = true;
break;
}
if (!found) {
//print("not found. l:", uneval(l));
wanted_depth++;
if ( wanted_depth <= stack.length ) {
stack.splice(wanted_depth, stack.length - wanted_depth);
//print("Desired depth now", wanted_depth, "stack:", stack.length);
list = stack[wanted_depth-1].list;
//print("list:", uneval(list) );
}
else {
//print ("made new stack for messy indent");
list = make_list(m);
last_li.push(list);
}
}
//print( uneval(list), "last", list === stack[stack.length-1].list );
last_li = [ "listitem" ];
list.push(last_li);
} // end depth of shenegains
nl = "";
}
// Add content
if ( l.length > m[0].length )
li_accumulate += nl + l.substr( m[0].length );
} // tight_search
if ( li_accumulate.length ) {
add( last_li, loose, this.processInline( li_accumulate ), nl );
// Loose mode will have been dealt with. Reset it
loose = false;
li_accumulate = "";
}
// Look at the next block - we might have a loose list. Or an extra
// paragraph for the current li
var contained = get_contained_blocks( stack.length, next );
// Deal with code blocks or properly nested lists
if ( contained.length > 0 ) {
// Make sure all listitems up the stack are paragraphs
forEach( stack, paragraphify, this);
last_li.push.apply( last_li, this.toTree( contained, [] ) );
}
var next_block = next[0] && next[0].valueOf() || "";
if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) {
block = next.shift();
// Check for an HR following a list: features/lists/hr_abutting
var hr = this.dialect.block.horizRule( block, next );
if ( hr ) {
ret.push.apply(ret, hr);
break;
}
// Make sure all listitems up the stack are paragraphs
forEach( stack, paragraphify, this);
loose = true;
continue loose_search;
}
break;
} // loose_search
return ret;
};
})(),
blockquote: function blockquote( block, next ) {
if ( !block.match( /^>/m ) )
return undefined;
var jsonml = [];
// separate out the leading abutting block, if any. I.e. in this case:
//
// a
// > b
//
if ( block[ 0 ] !== ">" ) {
var lines = block.split( /\n/ ),
prev = [],
line_no = block.lineNumber;
// keep shifting lines until you find a crotchet
while ( lines.length && lines[ 0 ][ 0 ] !== ">" ) {
prev.push( lines.shift() );
line_no++;
}
var abutting = mk_block( prev.join( "\n" ), "\n", block.lineNumber );
jsonml.push.apply( jsonml, this.processBlock( abutting, [] ) );
// reassemble new block of just block quotes!
block = mk_block( lines.join( "\n" ), block.trailing, line_no );
}
// if the next block is also a blockquote merge it in
while ( next.length && next[ 0 ][ 0 ] === ">" ) {
var b = next.shift();
block = mk_block( block + block.trailing + b, b.trailing, block.lineNumber );
}
// Strip off the leading "> " and re-process as a block.
var input = block.replace( /^> ?/gm, "" ),
old_tree = this.tree,
processedBlock = this.toTree( input, [ "blockquote" ] ),
attr = extract_attr( processedBlock );
// If any link references were found get rid of them
if ( attr && attr.references ) {
delete attr.references;
// And then remove the attribute object if it's empty
if ( isEmpty( attr ) )
processedBlock.splice( 1, 1 );
}
jsonml.push( processedBlock );
return jsonml;
},
referenceDefn: function referenceDefn( block, next) {
var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
// interesting matches are [ , ref_id, url, , title, title ]
if ( !block.match(re) )
return undefined;
// make an attribute node if it doesn't exist
if ( !extract_attr( this.tree ) )
this.tree.splice( 1, 0, {} );
var attrs = extract_attr( this.tree );
// make a references hash if it doesn't exist
if ( attrs.references === undefined )
attrs.references = {};
var b = this.loop_re_over_block(re, block, function( m ) {
if ( m[2] && m[2][0] === "<" && m[2][m[2].length-1] === ">" )
m[2] = m[2].substring( 1, m[2].length - 1 );
var ref = attrs.references[ m[1].toLowerCase() ] = {
href: m[2]
};
if ( m[4] !== undefined )
ref.title = m[4];
else if ( m[5] !== undefined )
ref.title = m[5];
} );
if ( b.length )
next.unshift( mk_block( b, block.trailing ) );
return [];
},
para: function para( block ) {
// everything's a para!
return [ ["para"].concat( this.processInline( block ) ) ];
}
},
inline: {
__oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) {
var m,
res;
patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__;
var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" );
m = re.exec( text );
if (!m) {
// Just boring text
return [ text.length, text ];
}
else if ( m[1] ) {
// Some un-interesting text matched. Return that first
return [ m[1].length, m[1] ];
}
var res;
if ( m[2] in this.dialect.inline ) {
res = this.dialect.inline[ m[2] ].call(
this,
text.substr( m.index ), m, previous_nodes || [] );
}
// Default for now to make dev easier. just slurp special and output it.
res = res || [ m[2].length, m[2] ];
return res;
},
__call__: function inline( text, patterns ) {
var out = [],
res;
function add(x) {
//D:self.debug(" adding output", uneval(x));
if ( typeof x === "string" && typeof out[out.length-1] === "string" )
out[ out.length-1 ] += x;
else
out.push(x);
}
while ( text.length > 0 ) {
res = this.dialect.inline.__oneElement__.call(this, text, patterns, out );
text = text.substr( res.shift() );
forEach(res, add );
}
return out;
},
// These characters are intersting elsewhere, so have rules for them so that
// chunks of plain text blocks don't include them
"]": function () {},
"}": function () {},
__escape__ : /^\\[\\`\*_{}\[\]()#\+.!\-]/,
"\\": function escaped( text ) {
// [ length of input processed, node/children to add... ]
// Only esacape: \ ` * _ { } [ ] ( ) # * + - . !
if ( this.dialect.inline.__escape__.exec( text ) )
return [ 2, text.charAt( 1 ) ];
else
// Not an esacpe
return [ 1, "\\" ];
},
"![": function image( text ) {
// Unlike images, alt text is plain text only. no other elements are
// allowed in there
// ![Alt text](/path/to/img.jpg "Optional title")
// 1 2 3 4 <--- captures
var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ );
if ( m ) {
if ( m[2] && m[2][0] === "<" && m[2][m[2].length-1] === ">" )
m[2] = m[2].substring( 1, m[2].length - 1 );
m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0];
var attrs = { alt: m[1], href: m[2] || "" };
if ( m[4] !== undefined)
attrs.title = m[4];
return [ m[0].length, [ "img", attrs ] ];
}
// ![Alt text][id]
m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ );
if ( m ) {
// We can't check if the reference is known here as it likely wont be
// found till after. Check it in md tree->hmtl tree conversion
return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ];
}
// Just consume the '!['
return [ 2, "![" ];
},
"[": function link( text ) {
var orig = String(text);
// Inline content is possible inside `link text`
var res = inline_until_char.call( this, text.substr(1), "]" );
// No closing ']' found. Just consume the [
if ( !res )
return [ 1, "[" ];
var consumed = 1 + res[ 0 ],
children = res[ 1 ],
link,
attrs;
// At this point the first [...] has been parsed. See what follows to find
// out which kind of link we are (reference or direct url)
text = text.substr( consumed );
// [link text](/path/to/img.jpg "Optional title")
// 1 2 3 <--- captures
// This will capture up to the last paren in the block. We then pull
// back based on if there a matching ones in the url
// ([here](/url/(test))
// The parens have to be balanced
var m = text.match( /^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ );
if ( m ) {
var url = m[1];
consumed += m[0].length;
if ( url && url[0] === "<" && url[url.length-1] === ">" )
url = url.substring( 1, url.length - 1 );
// If there is a title we don't have to worry about parens in the url
if ( !m[3] ) {
var open_parens = 1; // One open that isn't in the capture
for ( var len = 0; len < url.length; len++ ) {
switch ( url[len] ) {
case "(":
open_parens++;
break;
case ")":
if ( --open_parens === 0) {
consumed -= url.length - len;
url = url.substring(0, len);
}
break;
}
}
}
// Process escapes only
url = this.dialect.inline.__call__.call( this, url, /\\/ )[0];
attrs = { href: url || "" };
if ( m[3] !== undefined)
attrs.title = m[3];
link = [ "link", attrs ].concat( children );
return [ consumed, link ];
}
// [Alt text][id]
// [Alt text] [id]
m = text.match( /^\s*\[(.*?)\]/ );
if ( m ) {
consumed += m[ 0 ].length;
// [links][] uses links as its reference
attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) };
link = [ "link_ref", attrs ].concat( children );
// We can't check if the reference is known here as it likely wont be
// found till after. Check it in md tree->hmtl tree conversion.
// Store the original so that conversion can revert if the ref isn't found.
return [ consumed, link ];
}
// [id]
// Only if id is plain (no formatting.)
if ( children.length === 1 && typeof children[0] === "string" ) {
attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) };
link = [ "link_ref", attrs, children[0] ];
return [ consumed, link ];
}
// Just consume the "["
return [ 1, "[" ];
},
"<": function autoLink( text ) {
var m;
if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) !== null ) {
if ( m[3] )
return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ];
else if ( m[2] === "mailto" )
return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ];
else
return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ];
}
return [ 1, "<" ];
},
"`": function inlineCode( text ) {
// Inline code block. as many backticks as you like to start it
// Always skip over the opening ticks.
var m = text.match( /(`+)(([\s\S]*?)\1)/ );
if ( m && m[2] )
return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ];
else {
// TODO: No matching end code found - warn!
return [ 1, "`" ];
}
},
" \n": function lineBreak() {
return [ 3, [ "linebreak" ] ];
}
}
};
// Meta Helper/generator method for em and strong handling
function strong_em( tag, md ) {
var state_slot = tag + "_state",
other_slot = tag === "strong" ? "em_state" : "strong_state";
function CloseTag(len) {
this.len_after = len;
this.name = "close_" + md;
}
return function ( text ) {
if ( this[state_slot][0] === md ) {
// Most recent em is of this type
//D:this.debug("closing", md);
this[state_slot].shift();
// "Consume" everything to go back to the recrusion in the else-block below
return[ text.length, new CloseTag(text.length-md.length) ];
}
else {
// Store a clone of the em/strong states
var other = this[other_slot].slice(),
state = this[state_slot].slice();
this[state_slot].unshift(md);
//D:this.debug_indent += " ";
// Recurse
var res = this.processInline( text.substr( md.length ) );
//D:this.debug_indent = this.debug_indent.substr(2);
var last = res[res.length - 1];
//D:this.debug("processInline from", tag + ": ", uneval( res ) );
var check = this[state_slot].shift();
if ( last instanceof CloseTag ) {
res.pop();
// We matched! Huzzah.
var consumed = text.length - last.len_after;
return [ consumed, [ tag ].concat(res) ];
}
else {
// Restore the state of the other kind. We might have mistakenly closed it.
this[other_slot] = other;
this[state_slot] = state;
// We can't reuse the processed result as it could have wrong parsing contexts in it.
return [ md.length, md ];
}
}
}; // End returned function
}
Gruber.inline["**"] = strong_em("strong", "**");
Gruber.inline["__"] = strong_em("strong", "__");
Gruber.inline["*"] = strong_em("em", "*");
Gruber.inline["_"] = strong_em("em", "_");
Markdown.dialects.Gruber = Gruber;
Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block );
Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline );
var Maruku = DialectHelpers.subclassDialect( Gruber ),
extract_attr = MarkdownHelpers.extract_attr,
forEach = MarkdownHelpers.forEach;
Maruku.processMetaHash = function processMetaHash( meta_string ) {
var meta = split_meta_hash( meta_string ),
attr = {};
for ( var i = 0; i < meta.length; ++i ) {
// id: #foo
if ( /^#/.test( meta[ i ] ) )
attr.id = meta[ i ].substring( 1 );
// class: .foo
else if ( /^\./.test( meta[ i ] ) ) {
// if class already exists, append the new one
if ( attr["class"] )
attr["class"] = attr["class"] + meta[ i ].replace( /./, " " );
else
attr["class"] = meta[ i ].substring( 1 );
}
// attribute: foo=bar
else if ( /\=/.test( meta[ i ] ) ) {
var s = meta[ i ].split( /\=/ );
attr[ s[ 0 ] ] = s[ 1 ];
}
}
return attr;
};
function split_meta_hash( meta_string ) {
var meta = meta_string.split( "" ),
parts = [ "" ],
in_quotes = false;
while ( meta.length ) {
var letter = meta.shift();
switch ( letter ) {
case " " :
// if we're in a quoted section, keep it
if ( in_quotes )
parts[ parts.length - 1 ] += letter;
// otherwise make a new part
else
parts.push( "" );
break;
case "'" :
case '"' :
// reverse the quotes and move straight on
in_quotes = !in_quotes;
break;
case "\\" :
// shift off the next letter to be used straight away.
// it was escaped so we'll keep it whatever it is
letter = meta.shift();
/* falls through */
default :
parts[ parts.length - 1 ] += letter;
break;
}
}
return parts;
}
Maruku.block.document_meta = function document_meta( block ) {
// we're only interested in the first block
if ( block.lineNumber > 1 )
return undefined;
// document_meta blocks consist of one or more lines of `Key: Value\n`
if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) )
return undefined;
// make an attribute node if it doesn't exist
if ( !extract_attr( this.tree ) )
this.tree.splice( 1, 0, {} );
var pairs = block.split( /\n/ );
for ( var p in pairs ) {
var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ),
key = m[ 1 ].toLowerCase(),
value = m[ 2 ];
this.tree[ 1 ][ key ] = value;
}
// document_meta produces no content!
return [];
};
Maruku.block.block_meta = function block_meta( block ) {
// check if the last line of the block is an meta hash
var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ );
if ( !m )
return undefined;
// process the meta hash
var attr = this.dialect.processMetaHash( m[ 2 ] ),
hash;
// if we matched ^ then we need to apply meta to the previous block
if ( m[ 1 ] === "" ) {
var node = this.tree[ this.tree.length - 1 ];
hash = extract_attr( node );
// if the node is a string (rather than JsonML), bail
if ( typeof node === "string" )
return undefined;
// create the attribute hash if it doesn't exist
if ( !hash ) {
hash = {};
node.splice( 1, 0, hash );
}
// add the attributes in
for ( var a in attr )
hash[ a ] = attr[ a ];
// return nothing so the meta hash is removed
return [];
}
// pull the meta hash off the block and process what's left
var b = block.replace( /\n.*$/, "" ),
result = this.processBlock( b, [] );
// get or make the attributes hash
hash = extract_attr( result[ 0 ] );
if ( !hash ) {
hash = {};
result[ 0 ].splice( 1, 0, hash );
}
// attach the attributes to the block
for ( var a in attr )
hash[ a ] = attr[ a ];
return result;
};
Maruku.block.definition_list = function definition_list( block, next ) {
// one or more terms followed by one or more definitions, in a single block
var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,
list = [ "dl" ],
i, m;
// see if we're dealing with a tight or loose block
if ( ( m = block.match( tight ) ) ) {
// pull subsequent tight DL blocks out of `next`
var blocks = [ block ];
while ( next.length && tight.exec( next[ 0 ] ) )
blocks.push( next.shift() );
for ( var b = 0; b < blocks.length; ++b ) {
var m = blocks[ b ].match( tight ),
terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ),
defns = m[ 2 ].split( /\n:\s+/ );
// print( uneval( m ) );
for ( i = 0; i < terms.length; ++i )
list.push( [ "dt", terms[ i ] ] );
for ( i = 0; i < defns.length; ++i ) {
// run inline processing over the definition
list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) );
}
}
}
else {
return undefined;
}
return [ list ];
};
// splits on unescaped instances of @ch. If @ch is not a character the result
// can be unpredictable
Maruku.block.table = function table ( block ) {
var _split_on_unescaped = function( s, ch ) {
ch = ch || '\\s';
if ( ch.match(/^[\\|\[\]{}?*.+^$]$/) )
ch = '\\' + ch;
var res = [ ],
r = new RegExp('^((?:\\\\.|[^\\\\' + ch + '])*)' + ch + '(.*)'),
m;
while ( ( m = s.match( r ) ) ) {
res.push( m[1] );
s = m[2];
}
res.push(s);
return res;
};
var leading_pipe = /^ {0,3}\|(.+)\n {0,3}\|\s*([\-:]+[\-| :]*)\n((?:\s*\|.*(?:\n|$))*)(?=\n|$)/,
// find at least an unescaped pipe in each line
no_leading_pipe = /^ {0,3}(\S(?:\\.|[^\\|])*\|.*)\n {0,3}([\-:]+\s*\|[\-| :]*)\n((?:(?:\\.|[^\\|])*\|.*(?:\n|$))*)(?=\n|$)/,
i,
m;
if ( ( m = block.match( leading_pipe ) ) ) {
// remove leading pipes in contents
// (header and horizontal rule already have the leading pipe left out)
m[3] = m[3].replace(/^\s*\|/gm, '');
} else if ( ! ( m = block.match( no_leading_pipe ) ) ) {
return undefined;
}
var table = [ "table", [ "thead", [ "tr" ] ], [ "tbody" ] ];
// remove trailing pipes, then split on pipes
// (no escaped pipes are allowed in horizontal rule)
m[2] = m[2].replace(/\|\s*$/, '').split('|');
// process alignment
var html_attrs = [ ];
forEach (m[2], function (s) {
if (s.match(/^\s*-+:\s*$/))
html_attrs.push({align: "right"});
else if (s.match(/^\s*:-+\s*$/))
html_attrs.push({align: "left"});
else if (s.match(/^\s*:-+:\s*$/))
html_attrs.push({align: "center"});
else
html_attrs.push({});
});
// now for the header, avoid escaped pipes
m[1] = _split_on_unescaped(m[1].replace(/\|\s*$/, ''), '|');
for (i = 0; i < m[1].length; i++) {
table[1][1].push(['th', html_attrs[i] || {}].concat(
this.processInline(m[1][i].trim())));
}
// now for body contents
forEach (m[3].replace(/\|\s*$/mg, '').split('\n'), function (row) {
var html_row = ['tr'];
row = _split_on_unescaped(row, '|');
for (i = 0; i < row.length; i++)
html_row.push(['td', html_attrs[i] || {}].concat(this.processInline(row[i].trim())));
table[2].push(html_row);
}, this);
return [table];
};
Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) {
if ( !out.length )
return [ 2, "{:" ];
// get the preceeding element
var before = out[ out.length - 1 ];
if ( typeof before === "string" )
return [ 2, "{:" ];
// match a meta hash
var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ );
// no match, false alarm
if ( !m )
return [ 2, "{:" ];
// attach the attributes to the preceeding element
var meta = this.dialect.processMetaHash( m[ 1 ] ),
attr = extract_attr( before );
if ( !attr ) {
attr = {};
before.splice( 1, 0, attr );
}
for ( var k in meta )
attr[ k ] = meta[ k ];
// cut out the string and replace it with nothing
return [ m[ 0 ].length, "" ];
};
Markdown.dialects.Maruku = Maruku;
Markdown.dialects.Maruku.inline.__escape__ = /^\\[\\`\*_{}\[\]()#\+.!\-|:]/;
Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block );
Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline );
// Include all our depndencies and;
expose.Markdown = Markdown;
expose.parse = Markdown.parse;
expose.toHTML = Markdown.toHTML;
expose.toHTMLTree = Markdown.toHTMLTree;
expose.renderJsonML = Markdown.renderJsonML;
})(function() {
window.markdown = {};
return window.markdown;
}());
},{"util":30}]},{},[1])
(1)
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment