|
/* Copyright © 2012-2014 Jamie Zawinski <jwz@dnalounge.com> |
|
|
|
Permission to use, copy, modify, distribute, and sell this software |
|
and its documentation for any purpose is hereby granted without |
|
fee, provided that the above copyright notice appear in all copies |
|
and that both that copyright notice and this permission notice |
|
appear in supporting documentation. No representations are made |
|
about the suitability of this software for any purpose. It is |
|
provided "as is" without express or implied warranty. |
|
|
|
Loading this code causes "drag left" and "drag right" gestures |
|
to load the previous and next page, respectively, as per the |
|
LINK REL="prev" and "next" tags in the document. If there's |
|
no "prev"/"next", it uses "up" instead. |
|
|
|
It also binds left-arrow and right-arrow. |
|
*/ |
|
|
|
var ce = null; |
|
var drag_start_x = null; |
|
var drag_start_y = null; |
|
var drag_current_x = null; |
|
var drag_current_y = null; |
|
var drag_debug = true; |
|
|
|
function load_page(url, success, failure) { |
|
// Loads the specified url into an HTML document; calls the success |
|
// callback with the document. Returns the XMLHttpRequest object-- set its |
|
// cancelled property to something truthy to cancel the request. |
|
var doc = document.implementation.createHTMLDocument("fragment"); |
|
var req = new XMLHttpRequest(); |
|
req.onreadystatechange = function () { |
|
if (req.cancelled) { return; } |
|
if (req.readyState === 4) { |
|
if (req.status >= 200 && req.status < 300) { |
|
doc.documentElement.innerHTML = req.responseText; |
|
success(doc); |
|
} else { |
|
if (failure) { |
|
failure(req); |
|
} |
|
} |
|
req.onreadystatechange = function() { }; |
|
} |
|
}.bind(this); |
|
|
|
req.open("GET", url, true); |
|
req.responseType = ""; |
|
req.send(); |
|
return req; |
|
} |
|
|
|
function style_outer_container(width) { |
|
var oc = document.getElementById("outerContainer"); |
|
oc.style.width = width + "px"; |
|
} |
|
|
|
// PagePreview objects manage the fragments of HTML necessary to show the |
|
// previews of the next and previous pages. |
|
function PagePreview(forward_p) { |
|
this.forward_p = forward_p; |
|
} |
|
|
|
PagePreview.prototype.load = function() { |
|
// Start loading the preview, if we haven't already. |
|
if (!this.loading_p) { |
|
this.loading_p = true; |
|
this.url = drag_url(this.forward_p, false); |
|
if (this.url) { |
|
var width = document.documentElement.offsetWidth; |
|
|
|
style_outer_container(width); |
|
this.create_element(width); |
|
|
|
// Load the page fragment from the URL. |
|
this.request = load_page(this.url, function loadSuccess(document) { |
|
this.document = document; |
|
var otherElement = this.document.getElementById("thisPage"); |
|
if (otherElement) { |
|
this.element.innerHTML = otherElement.innerHTML; |
|
this.loaded_p = true; |
|
} |
|
}.bind(this)); |
|
|
|
ce.appendChild(this.element); |
|
} |
|
} |
|
}; |
|
|
|
PagePreview.prototype.reset = function() { |
|
// Clear out all the internal state that might be going on. |
|
if (this.element) { |
|
this.element.innerHTML = ''; |
|
} |
|
if (this.req) { |
|
// Make sure any completing outstanding XHR doesn't do any harm. |
|
this.req.cancelled = true; |
|
} |
|
|
|
delete this.document; |
|
delete this.url; |
|
delete this.element; |
|
delete this.loading_p; |
|
delete this.loaded_p; |
|
delete this.req; |
|
}; |
|
|
|
PagePreview.prototype.create_element = function(width) { |
|
this.element = document.createElement("div"); |
|
this.element.style.position = "absolute"; |
|
this.element.style.top = "0px"; |
|
this.element.style.width = width + "px"; |
|
this.element.style.left = (this.forward_p ? "+" : "-") + width + "px"; |
|
}; |
|
|
|
PagePreview.prev = new PagePreview(false); |
|
PagePreview.next = new PagePreview(true); |
|
|
|
function drag_zoomed_p() { |
|
// If the page is pinch-zoomed in, don't do any of this stuff. |
|
// But note that sometimes it gets zoomed in by 1px at random when |
|
// dragging, so only consider it zoomed when it's a couple percent. |
|
var r = window.innerWidth / document.documentElement.clientWidth; |
|
return (r < 0.98); |
|
} |
|
|
|
function drag_start(e) { // finger down |
|
drag_start_x = e.pageX; |
|
drag_start_y = e.pageY; |
|
drag_current_x = drag_start_x; |
|
drag_current_y = drag_start_y; |
|
if (drag_debug) |
|
console.log ("drag_start " + |
|
drag_start_x + " " + drag_start_y + ", " + |
|
drag_current_x + " " + drag_current_y); |
|
} |
|
|
|
function drag_stop(e, good_p) { // finger up |
|
var xoff = drag_current_x - drag_start_x; |
|
var yoff = drag_current_y - drag_start_y; |
|
|
|
ce.style.left = "0"; |
|
// ce.style.top = "0"; |
|
drag_start_x = null; |
|
drag_start_y = null; |
|
drag_current_x = null; |
|
drag_current_y = null; |
|
|
|
var xratio = Math.abs(xoff) / window.innerWidth; |
|
var yratio = Math.abs(yoff) / window.innerWidth; // use width for both |
|
|
|
if (drag_debug) |
|
console.log ("drag_stop " + xoff + " " + yoff + ", " + |
|
xratio + " " + yratio); |
|
|
|
if (yratio > xratio) { |
|
good_p = false; |
|
if (drag_debug) |
|
console.log ("drag_stop vertical"); |
|
} else if (xratio < 0.33) { |
|
good_p = false; |
|
if (drag_debug) |
|
console.log ("drag_stop short horizontal"); |
|
} |
|
|
|
if (good_p) { |
|
drag_nav (xoff < 0); |
|
} |
|
} |
|
|
|
function drag_url(forward_p, up_p) { |
|
var next = null; |
|
var up = null; |
|
var links = document.getElementsByTagName("link"); |
|
for (var i = 0; i < links.length; i++) { |
|
var link = links[i]; |
|
if (link.rel === (forward_p ? "next" : "prev")) |
|
next = link.href; |
|
else if (link.rel === "up" && up_p) |
|
up = link.href; |
|
} |
|
|
|
return next || up; |
|
} |
|
|
|
function drag_moved(e) { // finger moved |
|
if (drag_zoomed_p()) { |
|
if (drag_debug) |
|
console.log ("drag_moved while zoomed, " + |
|
window.innerWidth + ", " + |
|
document.documentElement.clientWidth); |
|
} else if (drag_start_x == null) { |
|
if (drag_debug) |
|
console.log ("drag_moved without start"); |
|
} else if (e.touches && (e.touches.length != 1)) { |
|
// Multi-touch started: abort dragging. |
|
if (drag_debug) |
|
console.log ("drag_moved multi-touch " + e.touches.length); |
|
drag_stop (e, false); |
|
} else { |
|
drag_current_x = e.pageX; |
|
drag_current_y = e.pageY; |
|
var xoff = drag_current_x - drag_start_x; |
|
var yoff = drag_current_y - drag_start_y; |
|
|
|
ce.style.left = xoff + "px"; |
|
// ce.style.top = yoff + "px"; |
|
if (drag_debug) |
|
console.log("drag_moved " + |
|
drag_current_x + " - " + drag_start_x + " = " + xoff + ", " + |
|
drag_current_y + " - " + drag_start_y + " = " + yoff); |
|
|
|
if (Math.abs(yoff) > window.innerHeight * 0.01 && |
|
Math.abs(yoff) > Math.abs(xoff) * 1.1) { |
|
// We appear to be dragging more up/down than left/right. |
|
// Just stop tracking immediately to end the flickery hell. |
|
if (drag_debug) |
|
console.log ("drag_moved vertical, abort"); |
|
drag_stop (e, false); |
|
} else { |
|
e.preventDefault(); |
|
|
|
var forward_p = xoff < 0; |
|
var preview = forward_p ? PagePreview.next : PagePreview.prev; |
|
preview.load(); |
|
} |
|
} |
|
} |
|
|
|
function drag_nav(forward_p) { // load "next" or "up" link. |
|
var preview = forward_p ? PagePreview.next : PagePreview.prev; |
|
if (preview.loaded_p) { |
|
// Remember what the new contents are supposed to be... |
|
var newContents = preview.document.documentElement.innerHTML; |
|
var newUrl = preview.url; |
|
|
|
// Reset the preview state so the new page can do it all over again. |
|
// NOTE: If we were clever we would remember this page as the preview in |
|
// the opposite direction. We are not clever yet. |
|
PagePreview.next.reset(); |
|
PagePreview.prev.reset(); |
|
|
|
// Blow away our contents and update the URL-- POOF ITS LIKE NAVIGATION. |
|
document.documentElement.innerHTML = newContents; |
|
window.history.pushState({}, '', newUrl); |
|
|
|
// But now we need to re-initialize drag navigation for the new contents. |
|
init_drag_nav(); |
|
} else { |
|
// Something went wrong loading the preview; just navigate like normal. |
|
var target = drag_url(forward_p, true); |
|
if (target) { |
|
if (drag_debug) |
|
console.log ("drag_nav " + target); |
|
document.location = target; |
|
} else if (drag_debug) { |
|
console.log ("drag_nav: no links"); |
|
} |
|
} |
|
} |
|
|
|
|
|
function arrow_keys(e) { |
|
e = e || window.event; |
|
if (e.keyCode == 37) { drag_nav (false); } // left-arrow |
|
else if (e.keyCode == 39) { drag_nav (true); } // right-arrow |
|
// up-arrow = 38, down-arrow = 40. |
|
} |
|
|
|
function init_drag_nav() { |
|
ce = document.getElementById("previewContainer"); |
|
if (!ce) { |
|
if (drag_debug) |
|
console.log("drag_nav can't find #previewContainer"); |
|
return; |
|
} |
|
|
|
var n = document.body; |
|
if (drag_debug) |
|
console.log ("drag_nav installed"); |
|
n.addEventListener('touchstart', drag_start, false); |
|
n.addEventListener('touchmove', drag_moved, false); |
|
n.addEventListener('touchend', function(e) { drag_stop (e, true); }, |
|
false); |
|
n.addEventListener('touchcancel',function(e) { drag_stop (e, false); }, |
|
false); |
|
document.onkeyup = arrow_keys; |
|
|
|
// HAX: Let me work the problem without having touch events |
|
n.addEventListener('pointerdown', drag_start, false); |
|
n.addEventListener('pointermove', function(e) { if(drag_start_x) { drag_moved(e); } }, |
|
false); |
|
n.addEventListener('pointerup', function(e) { drag_stop(e, true); }, |
|
false); |
|
n.addEventListener('pointercancel', function(e) { drag_stop(e, false); }, |
|
false); |
|
} |
|
|
|
// When this file is loaded, there is no BODY yet. |
|
// Use the not-supported-in-old-browsers DOM event to wait. |
|
// |
|
// I used to do this portably by firing a timer every 1/10th second |
|
// until BODY existed, but that stopped working for some unknown |
|
// reason. |
|
// |
|
document.addEventListener("DOMContentLoaded", init_drag_nav); |
|
|