bmstu-mt-wp/shower.js

716 lines
17 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Shower HTML presentation engine: github.com/shower/shower
// Copyright © 20102013 Vadim Makeev, pepelsbey.net
// Licensed under MIT license: github.com/shower/shower/wiki/MIT-License
window.shower = (function(window, document, undefined) {
var shower = {},
url = window.location,
body = document.body,
slides = document.querySelectorAll('.slide'),
progress = document.querySelector('div.progress div'),
slideList = [],
timer,
isHistoryApiSupported = !!(window.history && history.pushState),
l = slides.length, i;
/**
* Get value at named data store for the DOM element.
* @private
* @param {domElem} element
* @param {String} name
* @returns {String}
*/
shower._getData = function(element, name) {
return element.dataset ? element.dataset[name] : element.getAttribute('data-' + name);
};
for (i = 0; i < l; i++) {
// Slide ID's are optional. In case of missing ID we set it to the
// slide number
if ( ! slides[i].id) {
slides[i].id = i + 1;
}
slideList.push({
id: slides[i].id,
hasInnerNavigation: null !== slides[i].querySelector('.next'),
hasTiming: (shower._getData(slides[i], 'timing') && shower._getData(slides[i], 'timing').indexOf(':') !== -1)
});
}
/**
* Get slide scale value
* @private
* @returns {String}
*/
shower._getTransform = function() {
var denominator = Math.max(
body.clientWidth / window.innerWidth,
body.clientHeight / window.innerHeight
);
return 'scale(' + (1 / denominator) + ')';
};
/**
* Set CSS transform with prefixes to body
* @private
* @returns {Boolean}
*/
shower._applyTransform = function(transform) {
body.style.WebkitTransform = transform;
body.style.MozTransform = transform;
body.style.msTransform = transform;
body.style.OTransform = transform;
body.style.transform = transform;
return true;
};
/**
* Check if arg is number.
* @private
* @param {Number|whatelse} arg Any type
* @returns {Boolean}
*/
shower._isNumber = function(arg) {
if ( ! ( ! isNaN(parseFloat(arg)) && isFinite(arg))) {
return false;
}
return true;
};
/**
* Normalize slide number.
* @private
* @param {Number} slideNumber slide number (sic!)
* @returns {Number}
*/
shower._normalizeSlideNumber = function(slideNumber) {
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
if (slideNumber < 0) {
slideNumber = 0;
}
if (slideNumber >= slideList.length) {
slideNumber = slideList.length - 1;
}
return slideNumber;
};
/**
* Get containing slide id.
* @private
* @param {domElem} el
* @returns {String}
*/
shower._getContainingSlideId = function(el) {
while ('BODY' !== el.nodeName && 'HTML' !== el.nodeName) {
if (el.classList.contains('slide')) {
return el.id;
} else {
el = el.parentNode;
}
}
return '';
};
/**
* Dispatch single slide mode.
* @TODO: Renaming needed? Or just some handlers rewriting?
* @private
* @param {domElem} e
* @returns {Undefined}
*/
shower._dispatchSingleSlideMode = function(e) {
// Process links
// @TODO: presentation links support
if ('A' === e.target.nodeName) {
e.preventDefault();
window.open(e.target.getAttribute('href'));
return;
}
var slideId = shower._getContainingSlideId(e.target);
if ('' !== slideId && shower.isListMode()) {
e.preventDefault();
// NOTE: we should update hash to get things work properly
url.hash = '#' + slideId;
if (isHistoryApiSupported) {
history.replaceState(null, null, url.pathname + '?full#' + slideId);
}
shower.enterSlideMode();
shower.updateProgress(shower.getCurrentSlideNumber());
shower.updateCurrentAndPassedSlides(shower.getCurrentSlideNumber());
shower.runSlideshowIfPresented(shower.getCurrentSlideNumber());
}
return;
};
/**
* Show next slide. If slide is last returns false, otherwise return slide
* number which been shown.
* @param {Function} callback runs only if shower.next() complete successfully
* @returns {Number|Boolean}
*/
shower.next = function(callback) {
var currentSlideNumber = shower.getCurrentSlideNumber(),
ret;
// Only go to next slide if current slide have no inner
// navigation or inner navigation is fully shown
// NOTE: But first of all check if there is no current slide
if (
-1 === currentSlideNumber ||
!slideList[currentSlideNumber].hasInnerNavigation ||
-1 === shower.increaseInnerNavigation(currentSlideNumber)
) {
shower.go(currentSlideNumber + 1);
// slides starts from 0
ret = currentSlideNumber + 2;
if (typeof(callback) === "function") {
callback();
}
} else {
ret = false;
}
return ret;
};
/**
* Show previous slide. If slide is first returns false, otherwise return slide
* number which been shown.
* @param {Function} callback runs only if shower.previous() complete successfully
* @returns {Number|Boolean}
*/
shower.previous = function(callback) {
var currentSlideNumber = shower.getCurrentSlideNumber(),
ret;
// slides starts from 0
if (currentSlideNumber > 0) {
ret = currentSlideNumber;
shower.go(currentSlideNumber - 1);
if (typeof(callback) === "function") {
callback();
}
} else {
ret = false;
}
return ret;
};
/**
* Show first slide.
* @param {Function} callback
* @returns {Number}
*/
shower.first = function(callback) {
if (typeof(callback) === "function") {
callback();
}
return shower.go(0);
};
/**
* Show last slide.
* @param {Function} callback
* @returns {Number}
*/
shower.last = function(callback) {
if (typeof(callback) === "function") {
callback();
}
return shower.go(slideList.length - 1);
};
/**
* Switch to slide view.
* @param {Function} callback runs only if shower.enterSlideMode() complete successfully
* @returns {Number|Boolean}
*/
shower.enterSlideMode = function(callback) {
// check if it's already in slide mode...
if (body.classList.contains('full')) {
return false;
}
body.classList.remove('list');
body.classList.add('full');
if (window.console && window.console.clear) {
console.clear();
}
shower.showPresenterNotes(shower.getCurrentSlideNumber());
if (typeof(callback) === "function") {
callback();
}
return shower._applyTransform(shower._getTransform());
};
/**
* Switch to list view.
* @param {Function} callback runs only if shower.enterListMode() complete successfully
* @returns {Number}
*/
shower.enterListMode = function(callback) {
// check if it's already in list mode...
if (body.classList.contains('list')) {
return false;
}
body.classList.remove('full');
body.classList.add('list');
if (typeof(callback) === "function") {
callback();
}
return shower._applyTransform('none');
};
// @TODO: add method shower.toggleMode()
/**
* Get current slide number. Starts from zero. Warning: when in url you have
* slide number 1 this method will return 0.
* If something wrong return -1.
* @returns {Number}
*/
shower.getCurrentSlideNumber = function() {
var i = slideList.length - 1,
currentSlideId = url.hash.substr(1);
// As fast as you can ;-)
// http://jsperf.com/for-vs-foreach/46
for (; i >= 0; --i) {
if (currentSlideId === slideList[i].id) {
return i;
}
}
return -1;
};
/**
* Scroll to slide.
* @param {Number} slideNumber slide number (sic!)
* @returns {Undefined|Boolean}
*/
shower.scrollToSlide = function(slideNumber) {
var currentSlide,
ret;
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
if ( ! shower.isListMode()) {
throw new Error('You can\'t scroll to because you in slide mode. Please, switch to list mode.');
}
// @TODO: WTF?
if (-1 === slideNumber) {
return;
}
if (slideList[slideNumber]) {
currentSlide = document.getElementById(slideList[slideNumber].id);
window.scrollTo(0, currentSlide.offsetTop);
ret = true;
} else {
throw new Error('There is no slide with number ' + slideNumber);
}
return ret;
};
/**
* Chech if it's list mode.
* @returns {Boolean}
*/
shower.isListMode = function() {
return isHistoryApiSupported ? ! /^full.*/.test(url.search.substr(1)) : body.classList.contains('list');
};
/**
* Update progress bar.
* @param {Number} slideNumber slide number (sic!)
* @returns {Boolean}
*/
shower.updateProgress = function(slideNumber) {
// if progress bar doesn't exist
if (null === progress) {
return false;
}
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
progress.style.width = (100 / (slideList.length - 1) * shower._normalizeSlideNumber(slideNumber)).toFixed(2) + '%';
return true;
};
/**
* Update current and passed slides.
* @param {Number} slideNumber slide number (sic!)
* @returns {Boolean}
*/
shower.updateCurrentAndPassedSlides = function(slideNumber) {
var i,
slide,
l = slideList.length;
slideNumber = shower._normalizeSlideNumber(slideNumber);
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
for (i = 0; i < l; ++i) {
slide = document.getElementById(slideList[i].id);
if (i < slideNumber) {
slide.classList.remove('current');
slide.classList.add('passed');
} else if (i > slideNumber) {
slide.classList.remove('passed');
slide.classList.remove('current');
} else {
slide.classList.remove('passed');
slide.classList.add('current');
}
}
return true;
};
/**
* Show presenter notes in console.
* @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
*/
shower.showPresenterNotes = function(slideNumber) {
if (window.console) {
slideNumber = shower._normalizeSlideNumber(slideNumber);
var slideId = slideList[slideNumber].id;
var nextSlideId = slideList[slideNumber + 1] ? slideList[slideNumber + 1].id : null;
var notes = document.getElementById(slideId).querySelector('footer');
if (notes) {
console.info(notes.innerHTML.replace(/\n\s+/g,'\n'));
}
if (nextSlideId) {
var next = document.getElementById(nextSlideId).querySelector('h2');
if (next) {
next = next.innerHTML.replace(/^\s+|<[^>]+>/g,'');
console.info('NEXT: ' + next);
}
}
}
};
/**
* Get slide hash.
* @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
* @returns {Boolean}
*/
shower.getSlideHash = function(slideNumber) {
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
slideNumber = shower._normalizeSlideNumber(slideNumber);
return '#' + slideList[slideNumber].id;
};
/**
* Go to slide number...
* @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
* @param {Function} callback runs only if you not in List mode
* @returns {Number}
*/
shower.go = function(slideNumber, callback) {
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
url.hash = shower.getSlideHash(slideNumber);
if ( ! shower.isListMode()) {
shower.updateProgress(slideNumber);
shower.updateCurrentAndPassedSlides(slideNumber);
shower.showPresenterNotes(slideNumber);
if (typeof(callback) === "function") {
callback();
}
}
return slideNumber;
};
/**
* Run slide show if presented.
* @param {Number} slideNumber
* @returns {Undefined}
*/
shower.runSlideshowIfPresented = function(slideNumber) {
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
slideNumber = shower._normalizeSlideNumber(slideNumber);
clearTimeout(timer);
if (slideList[slideNumber].hasTiming) {
// Compute number of milliseconds from format "X:Y", where X is
// number of minutes, and Y is number of seconds
var timing = shower._getData(document.getElementById(slideList[slideNumber].id), 'timing').split(':');
timing = parseInt(timing[0], 10) * 60 * 1000 + parseInt(timing[1], 10) * 1000;
timer = setTimeout(function() {
shower.go(slideNumber + 1);
shower.runSlideshowIfPresented(slideNumber + 1);
},
timing);
}
};
/**
* Increases inner navigation by adding 'active' class to next inactive inner navigation item
* @param {Number} slideNumber
* @returns {Number}
*/
shower.increaseInnerNavigation = function(slideNumber) {
var nextNodes,
node,
ret = -1;
if ( ! shower._isNumber(slideNumber)) {
throw new Error('Gimme slide number as Number, baby!');
}
// If inner navigation in this slide...
if (slideList[slideNumber].hasInnerNavigation) {
nextNodes = document.getElementById(slideList[slideNumber].id).querySelectorAll('.next:not(.active)');
if (0 !== nextNodes.length) {
node = nextNodes[0];
node.classList.add('active');
ret = nextNodes.length - 1;
}
}
return ret;
};
// Event handlers
window.addEventListener('DOMContentLoaded', function() {
if ( ! shower.isListMode()) {
// "?full" is present without slide hash, so we should display first slide
if (-1 === shower.getCurrentSlideNumber()) {
if (isHistoryApiSupported) {
history.replaceState(null, null, url.pathname + '?full' + shower.getSlideHash(0));
}
}
shower.enterSlideMode();
shower.updateProgress(shower.getCurrentSlideNumber());
shower.updateCurrentAndPassedSlides(shower.getCurrentSlideNumber());
shower.runSlideshowIfPresented(shower.getCurrentSlideNumber());
}
}, false);
window.addEventListener('popstate', function(e) {
if (shower.isListMode()) {
shower.enterListMode();
shower.scrollToSlide(shower.getCurrentSlideNumber());
} else {
shower.enterSlideMode();
}
}, false);
window.addEventListener('resize', function(e) {
if ( ! shower.isListMode()) {
shower._applyTransform(shower._getTransform());
}
}, false);
document.addEventListener('keydown', function(e) {
// Shortcut for alt, ctrl and meta keys
if (e.altKey || e.ctrlKey || e.metaKey) { return; }
var currentSlideNumber = shower.getCurrentSlideNumber(),
innerNavigationCompleted = true;
switch (e.which) {
case 116: // F5
e.preventDefault();
if (shower.isListMode()) {
var slideNumber = e.shiftKey ? currentSlideNumber : 0;
shower.go(slideNumber);
if (isHistoryApiSupported) {
history.pushState(null, null, url.pathname + '?full' + shower.getSlideHash(slideNumber));
}
shower.enterSlideMode();
shower.updateProgress(slideNumber);
shower.updateCurrentAndPassedSlides(slideNumber);
shower.runSlideshowIfPresented(slideNumber);
} else {
if (isHistoryApiSupported) {
history.pushState(null, null, url.pathname + shower.getSlideHash(currentSlideNumber));
}
shower.enterListMode();
shower.scrollToSlide(currentSlideNumber);
}
break;
case 13: // Enter
if (shower.isListMode() && -1 !== currentSlideNumber) {
e.preventDefault();
if (isHistoryApiSupported) {
history.pushState(null, null, url.pathname + '?full' + shower.getSlideHash(currentSlideNumber));
}
shower.enterSlideMode();
shower.updateProgress(currentSlideNumber);
shower.updateCurrentAndPassedSlides(currentSlideNumber);
shower.runSlideshowIfPresented(currentSlideNumber);
}
break;
case 27: // Esc
if ( ! shower.isListMode()) {
e.preventDefault();
if (isHistoryApiSupported) {
history.pushState(null, null, url.pathname + shower.getSlideHash(currentSlideNumber));
}
shower.enterListMode();
shower.scrollToSlide(currentSlideNumber);
}
break;
case 33: // PgUp
case 38: // Up
case 37: // Left
case 72: // h
case 75: // k
e.preventDefault();
shower.previous();
break;
case 34: // PgDown
case 40: // Down
case 39: // Right
case 76: // l
case 74: // j
e.preventDefault();
if ( ! shower.isListMode()) {
// Inner navigation is "completed" if current slide have
// no inner navigation or inner navigation is fully shown
innerNavigationCompleted = !slideList[currentSlideNumber].hasInnerNavigation ||
-1 === shower.increaseInnerNavigation(currentSlideNumber);
} else {
// Also inner navigation is always "completed" if we are in
// list mode
innerNavigationCompleted = true;
}
// NOTE: First of all check if there is no current slide
if (
-1 === currentSlideNumber || innerNavigationCompleted
) {
currentSlideNumber++;
shower.go(currentSlideNumber);
// We must run slideshow only in full mode
if ( ! shower.isListMode()) {
shower.runSlideshowIfPresented(currentSlideNumber);
}
}
break;
case 36: // Home
e.preventDefault();
shower.first();
break;
case 35: // End
e.preventDefault();
shower.last();
break;
case 9: // Tab = +1; Shift + Tab = -1
case 32: // Space = +1; Shift + Space = -1
e.preventDefault();
shower[e.shiftKey ? 'previous' : 'next']();
break;
default:
// Behave as usual
}
}, false);
document.addEventListener('click', shower._dispatchSingleSlideMode, false);
document.addEventListener('touchend', shower._dispatchSingleSlideMode, false);
document.addEventListener('touchstart', function(e) {
if ( ! shower.isListMode()) {
var currentSlideNumber = shower.getCurrentSlideNumber(),
x = e.touches[0].pageX;
if (x > window.innerWidth / 2) {
currentSlideNumber++;
} else {
currentSlideNumber--;
}
shower.go(currentSlideNumber);
}
}, false);
document.addEventListener('touchmove', function(e) {
if ( ! shower.isListMode()) {
e.preventDefault();
}
}, false);
return shower;
})(this, this.document);