Skip to content

Instantly share code, notes, and snippets.

@madevelopers
Forked from cacheleocode/ibooks.js
Last active August 29, 2015 14:15
Show Gist options
  • Save madevelopers/1fa4122a93225a15b9eb to your computer and use it in GitHub Desktop.
Save madevelopers/1fa4122a93225a15b9eb to your computer and use it in GitHub Desktop.
/**
* iBooks JS Framework
* Compatibility: iBooks 1.5+
* Copyright © 2009-2011 Apple Inc. All rights reserved.
*
**/
/**
* @name iBooks
* @namespace
*
* Top-level object containing some core constants providing information about the environment.
*/
var iBooks = {};
/**
* Indicates the version of iBooks JS.
* @constant
* @type String
*/
iBooks.VERSION = '1.0';
/**
* Indicates whether the platform is an iBooks.IS_IPAD.
* @constant
* @type bool
*/
iBooks.IS_IPAD = (navigator.platform == 'iPad');
/**
* Indicates whether the platform supports touches.
* @constant
* @type bool
*/
iBooks.SUPPORTS_TOUCHES = ('createTouch' in document);
/**
* The interaction start event name, returns <code>touchstart</code> if the platform supports touches
* and <code>mousedown</code> if it does not.
* @constant
* @type String
*/
iBooks.START_EVENT = iBooks.SUPPORTS_TOUCHES ? 'touchstart' : 'mousedown';
/**
* The interaction move event name, returns <code>touchmove</code> if the platform supports touches
* and <code>mousemove</code> if it does not.
* @constant
* @type String
*/
iBooks.MOVE_EVENT = iBooks.SUPPORTS_TOUCHES ? 'touchmove' : 'mousemove';
/**
* The interaction end event name, returns <code>touchend</code> if the platform supports touches
* and <code>mouseup</code> if it does not.
* @constant
* @type String
*/
iBooks.END_EVENT = iBooks.SUPPORTS_TOUCHES ? 'touchend' : 'mouseup';
/**
* The device motion name.
* @constant
* @type String
*/
iBooks.MOTION_EVENT = 'devicemotion';
/**
* The device orientation name.
* @constant
* @type String
*/
iBooks.DEVICE_ORIENTATION_EVENT = 'deviceorientation';
/**
* The touch cancel event name.
* @constant
* @type String
*/
iBooks.CANCEL_EVENT = 'touchcancel';
/**
* The CSS selector for toggable elements.
* @constant
* @type String
*/
iBooks.TOGGLEABLE_CSS_SELECTOR = '.ibooks-toggleable';
/**
* The CSS selector for deferred event elements.
* @constant
* @type String
*/
iBooks.DEFERRED_EVENT_CSS_SELECTOR = '.ibooks-deferred-event';
/**
* The CSS selector for stampable elements.
* @constant
* @type String
*/
iBooks.STAMPABLE_CSS_SELECTOR = '.ibooks-stampable';
/**
* The CSS selector for media elements.
* @constant
* @type String
*/
iBooks.MEDIA_BASE_CSS_SELECTOR = '.ibooks-media';
/**
* The HTML attribute for the audio source
* @constant
* @type String
*/
iBooks.MEDIA_AUDIO_SOURCE_ATTRIBUTE = 'data-ibooks-audio-src';
/**
* The HTML attribute for the audio reset on play
* @constant
* @type String
*/
iBooks.MEDIA_AUDIO_RESET_ATTRIBUTE = 'data-ibooks-audio-reset-on-play';
/**
* The CSS class name appended to the <code>body</code> when media
* is playing.
* @constant
* @type String
*/
iBooks.MEDIA_PLAYING_CSS_CLASS = 'ibooks-media-playing';
/**
* The HTML attribute for pausing iBooks read aloud
* is playing.
* @constant
* @type String
*/
iBooks.MEDIA_PAUSE_READ_ALOUD_ATTRIBUTE = 'data-ibooks-pause-readaloud';
/**
* The CSS selector for draggable elements.
* @constant
* @type String
*/
iBooks.DRAGGABLE_CSS_SELECTOR = '.ibooks-draggable';
/**
* The CSS class name appended to the <code>body</code> when
* a draggable element is being dragged.
* @constant
* @type String
*/
iBooks.ELEMENT_DRAGGING_CLASS = 'ibooks-element-is-dragging';
/**
* iBooks base controller.
*/
function iBooksBaseController(){
// Set user configurable values and initialize our components
this.initConfigurables();
this.initComponents();
// Provides a CSS class on DOMContentLoaded
setTimeout(function(){
document.body.addClassName(iBooks.CSS_CLASS_ON_LOAD);
}, iBooks.BUILD_IN_EVENT_DELAY);
};
/**
* Configuration of user defined constants.
*/
iBooksBaseController.prototype.initConfigurables = function() {
// CSS class name on active elements
iBooks.ACTIVE_CSS_CLASS = "active";
// CSS class name appended to body on page load
iBooks.CSS_CLASS_ON_LOAD = "build-in";
// Delay in milliseconds before deferred events fire
iBooks.DEFERRED_EVENT_DELAY = 1000;
// Delay in milliseconds before iBooks.CSS_CLASS_ON_LOAD is appended to body
iBooks.BUILD_IN_EVENT_DELAY = 1000;
// Tap threshold value, in pixels
iBooks.TAP_THRESHOLD = 10;
// CSS selector for page
iBooks.PAGE_CSS_SELECTOR = ".page";
// CSS class for stamped elements
iBooks.STAMPED_ELEMENT_CSS_CLASS = "stamp";
};
/**
* Initializes iBooks JS components
*/
iBooksBaseController.prototype.initComponents = function() {
this.deferredEvent = new iBooksDeferredEventController();
this.draggables = new iBooksDraggablesBaseController();
this.media = new iBooksMediaController();
this.stampables = new iBooksStampablesBaseController();
this.toggleables = new iBooksToggleablesBaseController();
};
/**
* On DOM content loaded, instantiate the iBook base controller
*/
window.addEventListener("DOMContentLoaded", function() {
window.iBookController = new iBooksBaseController();
}, false);
/* ==================== TRANSFORMS SHORTHANDS ==================== */
iBooks.Utils = {};
/**
* Prints a <code>translate3d()</code> command that can be used as input for a <code>-webkit-transform</code> property.
*
* @param {int} tx The x coordinate for the translation.
* @param {int} ty The y coordinate for the translation
*
* @returns {String} The <code>translate3d()</code> command
*/
iBooks.Utils.t = function (tx, ty) {
return iBooks.Utils.t3d(tx, ty, 0);
};
/**
* Prints a <code>translate3d()</code> command that can be used as input for a <code>-webkit-transform</code> property.
*
* @param {int} tx The x coordinate for the translation.
* @param {int} ty The y coordinate for the translation
* @param {int} tz The z coordinate for the translation
*
* @returns {String} The <code>translate3d()</code> command
*/
iBooks.Utils.t3d = function (tx, ty, tz) {
return 'translate3d(' + tx + 'px, ' + ty + 'px, ' + tz + 'px)';
};
/**
* Prints a <code>rotate3d()</code> command that can be used as input for a <code>-webkit-transform</code> property.
*
* @param {int} x The x component of the rotation vector
* @param {int} y The y component of the rotation vector
* @param {int} z The z component of the rotation vector
* @param {int} angle The angle in radians for the rotation
*
* @returns {String} The <code>rotate3d()</code> command
*/
iBooks.Utils.r3d = function (x, y, z, angle) {
return 'rotate3d(' + x + ', ' + y + ', ' + z + ', ' + angle + 'rad)';
};
/**
* Creates a CSS string representation for a number in pixels.
*
* @param {number} value The value to be converted.
*
* @returns {String} A CSS string representation for <code>value</code> in pixels.
*/
iBooks.Utils.px = function (value) {
return value + 'px';
};
/**
* Indicates whether the element has a given class name within its <code>class</code> attribute.
*
* @param {String} className The CSS class name.
* @returns {bool} Whether the element has this class name within its <code>class</code> attribute.
* @memberOf Element.prototype
*/
Element.prototype.hasClassName = function (className) {
return new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)').test(this.className);
};
/**
* Adds the given class name to the element's <code>class</code> attribute if it's not already there.
*
* @param {String} className The CSS class name.
* @returns {bool} Whether the class was actually added to the element if it weren't present before addition.
* @memberOf Element.prototype
*/
Element.prototype.addClassName = function (className) {
if (!this.hasClassName(className)) {
this.className = [this.className, className].join(' ');
return true;
}
else {
return false;
}
};
/**
* Removes the given class name from the element's <code>class</code> attribute if it's there.
*
* @param {String} className The CSS class name.
* @returns {bool} Whether the class was actually removed from the element if it were present before removal.
* @memberOf Element.prototype
*/
Element.prototype.removeClassName = function (className) {
if (this.hasClassName(className)) {
var curClasses = this.className;
this.className = curClasses.replace(new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g'), ' ');
return true;
}
return false;
};
/**
* Adds or removes the given class name from the element's <code>class</code> attribute based on a condition. If no
* condition is set, the class will be added if it is not already present and removed if it is.
*
* @param {String} className The CSS class name.
* @param {Boolean} [condition] Whether to add or remove the class (true adds the class, false removes).
* @memberOf Element.prototype
*/
Element.prototype.toggleClassName = function (className, condition) {
if (condition == null) {
condition = !this.hasClassName(className);
}
this[condition ? 'addClassName' : 'removeClassName'](className);
};
/* ==================== DEFERRED EVENTS CONTROLLER ==================== */
/**
* Deferred events controller interates through all elements with
* CSS class name <code>iBooks.DEFERRED_EVENT_CSS_SELECTOR</code>.
*
* Once found, the <code>iBooks.ACTIVE_CSS_CLASS</code> is appended
* to the CSS class name with a delay of <code>iBooks.DEFERRED_EVENT_DELAY</code>ms.
*
* User defined event delays can be set with the HTML <code>data-deferred-event-delay</code> attribute.
*/
function iBooksDeferredEventController(){
var deferredElements = document.querySelectorAll(iBooks.DEFERRED_EVENT_CSS_SELECTOR),
deferredElementsLength = deferredElements.length,
deferredEventDelay;
for (var i=0; i < deferredElementsLength; i++) {
deferredEventDelay = deferredElements[i].getAttribute("data-deferred-event-delay");
deferredEventDelay = (deferredEventDelay === null) ? iBooks.DEFERRED_EVENT_DELAY : deferredEventDelay;
setTimeout(function(index, eventDelay){
return function(){
deferredElements[index].addClassName(iBooks.ACTIVE_CSS_CLASS);
};
}(i), deferredEventDelay);
}
};
/* ==================== MEDIA CONTROLLER ==================== */
/**
* Media controller interates through all elements with
* CSS class name <code>iBooks.MEDIA_BASE_CSS_SELECTOR</code>.
*
* Once found, either <code>iBooksAudioController</code> or
* <code>iBooksVideoController</code> will be instantiated
* for each element.
*/
function iBooksMediaController() {
var mediaElements,
mediaElementsLength,
supportedMediaTypes,
supportedMediaLength;
this.allMedia = [];
supportedMediaTypes = ["audio", "video"];
supportedMediaTypesLength = supportedMediaTypes.length;
for (var i = supportedMediaTypesLength - 1; i >= 0; i--){
mediaElements = document.querySelectorAll(iBooks.MEDIA_BASE_CSS_SELECTOR+"-"+supportedMediaTypes[i]);
// A media element isn't always present, in that case continue the loop
if(mediaElements == undefined){ continue; }
mediaElementsLength = mediaElements.length;
for (var j = mediaElementsLength - 1; j >= 0; j--){
new iBooksAudioController(mediaElements[j]);
};
// Track all media
mediaElementsLength = mediaElements.length;
for (var j = mediaElementsLength - 1; j >= 0; j--){
this.allMedia[j] = new iBooksAudioController(mediaElements[j]);
};
};
};
/**
* This is called when we've found a valid iBooks media HTML element.
*
* By default, audio will pause itself on touch, then resume playing when touched again.
* To reset the audio track, include the HTML attribute <code>iBooks.MEDIA_AUDIO_RESET_ATTRIBUTE</code>
* and set the value to equal to <code>true<code>.
*
* For example:
* <div class="ibooks-media-audio" data-ibooks-audio-src="audio/src.m4a">Play audio</div>
*
* @property {Object} element The required object to instantiate the <code>iBooksDraggableController</code>
*/
function iBooksAudioController(element){
var resetAudioOnPlay,
pauseReadAloud;
this.el = element;
this.el.addEventListener(iBooks.START_EVENT, this, false);
this.src = this.el.getAttribute(iBooks.MEDIA_AUDIO_SOURCE_ATTRIBUTE);
resetAudioOnPlay = this.el.getAttribute(iBooks.MEDIA_AUDIO_RESET_ATTRIBUTE);
this.resetAudioOnPlay = (resetAudioOnPlay == undefined || resetAudioOnPlay == "false") ? false : true;
pauseReadAloud = this.el.getAttribute(iBooks.MEDIA_PAUSE_READ_ALOUD_ATTRIBUTE);
this.pauseReadAloud = (pauseReadAloud == undefined || pauseReadAloud == "false") ? false : true;
// this.setAudio();
};
/**
* Creates a new audio element, set the source, then load it.
*/
iBooksAudioController.prototype.setAudio = function(){
this.media = new Audio();
this.media.src = this.src;
if(this.pauseReadAloud){
this.media.setAttributeNS("http://apple.com/ibooks/html-extensions", "pause-readaloud", true);
};
document.documentElement.appendChild(this.media);
};
/**
* Plays the audio source, if <code>iBooks.MEDIA_PLAYING_CSS_CLASS</code>
* is <code>true</code> reset the audio track.
*
* Additionally, adds <code>iBooks.MEDIA_PLAYING_CSS_CLASS</code> to the body.
*/
iBooksAudioController.prototype.play = function(){
document.body.addClassName(iBooks.MEDIA_PLAYING_CSS_CLASS);
if(this.resetAudioOnPlay){
// Remove the existing element to prevent duplicates.
document.documentElement.removeChild(this.media);
this.setAudio();
}
this.media.play();
};
/**
* Pauses the audio source. Additionally, removes
* <code>iBooks.MEDIA_PLAYING_CSS_CLASS</code> from the body.
*/
iBooksAudioController.prototype.pause = function(){
document.body.removeClassName(iBooks.MEDIA_PLAYING_CSS_CLASS);
this.media.pause();
};
/**
* Toggles between play and pause.
*/
iBooksAudioController.prototype.togglePlayPause = function(){
if(this.media == undefined){ this.setAudio() }
this.media.paused ? this.play() : this.pause();
};
/**
* On touch start, add an event listener for touch end. Store the
* touch start X, Y coordinates for later use.
*
* @property {Object} event The required event object
*/
iBooksAudioController.prototype.touchStart = function(event){
this.startX = event.pageX;
this.startY = event.pageY;
window.addEventListener(iBooks.END_EVENT, this, false);
};
/**
* On touch end, remove our event listeners. Determine if the user action was a
* tap, or gesture; if the action was a tap then add <code>iBooks.ACTIVE_CSS_CLASS</code>
* to the body class and prevent default. Otherwise, allow iBooks to handle the event.
*
* @property {Object} event The required event object
*/
iBooksAudioController.prototype.touchEnd = function(event){
window.removeEventListener(iBooks.END_EVENT, this, false);
this.xTap = Math.abs(this.startX - event.clientX) < iBooks.TAP_THRESHOLD || event.pageX == 0;
this.yTap = Math.abs(this.startY - event.clientY) < iBooks.TAP_THRESHOLD || event.pageY == 0;
if (this.xTap && this.yTap){
event.preventDefault();
this.togglePlayPause();
};
};
/**
* Event triage.
*/
iBooksAudioController.prototype.handleEvent = function(event){
switch(event.type){
case iBooks.START_EVENT:
this.touchStart(event);
break;
case iBooks.END_EVENT:
this.touchEnd(event);
break;
}
};
/* ==================== DRAGGABLES ==================== */
/**
* Draggables controller interates through all elements with
* CSS class name <code>iBooks.DRAGGABLE_CSS_SELECTOR</code>.
*
* Once found, a <code>iBooksDraggableController</code> is instantiated
* for each element.
*/
function iBooksDraggablesBaseController(){
var draggableElements = document.querySelectorAll(iBooks.DRAGGABLE_CSS_SELECTOR),
draggableElementsLength = draggableElements.length;
for (var i = draggableElementsLength - 1; i >= 0; i--){
new iBooksDraggableController(draggableElements[i]);
};
};
/**
* This is called when we've found a draggable HTML element.
*
* @property {Object} element The required object to instantiate the <code>iBooksDraggableController</code>.
*/
function iBooksDraggableController(element){
var page,
pageComputedStyle,
elementComputedStyle;
page = document.querySelector(iBooks.PAGE_CSS_SELECTOR);
pageComputedStyle = window.getComputedStyle(page);
elementComputedStyle = window.getComputedStyle(element);
this.el = element;
this.el.addEventListener(iBooks.START_EVENT, this, false);
this.draggable = false;
this.deltaX = 0;
this.deltaY = 0;
this.draggableHeight = parseInt(pageComputedStyle.height);
this.draggableWidth = parseInt(pageComputedStyle.width);
};
/**
* Called on <code>iBooks.START_EVENT</code>.
* Appends <code>iBooks.ELEMENT_DRAGGING_CLASS</code> to the body on drag start.
*/
iBooksDraggableController.prototype.dragStart = function(event){
event.preventDefault();
document.body.addClassName(iBooks.ELEMENT_DRAGGING_CLASS);
var e = (iBooks.SUPPORTS_TOUCHES && event.touches && event.touches.length > 0) ? event.touches[0] : event;
// make our starting point be the location we tapped minus the existing drag delta
// this ensures the point within the dragged element at which we started the interaction
// remains under the finger at all times
this.startX = e.pageX - this.deltaX;
this.startY = e.pageY - this.deltaY;
window.addEventListener(iBooks.MOVE_EVENT, this, true);
window.addEventListener(iBooks.END_EVENT, this, true);
};
/**
* Called on <code>iBooks.MOVE_EVENT</code>.
* Calculates the translation of the draggable element.
*/
iBooksDraggableController.prototype.dragMove = function(event){
event.preventDefault();
var translate,
e;
e = (iBooks.SUPPORTS_TOUCHES && event.touches && event.touches.length > 0) ? event.touches[0] : event;
this.parseAxialData(e, "x", this.draggableWidth);
this.parseAxialData(e, "y", this.draggableHeight);
translate = iBooks.Utils.t3d(this.deltaX, this.deltaY, 0);
this.el.style.webkitTransform = translate;
};
/**
* Called on <code>iBooks.END_EVENT</code>.
* Removes event listeners and removes <code>iBooks.ELEMENT_DRAGGING_CLASS</code>
* from the body.
*/
iBooksDraggableController.prototype.dragStop = function(){
event.preventDefault();
document.body.removeClassName(iBooks.ELEMENT_DRAGGING_CLASS);
window.removeEventListener(iBooks.MOVE_EVENT, this, true);
window.removeEventListener(iBooks.END_EVENT, this, true);
};
/**
* Parses client X and Y coordinates.
* Verifies client coordinates are within valid X and Y boundaries.
*/
iBooksDraggableController.prototype.parseAxialData = function(event, axis, targetBoundary){
var invalidBoundary;
axis = axis.toUpperCase();
invalidBoundary = (event["client"+axis] < 0 || event["client"+axis] > targetBoundary);
if(invalidBoundary){
if(event["client"+axis] < 0) { this["delta"+axis] = -(this["start"+axis]); }
if(event["client"+axis] > targetBoundary) { this["delta"+axis] = targetBoundary - this["start"+axis]; }
}
else {
this["delta"+axis] = event["client"+axis] - this["start"+axis];
}
};
/**
* Event triage.
*/
iBooksDraggableController.prototype.handleEvent = function(event){
switch(event.type){
case iBooks.START_EVENT:
this.dragStart(event);
break;
case iBooks.MOVE_EVENT:
this.dragMove(event);
break;
case iBooks.END_EVENT:
this.dragStop();
break;
}
};
/* ==================== STAMPABLES CONTROLLER ==================== */
/**
* Stampables controller interates through all elements with
* CSS class name <code>iBooks.STAMPABLE_CSS_SELECTOR</code>.
*
* Once found, a <code>iBooksStampableController</code> is instantiated
* for each element.
*/
function iBooksStampablesBaseController(){
var stampableElements = document.querySelectorAll(iBooks.STAMPABLE_CSS_SELECTOR),
stampableElementsLength = stampableElements.length;
for (var i = stampableElementsLength - 1; i >= 0; i--){
new iBooksStampableController(stampableElements[i]);
};
};
/**
* This is called when we've found a stampable HTML element.
*
* @property {Object} element The required object to instantiate the <code>iBooksStampableController</code>.
*/
function iBooksStampableController(element){
this.el = element;
this.hitarea = this.el.querySelector("svg path");
this.hitarea.addEventListener(iBooks.START_EVENT, this, false);
};
/**
* Clones the target element.
*/
iBooksStampableController.prototype.stamp = function(event){
var e,
stampedElement;
e = (iBooks.SUPPORTS_TOUCHES && event.touches && event.touches.length > 0) ? event.touches[0] : event;
stampedElement = document.createElement("div");
stampedElement.addClassName(iBooks.STAMPED_ELEMENT_CSS_CLASS);
stampedElement.style.left = e.pageX + "px";
stampedElement.style.top = e.pageY + "px";
this.el.appendChild(stampedElement);
};
/**
* Event triage.
*/
iBooksStampableController.prototype.handleEvent = function(event){
if (event.type === iBooks.START_EVENT){
event.preventDefault();
this.stamp(event);
}
};
/* ==================== TOGGLEABLES CONTROLLER ==================== */
/**
* Toggleables controller interates through all elements with
* CSS class name <code>iBooks.TOGGLEABLE_CSS_SELECTOR</code>.
*
* Once found, a <code>iBooksToggleableController</code> is instantiated
* for each element.
*
* User defined class name on toggle set using the HTML <code>data-toggled-class-name</code> attribute.
*/
function iBooksToggleablesBaseController(element){
var toggleableElements = document.querySelectorAll(iBooks.TOGGLEABLE_CSS_SELECTOR),
toggleableElementsLength = toggleableElements.length;
for (var i=0; i < toggleableElementsLength; i++) {
new iBooksToggleableController(toggleableElements[i]);
};
};
/**
* This is called when we've found a toggleable HTML element.
*
* @property {Object} element The required object to instantiate the <code>iBooksToggleableController</code>.
*/
function iBooksToggleableController(element){
var toggledClassName = element.getAttribute("data-toggled-class-name");
this.el = element;
this.el.addEventListener(iBooks.START_EVENT, this, false);
this.toggledClassName = (toggledClassName === null) ? iBooks.ACTIVE_CSS_CLASS : toggledClassName;
};
/**
* On touch start, add an event listener for touch end. Store the
* touch start X, Y coordinates for later use.
*
* @property {Object} event The required event object
*/
iBooksToggleableController.prototype.touchStart = function(event){
this.startX = event.pageX;
this.startY = event.pageY;
window.addEventListener(iBooks.END_EVENT, this, false);
};
/**
* On touch end, remove our event listeners. Determine if the user action was a
* tap, or gesture; if the action was a tap then add <code>iBooks.ACTIVE_CSS_CLASS</code>
* to the body class and prevent default. Otherwise, allow iBooks to handle the event.
*
* @property {Object} event The required event object
*/
iBooksToggleableController.prototype.touchEnd = function(event){
window.removeEventListener(iBooks.END_EVENT, this, false);
this.xTap = Math.abs(this.startX - event.clientX) < iBooks.TAP_THRESHOLD || event.pageX == 0;
this.yTap = Math.abs(this.startY - event.clientY) < iBooks.TAP_THRESHOLD || event.pageY == 0;
if (this.xTap && this.yTap){
event.preventDefault();
this.el.toggleClassName(this.toggledClassName);
};
};
/**
* Event triage.
*
*/
iBooksToggleableController.prototype.handleEvent = function(event){
switch(event.type){
case iBooks.START_EVENT:
this.touchStart(event);
break;
case iBooks.END_EVENT:
this.touchEnd(event);
break;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment