shower.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. /**
  2. * Shower HTML presentation engine: github.com/shower/shower
  3. * @copyright 2010–2013 Vadim Makeev, pepelsbey.net
  4. * @license MIT license: github.com/shower/shower/wiki/MIT-License
  5. */
  6. window.shower = (function(window, document, undefined) {
  7. var shower = {},
  8. url = window.location,
  9. body = document.body,
  10. slides = document.querySelectorAll('.slide'),
  11. progress = document.querySelector('div.progress div'),
  12. slideList = [],
  13. timer,
  14. isHistoryApiSupported = !!(window.history && history.pushState),
  15. l = slides.length, i;
  16. /**
  17. * Get value at named data store for the DOM element.
  18. * @private
  19. * @param {HTMLElement} element
  20. * @param {String} name
  21. * @returns {String}
  22. */
  23. shower._getData = function(element, name) {
  24. return element.dataset ? element.dataset[name] : element.getAttribute('data-' + name);
  25. };
  26. for (i = 0; i < l; i++) {
  27. // Slide IDs are optional. In case of missing ID we set it to the
  28. // slide number
  29. if ( ! slides[i].id) {
  30. slides[i].id = i + 1;
  31. }
  32. slideList.push({
  33. id: slides[i].id,
  34. hasInnerNavigation: null !== slides[i].querySelector('.next'),
  35. hasTiming: (shower._getData(slides[i], 'timing') && shower._getData(slides[i], 'timing').indexOf(':') !== -1)
  36. });
  37. }
  38. /**
  39. * Get slide scale value.
  40. * @private
  41. * @returns {String}
  42. */
  43. shower._getTransform = function() {
  44. var denominator = Math.max(
  45. body.clientWidth / window.innerWidth,
  46. body.clientHeight / window.innerHeight
  47. );
  48. return 'scale(' + (1 / denominator) + ')';
  49. };
  50. /**
  51. * Set CSS transform with prefixes to body.
  52. * @private
  53. * @returns {Boolean}
  54. */
  55. shower._applyTransform = function(transform) {
  56. body.style.WebkitTransform = transform;
  57. body.style.MozTransform = transform;
  58. body.style.msTransform = transform;
  59. body.style.OTransform = transform;
  60. body.style.transform = transform;
  61. return true;
  62. };
  63. /**
  64. * Check if arg is number.
  65. * @private
  66. * @param {String|Number} arg
  67. * @returns {Boolean}
  68. */
  69. shower._isNumber = function(arg) {
  70. return ! isNaN(parseFloat(arg)) && isFinite(arg);
  71. };
  72. /**
  73. * Normalize slide number.
  74. * @private
  75. * @param {Number} slideNumber slide number (sic!)
  76. * @returns {Number}
  77. */
  78. shower._normalizeSlideNumber = function(slideNumber) {
  79. if ( ! shower._isNumber(slideNumber)) {
  80. throw new Error('Gimme slide number as Number, baby!');
  81. }
  82. if (slideNumber < 0) {
  83. slideNumber = 0;
  84. }
  85. if (slideNumber >= slideList.length) {
  86. slideNumber = slideList.length - 1;
  87. }
  88. return slideNumber;
  89. };
  90. /**
  91. * Get slide id from HTML element.
  92. * @private
  93. * @param {HTMLElement} el
  94. * @returns {String}
  95. */
  96. shower._getSlideIdByEl = function(el) {
  97. while ('BODY' !== el.nodeName && 'HTML' !== el.nodeName) {
  98. if (el.classList.contains('slide')) {
  99. return el.id;
  100. } else {
  101. el = el.parentNode;
  102. }
  103. }
  104. return '';
  105. };
  106. /**
  107. * For touch devices: check if link is clicked.
  108. *
  109. * @TODO: add support for textareas/inputs/etc.
  110. *
  111. * @private
  112. * @param {HTMLElement} e
  113. * @returns {Boolean}
  114. */
  115. shower._checkInteractiveElement = function(e) {
  116. return 'A' === e.target.nodeName;
  117. };
  118. /**
  119. * Get slide number by slideId.
  120. * @param {String} slideId (HTML id or position in slideList)
  121. * @returns {Number}
  122. */
  123. shower.getSlideNumber = function(slideId) {
  124. var i = slideList.length - 1,
  125. slideNumber;
  126. if (slideId === '') {
  127. slideNumber = 0;
  128. }
  129. // As fast as you can ;-)
  130. // http://jsperf.com/for-vs-foreach/46
  131. for (; i >= 0; --i) {
  132. if (slideId === slideList[i].id) {
  133. slideNumber = i;
  134. break;
  135. }
  136. }
  137. return slideNumber;
  138. };
  139. /**
  140. * Go to slide number.
  141. * @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
  142. * @param {Function} [callback] runs only if you not in List mode.
  143. * @returns {Number}
  144. */
  145. shower.go = function(slideNumber, callback) {
  146. if ( ! shower._isNumber(slideNumber)) {
  147. throw new Error('Gimme slide number as Number, baby!');
  148. }
  149. // Also triggers popstate and invoke shower.enter__Mode()
  150. url.hash = shower.getSlideHash(slideNumber);
  151. shower.updateProgress(slideNumber);
  152. shower.updateActiveAndVisitedSlides(slideNumber);
  153. if (shower.isSlideMode()) {
  154. shower.showPresenterNotes(slideNumber);
  155. shower.runInnerNavigation(slideNumber);
  156. }
  157. if (typeof(callback) === 'function') {
  158. callback();
  159. }
  160. return slideNumber;
  161. };
  162. /**
  163. * Show next slide or show next Inner navigation item.
  164. * Returns false on a last slide, otherwise returns shown slide number.
  165. * @param {Function} [callback] runs only if shower.next() is successfully completed.
  166. * @returns {Number|Boolean}
  167. */
  168. shower.next = function(callback) {
  169. var currentSlideNumber = shower.getCurrentSlideNumber(),
  170. ret = false;
  171. // Only go to next slide if current slide have no inner
  172. // navigation or inner navigation is fully shown
  173. // NOTE: But first of all check if there is no current slide
  174. if (
  175. (
  176. -1 === currentSlideNumber ||
  177. ! slideList[currentSlideNumber].hasInnerNavigation ||
  178. ! shower.increaseInnerNavigation(currentSlideNumber)
  179. ) &&
  180. // If exist next slide
  181. (currentSlideNumber + 2) <= slideList.length
  182. ) {
  183. shower.go(currentSlideNumber + 1);
  184. // Slides starts from 0. So return next slide number.
  185. ret = currentSlideNumber + 2;
  186. }
  187. if (shower.isSlideMode()) {
  188. shower.runInnerNavigation(currentSlideNumber + 1);
  189. }
  190. if (typeof(callback) === 'function') {
  191. callback();
  192. }
  193. return ret;
  194. };
  195. /**
  196. * Show previous slide. Returns false on a first slide, otherwise returns shown slide number.
  197. * @param {Function} [callback] runs only if shower.previous() is successfully completed.
  198. * @returns {Number|Boolean}
  199. */
  200. shower.previous = function(callback) {
  201. var currentSlideNumber = shower.getCurrentSlideNumber(),
  202. ret = false;
  203. // slides starts from 0
  204. if (currentSlideNumber > 0) {
  205. ret = currentSlideNumber;
  206. shower.go(currentSlideNumber - 1);
  207. if (typeof(callback) === 'function') {
  208. callback();
  209. }
  210. }
  211. return ret;
  212. };
  213. /**
  214. * Show first slide.
  215. * @param {Function} [callback]
  216. * @returns {Number}
  217. */
  218. shower.first = function(callback) {
  219. if (typeof(callback) === 'function') {
  220. callback();
  221. }
  222. return shower.go(0);
  223. };
  224. /**
  225. * Show last slide.
  226. * @param {Function} [callback]
  227. * @returns {Number}
  228. */
  229. shower.last = function(callback) {
  230. if (typeof(callback) === 'function') {
  231. callback();
  232. }
  233. return shower.go(slideList.length - 1);
  234. };
  235. /**
  236. * Switch to slide view.
  237. * @param {Function} [callback] runs only if shower.enterSlideMode() is successfully completed.
  238. * @returns {Boolean}
  239. */
  240. shower.enterSlideMode = function(callback) {
  241. var currentSlideNumber = shower.getCurrentSlideNumber();
  242. // Anyway: change body class (@TODO: refactoring)
  243. body.classList.remove('list');
  244. body.classList.add('full');
  245. // Preparing URL for shower.go()
  246. if (shower.isListMode() && isHistoryApiSupported) {
  247. history.pushState(null, null, url.pathname + '?full' + shower.getSlideHash(currentSlideNumber));
  248. }
  249. shower._applyTransform(shower._getTransform());
  250. if (typeof(callback) === 'function') {
  251. callback();
  252. }
  253. return true;
  254. };
  255. /**
  256. * Switch to list view.
  257. * @param {Function} [callback] runs only if shower.enterListMode() is successfully completed.
  258. * @returns {Boolean}
  259. */
  260. shower.enterListMode = function(callback) {
  261. // Anyway: change body class (@TODO: refactoring)
  262. body.classList.remove('full');
  263. body.classList.add('list');
  264. shower.clearPresenterNotes();
  265. if (shower.isListMode()) {
  266. return false;
  267. }
  268. var currentSlideNumber = shower.getCurrentSlideNumber();
  269. clearTimeout(timer);
  270. if (shower.isSlideMode() && isHistoryApiSupported) {
  271. history.pushState(null, null, url.pathname + shower.getSlideHash(currentSlideNumber));
  272. }
  273. shower.scrollToSlide(currentSlideNumber);
  274. shower._applyTransform('none');
  275. if (typeof(callback) === 'function') {
  276. callback();
  277. }
  278. return true;
  279. };
  280. /**
  281. * Toggle Mode: Slide and List.
  282. * @param {Function} [callback]
  283. */
  284. shower.toggleMode = function(callback) {
  285. if (shower.isListMode()) {
  286. shower.enterSlideMode();
  287. } else {
  288. shower.enterListMode();
  289. }
  290. if (typeof(callback) === 'function') {
  291. callback();
  292. }
  293. return true;
  294. };
  295. /**
  296. * Get current slide number. Starts from zero. Warning: when you have
  297. * slide number 1 in URL this method will return 0.
  298. * If something is wrong return -1.
  299. * @returns {Number}
  300. */
  301. shower.getCurrentSlideNumber = function() {
  302. var i = slideList.length - 1,
  303. currentSlideId = url.hash.substr(1);
  304. // As fast as you can ;-)
  305. // http://jsperf.com/for-vs-foreach/46
  306. for (; i >= 0; --i) {
  307. if (currentSlideId === slideList[i].id) {
  308. return i;
  309. }
  310. }
  311. return -1;
  312. };
  313. /**
  314. * Scroll to slide.
  315. * @param {Number} slideNumber slide number (sic!)
  316. * @returns {Boolean}
  317. */
  318. shower.scrollToSlide = function(slideNumber) {
  319. var currentSlide,
  320. ret = false;
  321. if ( ! shower._isNumber(slideNumber)) {
  322. throw new Error('Gimme slide number as Number, baby!');
  323. }
  324. if (shower.isSlideMode()) {
  325. throw new Error('You can\'t scroll to because you in slide mode. Please, switch to list mode.');
  326. }
  327. // @TODO: WTF?
  328. if (-1 === slideNumber) {
  329. return ret;
  330. }
  331. if (slideList[slideNumber]) {
  332. currentSlide = document.getElementById(slideList[slideNumber].id);
  333. window.scrollTo(0, currentSlide.offsetTop);
  334. ret = true;
  335. } else {
  336. throw new Error('There is no slide with number ' + slideNumber);
  337. }
  338. return ret;
  339. };
  340. /**
  341. * Check if it's List mode.
  342. * @returns {Boolean}
  343. */
  344. shower.isListMode = function() {
  345. return isHistoryApiSupported ? ! /^full.*/.test(url.search.substr(1)) : body.classList.contains('list');
  346. };
  347. /**
  348. * Check if it's Slide mode.
  349. * @returns {Boolean}
  350. */
  351. shower.isSlideMode = function() {
  352. return isHistoryApiSupported ? /^full.*/.test(url.search.substr(1)) : body.classList.contains('full');
  353. };
  354. /**
  355. * Update progress bar.
  356. * @param {Number} slideNumber slide number (sic!)
  357. * @returns {Boolean}
  358. */
  359. shower.updateProgress = function(slideNumber) {
  360. // if progress bar doesn't exist
  361. if (null === progress) {
  362. return false;
  363. }
  364. if ( ! shower._isNumber(slideNumber)) {
  365. throw new Error('Gimme slide number as Number, baby!');
  366. }
  367. progress.style.width = (100 / (slideList.length - 1) * shower._normalizeSlideNumber(slideNumber)).toFixed(2) + '%';
  368. return true;
  369. };
  370. /**
  371. * Update active and visited slides.
  372. * @param {Number} slideNumber slide number (sic!)
  373. * @returns {Boolean}
  374. */
  375. shower.updateActiveAndVisitedSlides = function(slideNumber) {
  376. var i,
  377. slide,
  378. l = slideList.length;
  379. slideNumber = shower._normalizeSlideNumber(slideNumber);
  380. if ( ! shower._isNumber(slideNumber)) {
  381. throw new Error('Gimme slide number as Number, baby!');
  382. }
  383. for (i = 0; i < l; ++i) {
  384. slide = document.getElementById(slideList[i].id);
  385. if (i < slideNumber) {
  386. slide.classList.remove('active');
  387. slide.classList.add('visited');
  388. } else if (i > slideNumber) {
  389. slide.classList.remove('visited');
  390. slide.classList.remove('active');
  391. } else {
  392. slide.classList.remove('visited');
  393. slide.classList.add('active');
  394. }
  395. }
  396. return true;
  397. };
  398. /**
  399. * Clear presenter notes in console.
  400. */
  401. shower.clearPresenterNotes = function() {
  402. if (window.console && window.console.clear) {
  403. console.clear();
  404. }
  405. };
  406. /**
  407. * Show presenter notes in console.
  408. * @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
  409. */
  410. shower.showPresenterNotes = function(slideNumber) {
  411. shower.clearPresenterNotes();
  412. if (window.console) {
  413. slideNumber = shower._normalizeSlideNumber(slideNumber);
  414. var slideId = slideList[slideNumber].id,
  415. nextSlideId = slideList[slideNumber + 1] ? slideList[slideNumber + 1].id : null,
  416. notes = document.getElementById(slideId).querySelector('footer');
  417. if (notes && notes.innerHTML) {
  418. console.info(notes.innerHTML.replace(/\n\s+/g,'\n'));
  419. }
  420. if (nextSlideId) {
  421. var next = document.getElementById(nextSlideId).querySelector('h2');
  422. if (next) {
  423. next = next.innerHTML.replace(/^\s+|<[^>]+>/g,'');
  424. console.info('NEXT: ' + next);
  425. }
  426. }
  427. }
  428. };
  429. /**
  430. * Get slide hash.
  431. * @param {Number} slideNumber slide number (sic!). Attention: starts from zero.
  432. * @returns {String}
  433. */
  434. shower.getSlideHash = function(slideNumber) {
  435. if ( ! shower._isNumber(slideNumber)) {
  436. throw new Error('Gimme slide number as Number, baby!');
  437. }
  438. slideNumber = shower._normalizeSlideNumber(slideNumber);
  439. return '#' + slideList[slideNumber].id;
  440. };
  441. /**
  442. * Run slide show if presented.
  443. * @param {Number} slideNumber
  444. * @returns {Boolean}
  445. */
  446. shower.runInnerNavigation = function(slideNumber) {
  447. if ( ! shower._isNumber(slideNumber)) {
  448. throw new Error('Gimme slide number as Number, baby!');
  449. }
  450. slideNumber = shower._normalizeSlideNumber(slideNumber);
  451. clearTimeout(timer);
  452. if (slideList[slideNumber].hasTiming) {
  453. // Compute number of milliseconds from format "X:Y", where X is
  454. // number of minutes, and Y is number of seconds
  455. var timing = shower._getData(document.getElementById(slideList[slideNumber].id), 'timing').split(':');
  456. timing = parseInt(timing[0], 10) * 60 * 1000 + parseInt(timing[1], 10) * 1000;
  457. timer = setTimeout(function() {
  458. shower.next();
  459. },
  460. timing);
  461. }
  462. return true;
  463. };
  464. /**
  465. * Increases inner navigation by adding 'active' class to next inactive inner navigation item
  466. * @param {Number} slideNumber
  467. * @returns {Boolean}
  468. */
  469. shower.increaseInnerNavigation = function(slideNumber) {
  470. var nextNodes,
  471. node;
  472. if ( ! shower._isNumber(slideNumber)) {
  473. throw new Error('Gimme slide number as Number, baby!');
  474. }
  475. // If inner navigation in this slide
  476. if (slideList[slideNumber].hasInnerNavigation) {
  477. nextNodes = document.getElementById(slideList[slideNumber].id).querySelectorAll('.next:not(.active)');
  478. if (0 !== nextNodes.length) {
  479. node = nextNodes[0];
  480. node.classList.add('active');
  481. return true;
  482. }
  483. }
  484. return false;
  485. };
  486. // Event handlers
  487. window.addEventListener('DOMContentLoaded', function() {
  488. if (body.classList.contains('full') || shower.isSlideMode()) {
  489. shower.go(shower.getCurrentSlideNumber());
  490. shower.enterSlideMode();
  491. }
  492. }, false);
  493. window.addEventListener('popstate', function() {
  494. if (shower.isListMode()) {
  495. shower.enterListMode();
  496. } else {
  497. shower.enterSlideMode();
  498. }
  499. }, false);
  500. window.addEventListener('resize', function() {
  501. if (shower.isSlideMode()) {
  502. shower._applyTransform(shower._getTransform());
  503. }
  504. }, false);
  505. document.addEventListener('keydown', function(e) {
  506. // Shortcut for alt, ctrl and meta keys
  507. if (e.altKey || e.ctrlKey || e.metaKey) { return; }
  508. var currentSlideNumber = shower.getCurrentSlideNumber(),
  509. isInnerNavCompleted = true;
  510. switch (e.which) {
  511. case 116: // F5
  512. e.preventDefault();
  513. if (shower.isListMode()) {
  514. var slideNumber = e.shiftKey ? currentSlideNumber : 0;
  515. // Warning: go must be before enterSlideMode.
  516. // Otherwise there is a bug in Chrome
  517. shower.go(slideNumber);
  518. shower.enterSlideMode();
  519. shower.showPresenterNotes(slideNumber);
  520. } else {
  521. shower.enterListMode();
  522. }
  523. break;
  524. case 13: // Enter
  525. if (shower.isListMode() && -1 !== currentSlideNumber) {
  526. e.preventDefault();
  527. shower.enterSlideMode();
  528. }
  529. break;
  530. case 27: // Esc
  531. if (shower.isSlideMode()) {
  532. e.preventDefault();
  533. shower.enterListMode();
  534. }
  535. break;
  536. case 33: // PgUp
  537. case 38: // Up
  538. case 37: // Left
  539. case 72: // H
  540. case 75: // K
  541. e.preventDefault();
  542. shower.previous();
  543. break;
  544. case 34: // PgDown
  545. case 40: // Down
  546. case 39: // Right
  547. case 76: // L
  548. case 74: // J
  549. e.preventDefault();
  550. shower.next();
  551. break;
  552. case 36: // Home
  553. e.preventDefault();
  554. shower.first();
  555. break;
  556. case 35: // End
  557. e.preventDefault();
  558. shower.last();
  559. break;
  560. case 9: // Tab = +1; Shift + Tab = -1
  561. case 32: // Space = +1; Shift + Space = -1
  562. e.preventDefault();
  563. shower[e.shiftKey ? 'previous' : 'next']();
  564. break;
  565. default:
  566. // Behave as usual
  567. }
  568. }, false);
  569. document.addEventListener('click', function(e) {
  570. var slideNumber = shower.getSlideNumber(shower._getSlideIdByEl(e.target));
  571. // Click on slide in List mode
  572. if (shower.isListMode() && shower._getSlideIdByEl(e.target)) {
  573. // Warning: go must be before enterSlideMode.
  574. // Otherwise there is a bug in Chrome
  575. shower.go(slideNumber);
  576. shower.enterSlideMode();
  577. shower.showPresenterNotes(slideNumber);
  578. }
  579. }, false);
  580. document.addEventListener('touchstart', function(e) {
  581. if (shower._getSlideIdByEl(e.target)) {
  582. if (shower.isSlideMode() && ! shower._checkInteractiveElement(e)) {
  583. var x = e.touches[0].pageX;
  584. if (x > window.innerWidth / 2) {
  585. shower.next();
  586. } else {
  587. shower.previous();
  588. }
  589. }
  590. if (shower.isListMode()) {
  591. shower.go(shower.getSlideNumber(shower._getSlideIdByEl(e.target)));
  592. shower.enterSlideMode();
  593. }
  594. }
  595. }, false);
  596. document.addEventListener('touchmove', function(e) {
  597. if (shower.isSlideMode()) {
  598. e.preventDefault();
  599. }
  600. }, false);
  601. return shower;
  602. })(this, this.document);