UNPKG

storm-wall

Version:

Interactive animating content wall

267 lines (233 loc) 9.9 kB
/** * @name storm-wall: Interactive animating content wall * @version 1.2.4: Tue, 09 Apr 2019 08:27:53 GMT * @author stormid * @license MIT */ import throttle from 'raf-throttle'; import scrollTo from './libs/scrollTo'; import inView from './libs/inView'; import easeInOutQuad from './libs/easeInOutQuad'; import { defaults } from './defaults'; import { CONSTANTS } from './constants'; const StormWall = { init(){ this.openIndex = false; this.initThrottled(); this.initItems(); this.initTriggers(); this.initPanel(); this.initButtons(); window.addEventListener('resize', this.throttledResize.bind(this)); setTimeout(this.equalHeight.bind(this), 100); this.node.classList.add(this.settings.classNames.ready.substr(1)); setTimeout(() => { if(!!window.location.hash && !!~document.getElementById(window.location.hash.slice(1)).className.indexOf(this.settings.classNames.trigger.substr(1))) document.getElementById(window.location.hash.slice(1)).click(); }, 260); return this; }, initThrottled(){ this.throttledResize = throttle(() => { this.equalHeight(this.setPanelTop.bind(this)); }); this.throttledChange = throttle(this.change); this.throttledPrevious = throttle(this.previous); this.throttledNext = throttle(this.next); }, initTriggers(){ this.items.forEach((item, i) => { let trigger = item.node.querySelector(this.settings.classNames.trigger); if(!trigger) throw new Error(CONSTANTS.ERRORS.TRIGGER); CONSTANTS.EVENTS.forEach(ev => { trigger.addEventListener(ev, e => { if(e.keyCode && !~CONSTANTS.KEYCODES.indexOf(e.keyCode)) return; this.throttledChange(i); e.preventDefault(); }); }); }); }, initPanel(){ let elementFactory = (element, className, attributes) => { let el = document.createElement(element); el.className = className; for (var k in attributes) { if (attributes.hasOwnProperty(k)) { el.setAttribute(k, attributes[k]); } } return el; }, panelElement = elementFactory(this.items[0].node.tagName.toLowerCase(), this.settings.classNames.panel.substr(1), { 'aria-hidden': true }); this.panelInner = elementFactory('div', this.settings.classNames.panelInner.substr(1)); this.panel = this.node.appendChild(panelElement); return this; }, initButtons(){ let buttonsTemplate = `<button class="${this.settings.classNames.closeButton.substr(1)}" aria-label="close"> <svg fill="#000000" height="30" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> <path d="M0 0h24v24H0z" fill="none"/> </svg> </button> <button class="${this.settings.classNames.previousButton.substr(1)}" aria-label="previous"> <svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> <path d="M0 0h24v24H0z" fill="none"/> </svg> </button> <button class="${this.settings.classNames.nextButton.substr(1)}" aria-label="next"> <svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/> <path d="M0 0h24v24H0z" fill="none"/> </svg> </button>`; this.panel.innerHTML = `${this.panel.innerHTML}${buttonsTemplate}`; CONSTANTS.EVENTS.forEach(ev => { this.panel.querySelector(this.settings.classNames.closeButton).addEventListener(ev, e => { if(e.keyCode && !~CONSTANTS.KEYCODES.indexOf(e.keyCode)) return; this.close.call(this); }); this.panel.querySelector(this.settings.classNames.previousButton).addEventListener(ev, e => { if(e.keyCode && !~CONSTANTS.KEYCODES.indexOf(e.keyCode)) return; this.throttledPrevious.call(this); }); this.panel.querySelector(this.settings.classNames.nextButton).addEventListener(ev, e => { if(e.keyCode && !~CONSTANTS.KEYCODES.indexOf(e.keyCode)) return; this.throttledNext.call(this); }); }); }, initItems(){ let items = [].slice.call(this.node.querySelectorAll(this.settings.classNames.item)); if(items.length === 0) throw new Error(CONSTANTS.ERRORS.ITEM); this.items = items.map(item => { return { node: item, content: item.querySelector(this.settings.classNames.content), trigger: item.querySelector(this.settings.classNames.trigger) }; }); }, change(i){ if(this.openIndex === false) return this.open(i); if(this.openIndex === i) return this.close(); if (this.items[this.openIndex].node.offsetTop === this.items[i].node.offsetTop) this.close(() => this.open(i, this.panel.offsetHeight), this.panel.offsetHeight); else this.close(() => this.open(i)); }, open(i, start, speed){ this.panelSourceContainer = this.items[i].content; this.openIndex = i; this.setPanelTop(); this.panelContent = this.panelSourceContainer.firstElementChild.cloneNode(true); this.panelInner.appendChild(this.panelContent); this.panelSourceContainer.removeChild(this.panelSourceContainer.firstElementChild); this.panel.insertBefore(this.panelInner, this.panel.firstElementChild); let currentTime = 0, panelStart = start || 0, totalPanelChange = this.panel.offsetHeight - panelStart, rowStart = this.closedHeight + panelStart, totalRowChange = totalPanelChange, duration = speed || 16, animateOpen = () => { currentTime++; this.panel.style.height = easeInOutQuad(currentTime, panelStart, totalPanelChange, duration) + 'px'; this.resizeRow(this.items[this.openIndex].node, easeInOutQuad(currentTime, rowStart, totalRowChange, duration) + 'px'); if (currentTime < duration) window.requestAnimationFrame(animateOpen.bind(this)); else { this.panel.style.height = 'auto'; this.items[i].node.parentNode.insertBefore(this.panel, this.items[i].node.nextElementSibling); (!!window.history && !!window.history.pushState) && window.history.pushState({ URL: `#${this.items[i].trigger.getAttribute('id')}`}, '', `#${this.items[i].trigger.getAttribute('id')}`); if (!inView(this.panel, () => { return { l: 0, t: 0, b: (window.innerHeight || document.documentElement.clientHeight) - this.panel.offsetHeight, r: (window.innerWidth || document.documentElement.clientWidth) }; })) scrollTo(this.panel.offsetTop - this.settings.offset); } }; this.node.classList.add(this.settings.classNames.open.substr(1)); this.panel.removeAttribute('aria-hidden'); this.items[i].trigger.setAttribute('aria-expanded', true); animateOpen.call(this); return this; }, close(cb, end, speed){ let endPoint = end || 0, currentTime = 0, panelStart = this.panel.offsetHeight, totalPanelChange = endPoint - panelStart, rowStart = this.items[this.openIndex].node.offsetHeight, totalRowChange = totalPanelChange, duration = speed || 16, animateClosed = () => { currentTime++; this.panel.style.height = easeInOutQuad(currentTime, panelStart, totalPanelChange, duration) + 'px'; this.resizeRow(this.items[this.openIndex].node, easeInOutQuad(currentTime, rowStart, totalRowChange, duration) + 'px'); if (currentTime < duration) window.requestAnimationFrame(animateClosed.bind(this)); else { if (!endPoint) this.panel.style.height = 'auto'; this.panelInner.removeChild(this.panelContent); this.panel.setAttribute('aria-hidden', true); this.items[this.openIndex].trigger.setAttribute('aria-expanded', false); this.panelSourceContainer.appendChild(this.panelContent); this.node.classList.remove(this.settings.classNames.animating.substr(1)); this.node.classList.remove(this.settings.classNames.open.substr(1)); this.openIndex = false; if(typeof cb === 'function') cb(); else (!!window.history && !!window.history.pushState) && history.pushState('', document.title, window.location.pathname + window.location.search); } }; this.node.classList.add(this.settings.classNames.animating.substr(1)); animateClosed.call(this); }, previous() { return this.change((this.openIndex - 1 < 0 ? this.items.length - 1 : this.openIndex - 1)); }, next() { return this.change((this.openIndex + 1 === this.items.length ? 0 : this.openIndex + 1)); }, equalHeight(cb) { let openHeight = 0, closedHeight = 0; this.items.map((item, i) => { item.node.style.height = 'auto'; if (this.openIndex !== false && item.node.offsetTop === this.items[this.openIndex].node.offsetTop) { if (this.openIndex === i) openHeight = item.node.offsetHeight + this.panel.offsetHeight; } else { if (item.node.offsetHeight > closedHeight) closedHeight = item.node.offsetHeight; } return item; }).map((item, i) => { if (this.openIndex !== i) item.node.style.height = closedHeight + 'px'; }); this.openHeight = openHeight; this.closedHeight = closedHeight === 0 ? this.closedHeight : closedHeight; if (this.openHeight > 0) { this.resizeRow(this.items[this.openIndex].node, this.openHeight + 'px'); typeof cb === 'function' && cb(); } }, resizeRow(el, height){ this.items.forEach(item => { if (item.node.offsetTop === el.offsetTop) item.node.style.height = height; }); return this; }, setPanelTop() { this.panel.style.top = `${this.items[this.openIndex].node.offsetTop + this.items[this.openIndex].trigger.offsetHeight}px`; } }; const init = (sel, opts) => { let els = [].slice.call(document.querySelectorAll(sel)); if(els.length === 0) throw new Error(CONSTANTS.ERRORS.ROOT); return els.map(el => { return Object.assign(Object.create(StormWall), { node: el, settings: Object.assign({}, defaults, opts) }).init(); }); }; export default { init };