diff --git a/src/modules/hash.js b/src/modules/hash.js new file mode 100644 index 0000000..6aee80d --- /dev/null +++ b/src/modules/hash.js @@ -0,0 +1,25 @@ +const HASH = '#slide'; +const slideRegex = /#slide=(\d+)/; + +export default class Hash { + static getSlideNumber() { + let results = document.location.hash.match(slideRegex); + let slide = 0; + + if (Array.isArray(results)) { + slide = parseInt(results[1], 10); + } + + if (!Number.isInteger(slide) || slide < 0 || !Array.isArray(results)) { + slide = null; + } else { + slide--; // Convert to 0 index + } + + return slide; + } + + static setSlideNumber(number) { + history.pushState(null, `Slide ${number}`, `${HASH}=${number}`); + } +} diff --git a/src/modules/navigation.js b/src/modules/navigation.js index 79e3501..19d04de 100644 --- a/src/modules/navigation.js +++ b/src/modules/navigation.js @@ -9,12 +9,12 @@ const ELEMENT_ID = { const LABELS = { VERTICAL: { - NEXT: '↓', - PREV: '→' + NEXT: '↓', + PREV: '↑' }, HORIZONTAL: { - NEXT: '↑', - PREV: '←' + NEXT: '→', + PREV: '←' } }; @@ -31,7 +31,6 @@ export default class Navigation { this.el.appendChild(this.next); this.el.appendChild(this.prev); this.el.appendChild(this.counter); - console.log(this); } updateCounter(current, max) { diff --git a/src/modules/slide.js b/src/modules/slide.js new file mode 100644 index 0000000..efa8b14 --- /dev/null +++ b/src/modules/slide.js @@ -0,0 +1,46 @@ +import DOM from '../utils/dom'; + +const CLASSES = { + SLIDE: 'slide', + CURRENT: 'current' +}; + +export default class Slide { + constructor(el, i) { + this.el = el; + this.parent = el.parentNode; + this.i = i; + this.el.id = 'section-' + (i + 1); + + this.el.classList.add(CLASSES.SLIDE); + + // Hide slides by default + this.hide(); + } + + hide() { + DOM.hide(this.el); + this.el.classList.remove(CLASSES.CURRENT); + } + + show() { + DOM.show(this.el); + this.el.classList.add(CLASSES.CURRENT); + } + + moveAfterLast() { + const last = this.parent.childNodes[this.parent.childElementCount - 1]; + + this.parent.insertBefore(this.el, last.nextSibling); + } + + moveBeforeFirst() { + const first = this.parent.childNodes[0]; + + this.parent.insertBefore(this.el, first); + } + + static isCandidate(el) { + return el.nodeType === 1 && el.tagName === 'SECTION'; + } +} diff --git a/src/modules/webslides.js b/src/modules/webslides.js index 33125d2..d001481 100644 --- a/src/modules/webslides.js +++ b/src/modules/webslides.js @@ -1,33 +1,167 @@ +import Hash from './hash'; import Navigation from './navigation'; +import Slide from './slide'; +import DOM from '../utils/dom'; +import ScrollHelper from '../utils/scroll-to'; + +const CLASSES = { + VERTICAL: 'vertical' +}; export default class WebSlides { constructor() { this.el = document.getElementById('webslides'); - this.moving = false; - this.currentSlide = 0; + this.isMoving = false; + this.slides = null; + this.navigation = null; + this.currentSlideI_ = -1; + this.currentSlide_ = null; + this.maxSlide_ = 0; + this.isVertical = this.el.classList.contains(CLASSES.VERTICAL); if (!this.el) { throw new Error('Couldn\'t find the webslides container!'); } + this.removeChildren_(); + this.grabSlides_(); this.createNav_(); - this.navigation.updateCounter(this.currentSlide + 1, this.slides.length); + this.initSlides_(); + + window.st = ScrollHelper; + } + + removeChildren_() { + const nodes = this.el.childNodes; + let i = nodes.length; + + while (i--) { + const node = nodes[i]; + + if (!Slide.isCandidate(node)) { + this.el.removeChild(node); + } + } } createNav_() { this.navigation = new Navigation({ - isVertical: true + isVertical: this.isVertical }); + this.el.appendChild(this.navigation.el); } grabSlides_() { - this.slides = Array.from(this.el.getElementsByClassName('slide')); + this.slides = Array.from(this.el.childNodes) + .map((slide, i) => new Slide(slide, i)); + + this.maxSlide_ = this.slides.length; } - goToSlide(slide) { - if (slide >= 0 && slide < this.slides.length) { - console.log('Foo'); + goToSlide(slideI, forward = null) { + if (this.isValidIndexSlide_(slideI) && !this.isMoving) { + this.isMoving = true; + let isMovingForward = false; + + if (forward !== null) { + isMovingForward = forward; + } else { + if (Number.isInteger(this.currentSlideI_)) { + isMovingForward = slideI > this.currentSlideI_; + } + } + + const nextSlide = this.slides[slideI]; + + if (this.currentSlide_ !== null) { + this.animateToSlide_(isMovingForward, nextSlide, this.onSlideChange_); + } else { + this.transitionToSlide_( + isMovingForward, nextSlide, this.onSlideChange_); + nextSlide.moveBeforeFirst(); + } } } + + animateToSlide_(isMovingForward, nextSlide, callback) { + DOM.lockScroll(); + + nextSlide.show(); + + ScrollHelper.scrollTo(nextSlide.el.offsetTop, 500, () => { + this.currentSlide_.hide(); + + if (isMovingForward) { + this.currentSlide_.moveAfterLast(); + } else { + nextSlide.moveBeforeFirst(); + } + + DOM.unlockScroll(); + callback.call(this, nextSlide); + }); + } + + transitionToSlide_(isMovingForward, nextSlide, callback) { + ScrollHelper.scrollTo(0, 0); + + nextSlide.show(); + + if (this.currentSlide_) { + if (isMovingForward) { + this.currentSlide_.moveAfterLast(); + } else { + nextSlide.moveBeforeFirst(); + } + } + + callback.call(this, nextSlide); + } + + onSlideChange_(slide) { + this.currentSlide_ = slide; + this.currentSlideI_ = slide.i; + this.navigation.updateCounter( + this.currentSlideI_ + 1, this.maxSlide_); + this.isMoving = false; + + Hash.setSlideNumber(this.currentSlideI_ + 1); + } + + goNext() { + let nextIndex = this.currentSlideI_ + 1; + + if (nextIndex >= this.maxSlide_) { + nextIndex = 0; + } + + this.goToSlide(nextIndex, true); + } + + goPrev() { + let prevIndex = this.currentSlideI_ - 1; + + if (prevIndex < 0) { + prevIndex = this.maxSlide_ - 1; + } + + this.goToSlide(prevIndex, false); + } + + isValidIndexSlide_(i) { + return i >= 0 && i < this.maxSlide_; + } + + initSlides_() { + let slideNumber = Hash.getSlideNumber(); + + // Not valid + if (slideNumber === null || + slideNumber >= this.maxSlide_) { + slideNumber = 0; + } + + this.goToSlide(slideNumber); + } } diff --git a/src/utils/dom.js b/src/utils/dom.js index 11cd14e..4caa078 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -9,4 +9,20 @@ export default class DOM { return node; } + + static hide(el) { + el.style.display = 'none'; + } + + static show(el) { + el.style.display = ''; + } + + static lockScroll() { + document.documentElement.style.overflow = 'hidden'; + } + + static unlockScroll() { + document.documentElement.style.overflow = 'auto'; + } } diff --git a/src/utils/easing.js b/src/utils/easing.js new file mode 100644 index 0000000..68604c6 --- /dev/null +++ b/src/utils/easing.js @@ -0,0 +1,9 @@ +function swing (p) { + return 0.5 - Math.cos(p * Math.PI) / 2; +} + +function linear(p) { + return p; +} + +export default { swing, linear }; diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js new file mode 100644 index 0000000..94888e5 --- /dev/null +++ b/src/utils/scroll-to.js @@ -0,0 +1,58 @@ +import Easings from './easing'; + +let SCROLLABLE_CONTAINER; + +/** + * Returns the correct DOM element to be used for scrolling the + * page, due to Firefox not scrolling on document.body. + * @return {Element} Scrollable Element. + */ +function getScrollableContainer() { + if (SCROLLABLE_CONTAINER) { + return SCROLLABLE_CONTAINER; + } + + const documentElement = window.document.documentElement; + let scrollableContainer; + + documentElement.scrollTop = 1; + + if (documentElement.scrollTop === 1) { + documentElement.scrollTop = 0; + scrollableContainer = documentElement; + } else { + scrollableContainer = document.body; + } + + SCROLLABLE_CONTAINER = scrollableContainer; + + return scrollableContainer; +} + +function scrollTo(y, duration = 500, cb = () => {}) { + const scrollableContainer = getScrollableContainer(); + const delta = y - scrollableContainer.scrollTop; + const increment = 20; + + const animateScroll = elapsedTime => { + elapsedTime += increment; + const percent = elapsedTime / duration; + + scrollableContainer.scrollTop = Easings.swing( + percent, + elapsedTime * percent, + y, + delta, + duration) * delta; + + if (elapsedTime < duration) { + setTimeout(() => animateScroll(elapsedTime), increment); + } else { + cb(); + } + }; + + animateScroll(0); +} + +export default { getScrollableContainer, scrollTo }; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 922ad66..0b4fc02 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -10,7 +10,8 @@ module.exports = { }, output: { filename: '[name].js', - path: path.join(__dirname, 'dist') + path: path.join(__dirname, 'dist'), + publicPath: '/dist', }, devServer: { contentBase: __dirname,