@shower/core
Advanced tools
Comparing version 3.0.0 to 3.1.0
/** | ||
* Core for Shower HTML presentation engine | ||
* @shower/core v3.0.0-2, https://github.com/shower/core | ||
* @copyright 2010–2020 Vadim Makeev, https://pepelsbey.net | ||
* @shower/core v3.0.0, https://github.com/shower/core | ||
* @copyright 2010–2021 Vadim Makeev, https://pepelsbey.net | ||
* @license MIT | ||
@@ -10,40 +10,4 @@ */ | ||
const EVENT_TARGET = Symbol('EventTarget'); | ||
class EventTarget { | ||
constructor() { | ||
this[EVENT_TARGET] = document.createElement('div'); | ||
} | ||
addEventListener(...args) { | ||
this[EVENT_TARGET].addEventListener(...args); | ||
} | ||
removeEventListener(...args) { | ||
this[EVENT_TARGET].removeEventListener(...args); | ||
} | ||
dispatchEvent(event) { | ||
Object.defineProperties(event, { | ||
target: { value: this }, | ||
currentTarget: { value: this }, | ||
}); | ||
return this[EVENT_TARGET].dispatchEvent(event); | ||
} | ||
} | ||
const isInteractiveElement = (element) => element.tabIndex !== -1; | ||
const freezeHistory = (callback) => { | ||
history.pushState = () => {}; | ||
history.replaceState = () => {}; | ||
try { | ||
callback(); | ||
} finally { | ||
delete history.pushState; | ||
delete history.replaceState; | ||
} | ||
}; | ||
const contentLoaded = (callback) => { | ||
@@ -57,2 +21,15 @@ if (document.currentScript.async) { | ||
const defineReadOnly = (target, props) => { | ||
for (const [key, value] of Object.entries(props)) { | ||
Object.defineProperty(target, key, { | ||
value, | ||
writable: false, | ||
enumerable: true, | ||
configurable: true, | ||
}); | ||
} | ||
}; | ||
class ShowerError extends Error {} | ||
var defaultOptions = { | ||
@@ -71,18 +48,28 @@ containerSelector: '.shower', | ||
/** | ||
* @param {HTMLElement} element | ||
* @param {object} options | ||
*/ | ||
class Slide extends EventTarget { | ||
constructor(element, options) { | ||
/** | ||
* @param {Shower} shower | ||
* @param {HTMLElement} element | ||
*/ | ||
constructor(shower, element) { | ||
super(); | ||
this.element = element; | ||
this.options = options; | ||
defineReadOnly(this, { | ||
shower, | ||
element, | ||
state: { | ||
visitCount: 0, | ||
innerStepCount: 0, | ||
}, | ||
}); | ||
this._isActive = false; | ||
this.state = { | ||
visitsCount: 0, | ||
innerStepsCount: 0, | ||
}; | ||
this._options = this.shower.options; | ||
this.element.addEventListener('click', (event) => { | ||
if (event.defaultPrevented) return; | ||
this.activate(); | ||
this.shower.enterFullMode(); | ||
}); | ||
} | ||
@@ -95,3 +82,3 @@ | ||
get isVisited() { | ||
return this.state.visitsCount > 0; | ||
return this.state.visitCount > 0; | ||
} | ||
@@ -104,23 +91,53 @@ | ||
get title() { | ||
const titleElement = this.element.querySelector(this.options.slideTitleSelector); | ||
const titleElement = this.element.querySelector(this._options.slideTitleSelector); | ||
return titleElement ? titleElement.innerText : ''; | ||
} | ||
/** | ||
* Deactivates currently active slide (if any) and activates itself. | ||
* @emits Slide#deactivate | ||
* @emits Slide#activate | ||
* @emits Shower#slidechange | ||
*/ | ||
activate() { | ||
if (this._isActive) return; | ||
const prev = this.shower.activeSlide; | ||
if (prev) { | ||
prev._deactivate(); | ||
} | ||
this.state.visitCount++; | ||
this.element.classList.add(this._options.activeSlideClass); | ||
this._isActive = true; | ||
this.state.visitsCount++; | ||
this.element.classList.add(this.options.activeSlideClass); | ||
this.dispatchEvent(new Event('activate')); | ||
this.shower.dispatchEvent( | ||
new CustomEvent('slidechange', { | ||
detail: { prev }, | ||
}), | ||
); | ||
} | ||
/** | ||
* @throws {ShowerError} | ||
* @emits Slide#deactivate | ||
*/ | ||
deactivate() { | ||
if (!this._isActive) return; | ||
if (this.shower.isFullMode) { | ||
throw new ShowerError('In full mode, another slide should be activated instead.'); | ||
} | ||
this._isActive = false; | ||
if (this._isActive) { | ||
this._deactivate(); | ||
} | ||
} | ||
_deactivate() { | ||
this.element.classList.replace( | ||
this.options.activeSlideClass, | ||
this.options.visitedSlideClass, | ||
this._options.activeSlideClass, | ||
this._options.visitedSlideClass, | ||
); | ||
this._isActive = false; | ||
this.dispatchEvent(new Event('deactivate')); | ||
@@ -160,2 +177,7 @@ } | ||
shower.addEventListener('start', () => { | ||
updateDocumentRole(); | ||
updateLiveRegion(); | ||
}); | ||
shower.addEventListener('modechange', updateDocumentRole); | ||
@@ -188,2 +210,3 @@ shower.addEventListener('slidechange', updateLiveRegion); | ||
case 'BACKSPACE': | ||
case 'PAGEUP': | ||
@@ -287,11 +310,8 @@ case 'ARROWUP': | ||
const applyURLMode = () => { | ||
const isFull = new URL(location).searchParams.has('full'); | ||
freezeHistory(() => { | ||
if (isFull) { | ||
shower.enterFullMode(); | ||
} else { | ||
shower.exitFullMode(); | ||
} | ||
}); | ||
const isFull = new URLSearchParams(location.search).has('full'); | ||
if (isFull) { | ||
shower.enterFullMode(); | ||
} else { | ||
shower.exitFullMode(); | ||
} | ||
}; | ||
@@ -305,5 +325,3 @@ | ||
if (target) { | ||
freezeHistory(() => { | ||
target.activate(); | ||
}); | ||
target.activate(); | ||
} else if (!shower.activeSlide) { | ||
@@ -319,2 +337,9 @@ shower.first(); // invalid hash | ||
applyURL(); | ||
window.addEventListener('popstate', applyURL); | ||
shower.addEventListener('start', () => { | ||
history.replaceState(null, document.title, composeURL()); | ||
}); | ||
shower.addEventListener('modechange', () => { | ||
@@ -325,56 +350,61 @@ history.replaceState(null, document.title, composeURL()); | ||
shower.addEventListener('slidechange', () => { | ||
history.pushState(null, document.title, composeURL()); | ||
const url = composeURL(); | ||
if (!location.href.endsWith(url)) { | ||
history.pushState(null, document.title, url); | ||
} | ||
}); | ||
shower.addEventListener('start', applyURL); | ||
window.addEventListener('popstate', applyURL); | ||
}; | ||
var next = (shower) => { | ||
const { stepSelector, activeSlideClass } = shower.options; | ||
const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options; | ||
let innerSteps; | ||
let innerAt; | ||
let activeIndex; | ||
const getInnerSteps = () => { | ||
const { element } = shower.activeSlide; | ||
return [...element.querySelectorAll(stepSelector)]; | ||
}; | ||
const isActive = (step) => step.classList.contains(activeSlideClass); | ||
const isVisited = (step) => step.classList.contains(visitedSlideClass); | ||
const getInnerAt = () => { | ||
return innerSteps.filter((step) => { | ||
return step.classList.contains(activeSlideClass); | ||
}).length; | ||
}; | ||
const setInnerStepsState = () => { | ||
if (shower.isListMode) return; | ||
const toggleActive = () => { | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(activeSlideClass, index < innerAt); | ||
}); | ||
const slide = shower.activeSlide; | ||
innerSteps = [...slide.element.querySelectorAll(stepSelector)]; | ||
activeIndex = | ||
innerSteps.length && innerSteps.every(isVisited) | ||
? innerSteps.length | ||
: innerSteps.filter(isActive).length - 1; | ||
slide.state.innerStepCount = innerSteps.length; | ||
}; | ||
shower.addEventListener('slidechange', () => { | ||
innerSteps = getInnerSteps(); | ||
innerAt = getInnerAt(); | ||
shower.addEventListener('start', setInnerStepsState); | ||
shower.addEventListener('modechange', setInnerStepsState); | ||
shower.addEventListener('slidechange', setInnerStepsState); | ||
const slide = shower.activeSlide; | ||
slide.state.innerStepsCount = innerSteps.length; | ||
}); | ||
shower.addEventListener('next', (event) => { | ||
if (event.defaultPrevented || !event.cancelable) return; | ||
if (shower.isListMode || innerAt === innerSteps.length) return; | ||
if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; | ||
event.preventDefault(); | ||
innerAt++; | ||
toggleActive(); | ||
activeIndex++; | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(visitedSlideClass, index < activeIndex); | ||
step.classList.toggle(activeSlideClass, index === activeIndex); | ||
}); | ||
if (activeIndex < innerSteps.length) { | ||
event.preventDefault(); | ||
} | ||
}); | ||
shower.addEventListener('prev', (event) => { | ||
if (event.defaultPrevented || !event.cancelable) return; | ||
if (shower.isListMode || innerAt === innerSteps.length || !innerAt) return; | ||
if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; | ||
if (activeIndex === -1 || activeIndex === innerSteps.length) return; | ||
activeIndex--; | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(visitedSlideClass, index < activeIndex + 1); | ||
step.classList.toggle(activeSlideClass, index === activeIndex); | ||
}); | ||
event.preventDefault(); | ||
innerAt--; | ||
toggleActive(); | ||
}); | ||
@@ -402,25 +432,6 @@ }; | ||
shower.addEventListener('start', updateProgress); | ||
shower.addEventListener('slidechange', updateProgress); | ||
}; | ||
var scale = (shower) => { | ||
const { container } = shower; | ||
const getScale = () => { | ||
const maxRatio = Math.max( | ||
container.offsetWidth / window.innerWidth, | ||
container.offsetHeight / window.innerHeight, | ||
); | ||
return `scale(${1 / maxRatio})`; | ||
}; | ||
const updateStyle = () => { | ||
container.style.transform = shower.isFullMode ? getScale() : ''; | ||
}; | ||
shower.addEventListener('modechange', updateStyle); | ||
window.addEventListener('resize', updateStyle); | ||
window.addEventListener('load', updateStyle); | ||
}; | ||
const units = ['s', 'm', 'h']; | ||
@@ -458,3 +469,3 @@ const hasUnits = (timing) => { | ||
const setTimer = () => { | ||
const resetTimer = () => { | ||
clearTimeout(id); | ||
@@ -464,4 +475,4 @@ if (shower.isListMode) return; | ||
const slide = shower.activeSlide; | ||
const { visitsCount, innerStepsCount } = slide.state; | ||
if (visitsCount > 1) return; | ||
const { visitCount, innerStepCount } = slide.state; | ||
if (visitCount > 1) return; | ||
@@ -471,4 +482,4 @@ const timing = parseTiming(slide.element.dataset.timing); | ||
if (innerStepsCount) { | ||
const stepTiming = timing / (innerStepsCount + 1); | ||
if (innerStepCount) { | ||
const stepTiming = timing / (innerStepCount + 1); | ||
id = setInterval(() => shower.next(), stepTiming); | ||
@@ -480,4 +491,5 @@ } else { | ||
shower.addEventListener('modechange', setTimer); | ||
shower.addEventListener('slidechange', setTimer); | ||
shower.addEventListener('start', resetTimer); | ||
shower.addEventListener('modechange', resetTimer); | ||
shower.addEventListener('slidechange', resetTimer); | ||
@@ -508,2 +520,3 @@ shower.container.addEventListener('keydown', (event) => { | ||
shower.addEventListener('start', updateTitle); | ||
shower.addEventListener('modechange', updateTitle); | ||
@@ -517,12 +530,35 @@ shower.addEventListener('slidechange', updateTitle); | ||
shower.addEventListener('modechange', () => { | ||
if (container.classList.contains(fullModeClass)) { | ||
shower.enterFullMode(); | ||
} else { | ||
container.classList.add(listModeClass); | ||
} | ||
const updateScale = () => { | ||
const firstSlide = shower.slides[0]; | ||
if (!firstSlide) return; | ||
const { innerWidth, innerHeight } = window; | ||
const { offsetWidth, offsetHeight } = firstSlide.element; | ||
const listScale = 1 / (offsetWidth / innerWidth); | ||
const fullScale = 1 / Math.max(offsetWidth / innerWidth, offsetHeight / innerHeight); | ||
container.style.setProperty('--shower-list-scale', listScale); | ||
container.style.setProperty('--shower-full-scale', fullScale); | ||
}; | ||
const updateModeView = () => { | ||
if (shower.isFullMode) { | ||
container.classList.remove(listModeClass); | ||
container.classList.add(fullModeClass); | ||
return; | ||
} else { | ||
container.classList.remove(fullModeClass); | ||
container.classList.add(listModeClass); | ||
} | ||
container.classList.remove(fullModeClass); | ||
container.classList.add(listModeClass); | ||
updateScale(); | ||
if (shower.isFullMode) return; | ||
const slide = shower.activeSlide; | ||
@@ -532,5 +568,9 @@ if (slide) { | ||
} | ||
}); | ||
}; | ||
shower.addEventListener('start', updateModeView); | ||
shower.addEventListener('modechange', updateModeView); | ||
shower.addEventListener('slidechange', () => { | ||
if (shower.isFullMode) return; | ||
const slide = shower.activeSlide; | ||
@@ -540,9 +580,3 @@ slide.element.scrollIntoView({ block: 'nearest' }); | ||
shower.addEventListener('start', () => { | ||
if (container.classList.contains(fullModeClass)) { | ||
shower.enterFullMode(); | ||
} else { | ||
container.classList.add(listModeClass); | ||
} | ||
}); | ||
window.addEventListener('resize', updateScale); | ||
}; | ||
@@ -552,15 +586,13 @@ | ||
a11y(shower); | ||
keys(shower); // should come before `timer` | ||
progress(shower); | ||
next(shower); // should come before `timer` | ||
timer(shower); | ||
title(shower); // should come before `location` | ||
location$1(shower); | ||
keys(shower); | ||
next(shower); | ||
timer(shower); // should come after `keys` and `next` | ||
title(shower); | ||
location$1(shower); // should come after `title` | ||
view(shower); | ||
scale(shower); | ||
}; | ||
const ensureSlideId = (slideElement, index) => { | ||
if (!slideElement.id) { | ||
slideElement.id = index + 1; | ||
// maintains invariant: active slide always exists in `full` mode | ||
if (shower.isFullMode && !shower.activeSlide) { | ||
shower.first(); | ||
} | ||
@@ -570,17 +602,33 @@ }; | ||
class Shower extends EventTarget { | ||
/** | ||
* @param {object=} options | ||
*/ | ||
constructor(options) { | ||
super(); | ||
defineReadOnly(this, { | ||
options: { ...defaultOptions, ...options }, | ||
}); | ||
this._mode = 'list'; | ||
this._isStarted = false; | ||
this.options = { ...defaultOptions, ...options }; | ||
this._container = null; | ||
} | ||
/** | ||
* @param {object} options | ||
* @param {object=} options | ||
* @throws {ShowerError} | ||
*/ | ||
configure(options) { | ||
if (this._isStarted) { | ||
throw new ShowerError('Shower should be configured before it is started.'); | ||
} | ||
Object.assign(this.options, options); | ||
} | ||
/** | ||
* @throws {ShowerError} | ||
* @emits Shower#start | ||
*/ | ||
start() { | ||
@@ -590,18 +638,13 @@ if (this._isStarted) return; | ||
const { containerSelector } = this.options; | ||
this.container = document.querySelector(containerSelector); | ||
if (!this.container) { | ||
throw new Error(`Shower container with selector '${containerSelector}' not found.`); | ||
this._container = document.querySelector(containerSelector); | ||
if (!this._container) { | ||
throw new ShowerError( | ||
`Shower container with selector '${containerSelector}' was not found.`, | ||
); | ||
} | ||
this._isStarted = true; | ||
this._initSlides(); | ||
installModules(this); | ||
// maintains invariant: active slide always exists in `full` mode | ||
this.addEventListener('modechange', () => { | ||
if (this.isFullMode && !this.activeSlide) { | ||
this.first(); | ||
} | ||
}); | ||
installModules(this); | ||
this._isStarted = true; | ||
this.dispatchEvent(new Event('start')); | ||
@@ -611,41 +654,34 @@ } | ||
_initSlides() { | ||
const slideElements = [ | ||
...this.container.querySelectorAll(this.options.slideSelector), | ||
].filter((slideElement) => !slideElement.hidden); | ||
const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`; | ||
const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector); | ||
slideElements.forEach(ensureSlideId); | ||
this.slides = slideElements.map((slideElement) => { | ||
const slide = new Slide(slideElement, this.options); | ||
this.slides = Array.from(visibleSlideElements, (slideElement, index) => { | ||
if (!slideElement.id) { | ||
slideElement.id = index + 1; | ||
} | ||
slide.addEventListener('activate', () => { | ||
this._changeActiveSlide(slide); | ||
}); | ||
slide.element.addEventListener('click', () => { | ||
if (this.isListMode) { | ||
this.enterFullMode(); | ||
slide.activate(); | ||
} | ||
}); | ||
return slide; | ||
return new Slide(this, slideElement); | ||
}); | ||
} | ||
_changeActiveSlide(next) { | ||
const prev = this.slides.find((slide) => { | ||
return slide.isActive && slide !== next; | ||
}); | ||
_setMode(mode) { | ||
if (mode === this._mode) return; | ||
if (prev) { | ||
prev.deactivate(); | ||
} | ||
this._mode = mode; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
const event = new CustomEvent('slidechange', { | ||
detail: { prev }, | ||
}); | ||
/** | ||
* @param {Event} event | ||
*/ | ||
dispatchEvent(event) { | ||
if (!this._isStarted) return false; | ||
this.dispatchEvent(event); | ||
return super.dispatchEvent(event); | ||
} | ||
get container() { | ||
return this._container; | ||
} | ||
get isFullMode() { | ||
@@ -669,8 +705,6 @@ return this._mode === 'full'; | ||
* Slide fills the maximum area. | ||
* @emits Shower#modechange | ||
*/ | ||
enterFullMode() { | ||
if (!this.isFullMode) { | ||
this._mode = 'full'; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
this._setMode('full'); | ||
} | ||
@@ -680,8 +714,6 @@ | ||
* Shower returns into list mode. | ||
* @emits Shower#modechange | ||
*/ | ||
exitFullMode() { | ||
if (!this.isListMode) { | ||
this._mode = 'list'; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
this._setMode('list'); | ||
} | ||
@@ -702,3 +734,3 @@ | ||
*/ | ||
go(delta) { | ||
goBy(delta) { | ||
this.goTo(this.activeSlideIndex + delta); | ||
@@ -708,3 +740,4 @@ } | ||
/** | ||
* @param {boolean=} isForce | ||
* @param {boolean} [isForce=false] | ||
* @emits Shower#prev | ||
*/ | ||
@@ -714,3 +747,3 @@ prev(isForce) { | ||
if (this.dispatchEvent(prev)) { | ||
this.go(-1); | ||
this.goBy(-1); | ||
} | ||
@@ -720,3 +753,4 @@ } | ||
/** | ||
* @param {boolean=} isForce | ||
* @param {boolean} [isForce=false] | ||
* @emits Shower#next | ||
*/ | ||
@@ -726,3 +760,3 @@ next(isForce) { | ||
if (this.dispatchEvent(next)) { | ||
this.go(1); | ||
this.goBy(1); | ||
} | ||
@@ -729,0 +763,0 @@ } |
@@ -31,4 +31,9 @@ const createLiveRegion = () => { | ||
shower.addEventListener('start', () => { | ||
updateDocumentRole(); | ||
updateLiveRegion(); | ||
}); | ||
shower.addEventListener('modechange', updateDocumentRole); | ||
shower.addEventListener('slidechange', updateLiveRegion); | ||
}; |
@@ -26,2 +26,3 @@ import { isInteractiveElement } from '../utils'; | ||
case 'BACKSPACE': | ||
case 'PAGEUP': | ||
@@ -28,0 +29,0 @@ case 'ARROWUP': |
@@ -1,3 +0,1 @@ | ||
import { freezeHistory } from '../utils'; | ||
export default (shower) => { | ||
@@ -13,11 +11,8 @@ const composeURL = () => { | ||
const applyURLMode = () => { | ||
const isFull = new URL(location).searchParams.has('full'); | ||
freezeHistory(() => { | ||
if (isFull) { | ||
shower.enterFullMode(); | ||
} else { | ||
shower.exitFullMode(); | ||
} | ||
}); | ||
const isFull = new URLSearchParams(location.search).has('full'); | ||
if (isFull) { | ||
shower.enterFullMode(); | ||
} else { | ||
shower.exitFullMode(); | ||
} | ||
}; | ||
@@ -31,5 +26,3 @@ | ||
if (target) { | ||
freezeHistory(() => { | ||
target.activate(); | ||
}); | ||
target.activate(); | ||
} else if (!shower.activeSlide) { | ||
@@ -45,2 +38,9 @@ shower.first(); // invalid hash | ||
applyURL(); | ||
window.addEventListener('popstate', applyURL); | ||
shower.addEventListener('start', () => { | ||
history.replaceState(null, document.title, composeURL()); | ||
}); | ||
shower.addEventListener('modechange', () => { | ||
@@ -51,7 +51,7 @@ history.replaceState(null, document.title, composeURL()); | ||
shower.addEventListener('slidechange', () => { | ||
history.pushState(null, document.title, composeURL()); | ||
const url = composeURL(); | ||
if (!location.href.endsWith(url)) { | ||
history.pushState(null, document.title, url); | ||
} | ||
}); | ||
shower.addEventListener('start', applyURL); | ||
window.addEventListener('popstate', applyURL); | ||
}; |
export default (shower) => { | ||
const { stepSelector, activeSlideClass } = shower.options; | ||
const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options; | ||
let innerSteps; | ||
let innerAt; | ||
let activeIndex; | ||
const getInnerSteps = () => { | ||
const { element } = shower.activeSlide; | ||
return [...element.querySelectorAll(stepSelector)]; | ||
}; | ||
const isActive = (step) => step.classList.contains(activeSlideClass); | ||
const isVisited = (step) => step.classList.contains(visitedSlideClass); | ||
const getInnerAt = () => { | ||
return innerSteps.filter((step) => { | ||
return step.classList.contains(activeSlideClass); | ||
}).length; | ||
}; | ||
const setInnerStepsState = () => { | ||
if (shower.isListMode) return; | ||
const toggleActive = () => { | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(activeSlideClass, index < innerAt); | ||
}); | ||
const slide = shower.activeSlide; | ||
innerSteps = [...slide.element.querySelectorAll(stepSelector)]; | ||
activeIndex = | ||
innerSteps.length && innerSteps.every(isVisited) | ||
? innerSteps.length | ||
: innerSteps.filter(isActive).length - 1; | ||
slide.state.innerStepCount = innerSteps.length; | ||
}; | ||
shower.addEventListener('slidechange', () => { | ||
innerSteps = getInnerSteps(); | ||
innerAt = getInnerAt(); | ||
shower.addEventListener('start', setInnerStepsState); | ||
shower.addEventListener('modechange', setInnerStepsState); | ||
shower.addEventListener('slidechange', setInnerStepsState); | ||
const slide = shower.activeSlide; | ||
slide.state.innerStepsCount = innerSteps.length; | ||
}); | ||
shower.addEventListener('next', (event) => { | ||
if (event.defaultPrevented || !event.cancelable) return; | ||
if (shower.isListMode || innerAt === innerSteps.length) return; | ||
if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; | ||
event.preventDefault(); | ||
innerAt++; | ||
toggleActive(); | ||
activeIndex++; | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(visitedSlideClass, index < activeIndex); | ||
step.classList.toggle(activeSlideClass, index === activeIndex); | ||
}); | ||
if (activeIndex < innerSteps.length) { | ||
event.preventDefault(); | ||
} | ||
}); | ||
shower.addEventListener('prev', (event) => { | ||
if (event.defaultPrevented || !event.cancelable) return; | ||
if (shower.isListMode || innerAt === innerSteps.length || !innerAt) return; | ||
if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; | ||
if (activeIndex === -1 || activeIndex === innerSteps.length) return; | ||
activeIndex--; | ||
innerSteps.forEach((step, index) => { | ||
step.classList.toggle(visitedSlideClass, index < activeIndex + 1); | ||
step.classList.toggle(activeSlideClass, index === activeIndex); | ||
}); | ||
event.preventDefault(); | ||
innerAt--; | ||
toggleActive(); | ||
}); | ||
}; |
@@ -20,3 +20,4 @@ export default (shower) => { | ||
shower.addEventListener('start', updateProgress); | ||
shower.addEventListener('slidechange', updateProgress); | ||
}; |
@@ -18,4 +18,5 @@ const mdash = '\u2014'; | ||
shower.addEventListener('start', updateTitle); | ||
shower.addEventListener('modechange', updateTitle); | ||
shower.addEventListener('slidechange', updateTitle); | ||
}; |
@@ -5,12 +5,35 @@ export default (shower) => { | ||
shower.addEventListener('modechange', () => { | ||
if (container.classList.contains(fullModeClass)) { | ||
shower.enterFullMode(); | ||
} else { | ||
container.classList.add(listModeClass); | ||
} | ||
const updateScale = () => { | ||
const firstSlide = shower.slides[0]; | ||
if (!firstSlide) return; | ||
const { innerWidth, innerHeight } = window; | ||
const { offsetWidth, offsetHeight } = firstSlide.element; | ||
const listScale = 1 / (offsetWidth / innerWidth); | ||
const fullScale = 1 / Math.max(offsetWidth / innerWidth, offsetHeight / innerHeight); | ||
container.style.setProperty('--shower-list-scale', listScale); | ||
container.style.setProperty('--shower-full-scale', fullScale); | ||
}; | ||
const updateModeView = () => { | ||
if (shower.isFullMode) { | ||
container.classList.remove(listModeClass); | ||
container.classList.add(fullModeClass); | ||
return; | ||
} else { | ||
container.classList.remove(fullModeClass); | ||
container.classList.add(listModeClass); | ||
} | ||
container.classList.remove(fullModeClass); | ||
container.classList.add(listModeClass); | ||
updateScale(); | ||
if (shower.isFullMode) return; | ||
const slide = shower.activeSlide; | ||
@@ -20,5 +43,9 @@ if (slide) { | ||
} | ||
}); | ||
}; | ||
shower.addEventListener('start', updateModeView); | ||
shower.addEventListener('modechange', updateModeView); | ||
shower.addEventListener('slidechange', () => { | ||
if (shower.isFullMode) return; | ||
const slide = shower.activeSlide; | ||
@@ -28,9 +55,3 @@ slide.element.scrollIntoView({ block: 'nearest' }); | ||
shower.addEventListener('start', () => { | ||
if (container.classList.contains(fullModeClass)) { | ||
shower.enterFullMode(); | ||
} else { | ||
container.classList.add(listModeClass); | ||
} | ||
}); | ||
window.addEventListener('resize', updateScale); | ||
}; |
import defaultOptions from './default-options'; | ||
import Slide from './slide'; | ||
import { EventTarget } from './utils'; | ||
import installModules from './modules'; | ||
import { defineReadOnly, ShowerError } from './utils'; | ||
import installModules from './modules/install'; | ||
const ensureSlideId = (slideElement, index) => { | ||
if (!slideElement.id) { | ||
slideElement.id = index + 1; | ||
} | ||
}; | ||
class Shower extends EventTarget { | ||
/** | ||
* @param {object=} options | ||
*/ | ||
constructor(options) { | ||
super(); | ||
defineReadOnly(this, { | ||
options: { ...defaultOptions, ...options }, | ||
}); | ||
this._mode = 'list'; | ||
this._isStarted = false; | ||
this.options = { ...defaultOptions, ...options }; | ||
this._container = null; | ||
} | ||
/** | ||
* @param {object} options | ||
* @param {object=} options | ||
* @throws {ShowerError} | ||
*/ | ||
configure(options) { | ||
if (this._isStarted) { | ||
throw new ShowerError('Shower should be configured before it is started.'); | ||
} | ||
Object.assign(this.options, options); | ||
} | ||
/** | ||
* @throws {ShowerError} | ||
* @emits Shower#start | ||
*/ | ||
start() { | ||
@@ -32,18 +42,13 @@ if (this._isStarted) return; | ||
const { containerSelector } = this.options; | ||
this.container = document.querySelector(containerSelector); | ||
if (!this.container) { | ||
throw new Error(`Shower container with selector '${containerSelector}' not found.`); | ||
this._container = document.querySelector(containerSelector); | ||
if (!this._container) { | ||
throw new ShowerError( | ||
`Shower container with selector '${containerSelector}' was not found.`, | ||
); | ||
} | ||
this._isStarted = true; | ||
this._initSlides(); | ||
installModules(this); | ||
// maintains invariant: active slide always exists in `full` mode | ||
this.addEventListener('modechange', () => { | ||
if (this.isFullMode && !this.activeSlide) { | ||
this.first(); | ||
} | ||
}); | ||
installModules(this); | ||
this._isStarted = true; | ||
this.dispatchEvent(new Event('start')); | ||
@@ -53,41 +58,34 @@ } | ||
_initSlides() { | ||
const slideElements = [ | ||
...this.container.querySelectorAll(this.options.slideSelector), | ||
].filter((slideElement) => !slideElement.hidden); | ||
const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`; | ||
const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector); | ||
slideElements.forEach(ensureSlideId); | ||
this.slides = slideElements.map((slideElement) => { | ||
const slide = new Slide(slideElement, this.options); | ||
this.slides = Array.from(visibleSlideElements, (slideElement, index) => { | ||
if (!slideElement.id) { | ||
slideElement.id = index + 1; | ||
} | ||
slide.addEventListener('activate', () => { | ||
this._changeActiveSlide(slide); | ||
}); | ||
slide.element.addEventListener('click', () => { | ||
if (this.isListMode) { | ||
this.enterFullMode(); | ||
slide.activate(); | ||
} | ||
}); | ||
return slide; | ||
return new Slide(this, slideElement); | ||
}); | ||
} | ||
_changeActiveSlide(next) { | ||
const prev = this.slides.find((slide) => { | ||
return slide.isActive && slide !== next; | ||
}); | ||
_setMode(mode) { | ||
if (mode === this._mode) return; | ||
if (prev) { | ||
prev.deactivate(); | ||
} | ||
this._mode = mode; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
const event = new CustomEvent('slidechange', { | ||
detail: { prev }, | ||
}); | ||
/** | ||
* @param {Event} event | ||
*/ | ||
dispatchEvent(event) { | ||
if (!this._isStarted) return false; | ||
this.dispatchEvent(event); | ||
return super.dispatchEvent(event); | ||
} | ||
get container() { | ||
return this._container; | ||
} | ||
get isFullMode() { | ||
@@ -111,8 +109,6 @@ return this._mode === 'full'; | ||
* Slide fills the maximum area. | ||
* @emits Shower#modechange | ||
*/ | ||
enterFullMode() { | ||
if (!this.isFullMode) { | ||
this._mode = 'full'; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
this._setMode('full'); | ||
} | ||
@@ -122,8 +118,6 @@ | ||
* Shower returns into list mode. | ||
* @emits Shower#modechange | ||
*/ | ||
exitFullMode() { | ||
if (!this.isListMode) { | ||
this._mode = 'list'; | ||
this.dispatchEvent(new Event('modechange')); | ||
} | ||
this._setMode('list'); | ||
} | ||
@@ -144,3 +138,3 @@ | ||
*/ | ||
go(delta) { | ||
goBy(delta) { | ||
this.goTo(this.activeSlideIndex + delta); | ||
@@ -150,3 +144,4 @@ } | ||
/** | ||
* @param {boolean=} isForce | ||
* @param {boolean} [isForce=false] | ||
* @emits Shower#prev | ||
*/ | ||
@@ -156,3 +151,3 @@ prev(isForce) { | ||
if (this.dispatchEvent(prev)) { | ||
this.go(-1); | ||
this.goBy(-1); | ||
} | ||
@@ -162,3 +157,4 @@ } | ||
/** | ||
* @param {boolean=} isForce | ||
* @param {boolean} [isForce=false] | ||
* @emits Shower#next | ||
*/ | ||
@@ -168,3 +164,3 @@ next(isForce) { | ||
if (this.dispatchEvent(next)) { | ||
this.go(1); | ||
this.goBy(1); | ||
} | ||
@@ -171,0 +167,0 @@ } |
@@ -1,19 +0,29 @@ | ||
import { EventTarget } from './utils'; | ||
import { defineReadOnly, ShowerError } from './utils'; | ||
/** | ||
* @param {HTMLElement} element | ||
* @param {object} options | ||
*/ | ||
class Slide extends EventTarget { | ||
constructor(element, options) { | ||
/** | ||
* @param {Shower} shower | ||
* @param {HTMLElement} element | ||
*/ | ||
constructor(shower, element) { | ||
super(); | ||
this.element = element; | ||
this.options = options; | ||
defineReadOnly(this, { | ||
shower, | ||
element, | ||
state: { | ||
visitCount: 0, | ||
innerStepCount: 0, | ||
}, | ||
}); | ||
this._isActive = false; | ||
this.state = { | ||
visitsCount: 0, | ||
innerStepsCount: 0, | ||
}; | ||
this._options = this.shower.options; | ||
this.element.addEventListener('click', (event) => { | ||
if (event.defaultPrevented) return; | ||
this.activate(); | ||
this.shower.enterFullMode(); | ||
}); | ||
} | ||
@@ -26,3 +36,3 @@ | ||
get isVisited() { | ||
return this.state.visitsCount > 0; | ||
return this.state.visitCount > 0; | ||
} | ||
@@ -35,23 +45,53 @@ | ||
get title() { | ||
const titleElement = this.element.querySelector(this.options.slideTitleSelector); | ||
const titleElement = this.element.querySelector(this._options.slideTitleSelector); | ||
return titleElement ? titleElement.innerText : ''; | ||
} | ||
/** | ||
* Deactivates currently active slide (if any) and activates itself. | ||
* @emits Slide#deactivate | ||
* @emits Slide#activate | ||
* @emits Shower#slidechange | ||
*/ | ||
activate() { | ||
if (this._isActive) return; | ||
const prev = this.shower.activeSlide; | ||
if (prev) { | ||
prev._deactivate(); | ||
} | ||
this.state.visitCount++; | ||
this.element.classList.add(this._options.activeSlideClass); | ||
this._isActive = true; | ||
this.state.visitsCount++; | ||
this.element.classList.add(this.options.activeSlideClass); | ||
this.dispatchEvent(new Event('activate')); | ||
this.shower.dispatchEvent( | ||
new CustomEvent('slidechange', { | ||
detail: { prev }, | ||
}), | ||
); | ||
} | ||
/** | ||
* @throws {ShowerError} | ||
* @emits Slide#deactivate | ||
*/ | ||
deactivate() { | ||
if (!this._isActive) return; | ||
if (this.shower.isFullMode) { | ||
throw new ShowerError('In full mode, another slide should be activated instead.'); | ||
} | ||
this._isActive = false; | ||
if (this._isActive) { | ||
this._deactivate(); | ||
} | ||
} | ||
_deactivate() { | ||
this.element.classList.replace( | ||
this.options.activeSlideClass, | ||
this.options.visitedSlideClass, | ||
this._options.activeSlideClass, | ||
this._options.visitedSlideClass, | ||
); | ||
this._isActive = false; | ||
this.dispatchEvent(new Event('deactivate')); | ||
@@ -58,0 +98,0 @@ } |
{ | ||
"name": "@shower/core", | ||
"description": "Core for Shower HTML presentation engine", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"publishConfig": { | ||
@@ -28,38 +28,37 @@ "access": "public" | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"chai-dom": "^1.8.1", | ||
"chromedriver": "^81.0.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.1.0", | ||
"eslint-config-prettier": "^6.10.1", | ||
"eslint-plugin-import": "^2.17.3", | ||
"eslint-plugin-prettier": "^3.1.0", | ||
"esm": "^3.2.25", | ||
"husky": "^4.2.5", | ||
"lint-staged": "^10.1.3", | ||
"mocha": "^7.1.1", | ||
"nightwatch": "^1.3.4", | ||
"prettier": "^2.0.4", | ||
"puppeteer": "^3.0.0", | ||
"rollup": "^2.6.1", | ||
"rollup-plugin-commonjs": "^10.0.0", | ||
"rollup-plugin-node-resolve": "^5.0.3", | ||
"sauce-connect-launcher": "^1.2.7", | ||
"serve": "^11.0.2", | ||
"serve-handler": "^6.0.2", | ||
"yn": "^4.0.0" | ||
"chai": "^4.3.0", | ||
"chromedriver": "^83.0.1", | ||
"eslint": "^7.21.0", | ||
"eslint-config-airbnb-base": "^14.2.1", | ||
"eslint-config-prettier": "^6.15.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-prettier": "^3.3.1", | ||
"husky": "^4.3.8", | ||
"lint-staged": "^10.5.4", | ||
"mocha": "^7.2.0", | ||
"nightwatch": "^1.5.1", | ||
"prettier": "^2.2.1", | ||
"puppeteer": "^3.3.0", | ||
"rollup": "^2.40.0", | ||
"sauce-connect-launcher": "^1.3.2", | ||
"serve": "^11.3.2", | ||
"serve-handler": "^6.1.3" | ||
}, | ||
"scripts": { | ||
"build": "rollup --config", | ||
"build:watch": "rollup --config --watch", | ||
"serve": "serve dist", | ||
"lint": "eslint '**/*.js'", | ||
"lint:fix": "eslint '**/*.js' --fix", | ||
"lint": "eslint '**/*.{js,mjs}'", | ||
"lint:fix": "eslint '**/*.{js,mjs}' --fix", | ||
"format": "npm run format:js && npm run format:etc", | ||
"format:js": "prettier '**/*.js' --write", | ||
"format:js": "prettier '**/*.{js,mjs}' --write", | ||
"format:etc": "prettier '**/*.{json,md,yml}' --write", | ||
"test": "npm run lint && npm run test:unit && npm run test:local", | ||
"test:unit": "mocha test/unit --require esm --require chai/register-should", | ||
"test:local": "nightwatch --env chrome-local", | ||
"test:sauce": "nightwatch --env chrome,firefox,safari,edge" | ||
"test:unit": "mocha test/unit --require chai/register-should", | ||
"test:local": "nightwatch --env chrome-local --timeout 500", | ||
"test:sauce": "nightwatch --env chrome,firefox,safari" | ||
}, | ||
"config": { | ||
"test_port": 8031 | ||
} | ||
} |
@@ -9,3 +9,3 @@ # Core of Shower [![Build Status](https://travis-ci.org/shower/core.svg?branch=master)](https://travis-ci.org/shower/core) | ||
npm install shower | ||
npm install @shower/shower | ||
@@ -12,0 +12,0 @@ You can also install core as a separate package: |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47228
17
1238
20