Commit b8e67a43 authored by Vadim Makeev's avatar Vadim Makeev

Updates

parent 6841741a
...@@ -20,11 +20,11 @@ ...@@ -20,11 +20,11 @@
<section class="slide" id="cover"> <section class="slide" id="cover">
<h2>Shower Presentation Engine</h2> <h2>Shower Presentation Engine</h2>
<p>Brought to you by <a href="http://pepelsbey.net">Vadim Makeev</a></p> <p>Brought to you by <a href="https://pepelsbey.net">Vadim Makeev</a></p>
<figure> <figure>
<img class="cover" src="pictures/cover.jpg" alt="Hands on the orange typewriter in a park"> <img class="cover" src="pictures/cover.jpg" alt="Hands on the orange typewriter in a park">
<figcaption class="copyright right white"> <figcaption class="copyright right white">
<a href="http://fiftyfootshadows.net/">© John Carey</a> <a href="https://fiftyfootshadows.net">© John Carey</a>
</figcaption> </figcaption>
</figure> </figure>
<style> <style>
...@@ -155,7 +155,7 @@ ...@@ -155,7 +155,7 @@
<figure> <figure>
<img class="cover" src="pictures/picture.jpg" alt="Orange typewriter on a wooden table close-up"> <img class="cover" src="pictures/picture.jpg" alt="Orange typewriter on a wooden table close-up">
<figcaption class="copyright right white"> <figcaption class="copyright right white">
<a href="http://fiftyfootshadows.net/">© John Carey</a> <a href="https://fiftyfootshadows.net">© John Carey</a>
</figcaption> </figcaption>
</figure> </figure>
<style> <style>
......
No preview for this file type
/** /**
* Core for Shower HTML presentation engine * Core for Shower HTML presentation engine
* @shower/core v3.0.0-2, https://github.com/shower/core * @shower/core v3.0.0, https://github.com/shower/core
* @copyright 2010–2020 Vadim Makeev, https://pepelsbey.net * @copyright 2010–2021 Vadim Makeev, https://pepelsbey.net
* @license MIT * @license MIT
*/ */
(function () { (function () {
'use strict'; 'use strict';
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 isInteractiveElement = (element) => element.tabIndex !== -1;
const freezeHistory = (callback) => {
history.pushState = () => {};
history.replaceState = () => {};
try {
callback();
} finally {
delete history.pushState;
delete history.replaceState;
}
};
const contentLoaded = (callback) => { const contentLoaded = (callback) => {
if (document.currentScript.async) { if (document.currentScript.async) {
...@@ -53,6 +17,19 @@ ...@@ -53,6 +17,19 @@
} }
}; };
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 = { var defaultOptions = {
containerSelector: '.shower', containerSelector: '.shower',
progressSelector: '.progress', progressSelector: '.progress',
...@@ -66,22 +43,32 @@ ...@@ -66,22 +43,32 @@
visitedSlideClass: 'visited', visitedSlideClass: 'visited',
}; };
/**
* @param {HTMLElement} element
* @param {object} options
*/
class Slide extends EventTarget { class Slide extends EventTarget {
constructor(element, options) { /**
* @param {Shower} shower
* @param {HTMLElement} element
*/
constructor(shower, element) {
super(); super();
this.element = element; defineReadOnly(this, {
this.options = options; shower,
element,
state: {
visitCount: 0,
innerStepCount: 0,
},
});
this._isActive = false; this._isActive = false;
this.state = { this._options = this.shower.options;
visitsCount: 0,
innerStepsCount: 0, this.element.addEventListener('click', (event) => {
}; if (event.defaultPrevented) return;
this.activate();
this.shower.enterFullMode();
});
} }
get isActive() { get isActive() {
...@@ -89,7 +76,7 @@ ...@@ -89,7 +76,7 @@
} }
get isVisited() { get isVisited() {
return this.state.visitsCount > 0; return this.state.visitCount > 0;
} }
get id() { get id() {
...@@ -97,27 +84,57 @@ ...@@ -97,27 +84,57 @@
} }
get title() { get title() {
const titleElement = this.element.querySelector(this.options.slideTitleSelector); const titleElement = this.element.querySelector(this._options.slideTitleSelector);
return titleElement ? titleElement.innerText : ''; return titleElement ? titleElement.innerText : '';
} }
/**
* Deactivates currently active slide (if any) and activates itself.
* @emits Slide#deactivate
* @emits Slide#activate
* @emits Shower#slidechange
*/
activate() { activate() {
if (this._isActive) return; 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._isActive = true;
this.state.visitsCount++;
this.element.classList.add(this.options.activeSlideClass);
this.dispatchEvent(new Event('activate')); this.dispatchEvent(new Event('activate'));
this.shower.dispatchEvent(
new CustomEvent('slidechange', {
detail: { prev },
}),
);
} }
/**
* @throws {ShowerError}
* @emits Slide#deactivate
*/
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.element.classList.replace(
this.options.activeSlideClass, this._options.activeSlideClass,
this.options.visitedSlideClass, this._options.visitedSlideClass,
); );
this._isActive = false;
this.dispatchEvent(new Event('deactivate')); this.dispatchEvent(new Event('deactivate'));
} }
} }
...@@ -152,6 +169,11 @@ ...@@ -152,6 +169,11 @@
} }
}; };
shower.addEventListener('start', () => {
updateDocumentRole();
updateLiveRegion();
});
shower.addEventListener('modechange', updateDocumentRole); shower.addEventListener('modechange', updateDocumentRole);
shower.addEventListener('slidechange', updateLiveRegion); shower.addEventListener('slidechange', updateLiveRegion);
}; };
...@@ -179,6 +201,7 @@ ...@@ -179,6 +201,7 @@
} }
break; break;
case 'BACKSPACE':
case 'PAGEUP': case 'PAGEUP':
case 'ARROWUP': case 'ARROWUP':
case 'ARROWLEFT': case 'ARROWLEFT':
...@@ -277,15 +300,12 @@ ...@@ -277,15 +300,12 @@
}; };
const applyURLMode = () => { const applyURLMode = () => {
const isFull = new URL(location).searchParams.has('full'); const isFull = new URLSearchParams(location.search).has('full');
if (isFull) {
freezeHistory(() => { shower.enterFullMode();
if (isFull) { } else {
shower.enterFullMode(); shower.exitFullMode();
} else { }
shower.exitFullMode();
}
});
}; };
const applyURLSlide = () => { const applyURLSlide = () => {
...@@ -294,9 +314,7 @@ ...@@ -294,9 +314,7 @@
const target = shower.slides.find((slide) => slide.id === id); const target = shower.slides.find((slide) => slide.id === id);
if (target) { if (target) {
freezeHistory(() => { target.activate();
target.activate();
});
} else if (!shower.activeSlide) { } else if (!shower.activeSlide) {
shower.first(); // invalid hash shower.first(); // invalid hash
} }
...@@ -307,65 +325,77 @@ ...@@ -307,65 +325,77 @@
applyURLSlide(); applyURLSlide();
}; };
applyURL();
window.addEventListener('popstate', applyURL);
shower.addEventListener('start', () => {
history.replaceState(null, document.title, composeURL());
});
shower.addEventListener('modechange', () => { shower.addEventListener('modechange', () => {
history.replaceState(null, document.title, composeURL()); history.replaceState(null, document.title, composeURL());
}); });
shower.addEventListener('slidechange', () => { 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) => { var next = (shower) => {
const { stepSelector, activeSlideClass } = shower.options; const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options;
let innerSteps; let innerSteps;
let innerAt; let activeIndex;
const getInnerSteps = () => { const isActive = (step) => step.classList.contains(activeSlideClass);
const { element } = shower.activeSlide; const isVisited = (step) => step.classList.contains(visitedSlideClass);
return [...element.querySelectorAll(stepSelector)];
};
const getInnerAt = () => { const setInnerStepsState = () => {
return innerSteps.filter((step) => { if (shower.isListMode) return;
return step.classList.contains(activeSlideClass);
}).length;
};
const toggleActive = () => { const slide = shower.activeSlide;
innerSteps.forEach((step, index) => {
step.classList.toggle(activeSlideClass, index < innerAt);
});
};
shower.addEventListener('slidechange', () => { innerSteps = [...slide.element.querySelectorAll(stepSelector)];
innerSteps = getInnerSteps(); activeIndex =
innerAt = getInnerAt(); innerSteps.length && innerSteps.every(isVisited)
? innerSteps.length
: innerSteps.filter(isActive).length - 1;
const slide = shower.activeSlide; slide.state.innerStepCount = innerSteps.length;
slide.state.innerStepsCount = innerSteps.length; };
});
shower.addEventListener('start', setInnerStepsState);
shower.addEventListener('modechange', setInnerStepsState);
shower.addEventListener('slidechange', setInnerStepsState);
shower.addEventListener('next', (event) => { shower.addEventListener('next', (event) => {
if (event.defaultPrevented || !event.cancelable) return; if (shower.isListMode || event.defaultPrevented || !event.cancelable) return;
if (shower.isListMode || innerAt === innerSteps.length) return;
event.preventDefault(); activeIndex++;
innerAt++; innerSteps.forEach((step, index) => {
toggleActive(); step.classList.toggle(visitedSlideClass, index < activeIndex);
step.classList.toggle(activeSlideClass, index === activeIndex);
});
if (activeIndex < innerSteps.length) {
event.preventDefault();
}
}); });
shower.addEventListener('prev', (event) => { shower.addEventListener('prev', (event) => {
if (event.defaultPrevented || !event.cancelable) return; if (shower.isListMode || event.defaultPrevented || !event.cancelable) return;
if (shower.isListMode || innerAt === innerSteps.length || !innerAt) 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(); event.preventDefault();
innerAt--;
toggleActive();
}); });
}; };
...@@ -388,29 +418,10 @@ ...@@ -388,29 +418,10 @@
bar.setAttribute('aria-valuetext', `Slideshow progress: ${progress}%`); bar.setAttribute('aria-valuetext', `Slideshow progress: ${progress}%`);
}; };
shower.addEventListener('start', updateProgress);
shower.addEventListener('slidechange', 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']; const units = ['s', 'm', 'h'];
const hasUnits = (timing) => { const hasUnits = (timing) => {
return units.some((unit) => timing.includes(unit)); return units.some((unit) => timing.includes(unit));
...@@ -443,27 +454,28 @@ ...@@ -443,27 +454,28 @@
var timer = (shower) => { var timer = (shower) => {
let id; let id;
const setTimer = () => { const resetTimer = () => {
clearTimeout(id); clearTimeout(id);
if (shower.isListMode) return; if (shower.isListMode) return;
const slide = shower.activeSlide; const slide = shower.activeSlide;
const { visitsCount, innerStepsCount } = slide.state; const { visitCount, innerStepCount } = slide.state;
if (visitsCount > 1) return; if (visitCount > 1) return;
const timing = parseTiming(slide.element.dataset.timing); const timing = parseTiming(slide.element.dataset.timing);
if (!timing) return; if (!timing) return;
if (innerStepsCount) { if (innerStepCount) {
const stepTiming = timing / (innerStepsCount + 1); const stepTiming = timing / (innerStepCount + 1);
id = setInterval(() => shower.next(), stepTiming); id = setInterval(() => shower.next(), stepTiming);
} else { } else {
id = setTimeout(() => shower.next(), timing); id = setTimeout(() => shower.next(), timing);
} }
}; };
shower.addEventListener('modechange', setTimer); shower.addEventListener('start', resetTimer);
shower.addEventListener('slidechange', setTimer); shower.addEventListener('modechange', resetTimer);
shower.addEventListener('slidechange', resetTimer);
shower.container.addEventListener('keydown', (event) => { shower.container.addEventListener('keydown', (event) => {
if (!event.defaultPrevented) { if (!event.defaultPrevented) {
...@@ -489,6 +501,7 @@ ...@@ -489,6 +501,7 @@
document.title = title; document.title = title;
}; };
shower.addEventListener('start', updateTitle);
shower.addEventListener('modechange', updateTitle); shower.addEventListener('modechange', updateTitle);
shower.addEventListener('slidechange', updateTitle); shower.addEventListener('slidechange', updateTitle);
}; };
...@@ -497,131 +510,154 @@ ...@@ -497,131 +510,154 @@
const { container } = shower; const { container } = shower;
const { fullModeClass, listModeClass } = shower.options; const { fullModeClass, listModeClass } = shower.options;
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) { if (shower.isFullMode) {
container.classList.remove(listModeClass); container.classList.remove(listModeClass);
container.classList.add(fullModeClass); container.classList.add(fullModeClass);
return; } else {
container.classList.remove(fullModeClass);
container.classList.add(listModeClass);
} }
container.classList.remove(fullModeClass); updateScale();
container.classList.add(listModeClass);
if (shower.isFullMode) return;
const slide = shower.activeSlide; const slide = shower.activeSlide;
if (slide) { if (slide) {
slide.element.scrollIntoView({ block: 'center' }); slide.element.scrollIntoView({ block: 'center' });
} }
}); };
shower.addEventListener('start', updateModeView);
shower.addEventListener('modechange', updateModeView);
shower.addEventListener('slidechange', () => { shower.addEventListener('slidechange', () => {
if (shower.isFullMode) return;
const slide = shower.activeSlide; const slide = shower.activeSlide;
slide.element.scrollIntoView({ block: 'nearest' }); slide.element.scrollIntoView({ block: 'nearest' });
}); });
shower.addEventListener('start', () => { window.addEventListener('resize', updateScale);
if (container.classList.contains(fullModeClass)) {
shower.enterFullMode();
} else {
container.classList.add(listModeClass);
}
});
}; };
var installModules = (shower) => { var installModules = (shower) => {
a11y(shower); a11y(shower);
keys(shower); // should come before `timer`
progress(shower); progress(shower);
next(shower); // should come before `timer` keys(shower);
timer(shower); next(shower);
title(shower); // should come before `location` timer(shower); // should come after `keys` and `next`
location$1(shower); title(shower);
location$1(shower); // should come after `title`
view(shower); view(shower);
scale(shower);
};
const ensureSlideId = (slideElement, index) => { // maintains invariant: active slide always exists in `full` mode
if (!slideElement.id) { if (shower.isFullMode && !shower.activeSlide) {
slideElement.id = index + 1; shower.first();
} }
}; };
class Shower extends EventTarget { class Shower extends EventTarget {
/**
* @param {object=} options
*/
constructor(options) { constructor(options) {
super(); super();
defineReadOnly(this, {
options: { ...defaultOptions, ...options },
});
this._mode = 'list'; this._mode = 'list';
this._isStarted = false; this._isStarted = false;
this.options = { ...defaultOptions, ...options }; this._container = null;
} }
/** /**
* @param {object} options * @param {object=} options
* @throws {ShowerError}
*/ */
configure(options) { configure(options) {
if (this._isStarted) {
throw new ShowerError('Shower should be configured before it is started.');
}
Object.assign(this.options, options); Object.assign(this.options, options);
} }
/**
* @throws {ShowerError}
* @emits Shower#start
*/
start() { start() {
if (this._isStarted) return; if (this._isStarted) return;
const { containerSelector } = this.options; const { containerSelector } = this.options;
this.container = document.querySelector(containerSelector); this._container = document.querySelector(containerSelector);
if (!this.container) { if (!this._container) {
throw new Error(`Shower container with selector '${containerSelector}' not found.`); throw new ShowerError(
`Shower container with selector '${containerSelector}' was not found.`,
);
} }
this._isStarted = true;
this._initSlides(); this._initSlides();
// maintains invariant: active slide always exists in `full` mode
this.addEventListener('modechange', () => {
if (this.isFullMode && !this.activeSlide) {
this.first();
}
});
installModules(this); installModules(this);
this._isStarted = true;
this.dispatchEvent(new Event('start')); this.dispatchEvent(new Event('start'));
} }
_initSlides() { _initSlides() {
const slideElements = [ const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`;
...this.container.querySelectorAll(this.options.slideSelector), const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector);
].filter((slideElement) => !slideElement.hidden);
slideElements.forEach(ensureSlideId);
this.slides = slideElements.map((slideElement) => {
const slide = new Slide(slideElement, this.options);
slide.addEventListener('activate', () => {
this._changeActiveSlide(slide);
});
slide.element.addEventListener('click', () => {
if (this.isListMode) {
this.enterFullMode();
slide.activate();
}
});
return slide; this.slides = Array.from(visibleSlideElements, (slideElement, index) => {
if (!slideElement.id) {
slideElement.id = index + 1;
}
return new Slide(this, slideElement);
}); });
} }
_changeActiveSlide(next) { _setMode(mode) {
const prev = this.slides.find((slide) => { if (mode === this._mode) return;
return slide.isActive && slide !== next;
});
if (prev) { this._mode = mode;
prev.deactivate(); 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() { get isFullMode() {
...@@ -642,22 +678,18 @@ ...@@ -642,22 +678,18 @@
/** /**
* Slide fills the maximum area. * Slide fills the maximum area.
* @emits Shower#modechange
*/ */
enterFullMode() { enterFullMode() {
if (!this.isFullMode) { this._setMode('full');
this._mode = 'full';
this.dispatchEvent(new Event('modechange'));
}
} }
/** /**
* Shower returns into list mode. * Shower returns into list mode.
* @emits Shower#modechange
*/ */
exitFullMode() { exitFullMode() {
if (!this.isListMode) { this._setMode('list');
this._mode = 'list';
this.dispatchEvent(new Event('modechange'));
}
} }
/** /**
...@@ -673,27 +705,29 @@ ...@@ -673,27 +705,29 @@
/** /**
* @param {number} delta * @param {number} delta
*/ */
go(delta) { goBy(delta) {
this.goTo(this.activeSlideIndex + delta); this.goTo(this.activeSlideIndex + delta);
} }
/** /**
* @param {boolean=} isForce * @param {boolean} [isForce=false]
* @emits Shower#prev
*/ */
prev(isForce) { prev(isForce) {
const prev = new Event('prev', { cancelable: !isForce }); const prev = new Event('prev', { cancelable: !isForce });
if (this.dispatchEvent(prev)) { if (this.dispatchEvent(prev)) {
this.go(-1); this.goBy(-1);
} }
} }
/** /**
* @param {boolean=} isForce * @param {boolean} [isForce=false]
* @emits Shower#next
*/ */
next(isForce) { next(isForce) {
const next = new Event('next', { cancelable: !isForce }); const next = new Event('next', { cancelable: !isForce });
if (this.dispatchEvent(next)) { if (this.dispatchEvent(next)) {
this.go(1); this.goBy(1);
} }
} }
......
...@@ -8,7 +8,7 @@ Default theme for the [Shower](https://github.com/shower/shower/) presentation e ...@@ -8,7 +8,7 @@ Default theme for the [Shower](https://github.com/shower/shower/) presentation e
Get the Shower template where Material is already included. Download the [template archive](https://shwr.me/shower.zip) or install the package: Get the Shower template where Material is already included. Download the [template archive](https://shwr.me/shower.zip) or install the package:
npm install shower npm install @shower/shower
If you want to install Material separately you can install the package: If you want to install Material separately you can install the package:
......
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
width: var(--slide-width); width: var(--slide-width);
height: var(--slide-height); height: var(--slide-height);
background-color: black; background-color: black;
transform: scale(var(--shower-full-scale));
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.columns { .columns {
display: grid; display: grid;
width: 100%; width: 100%;
grid-column-gap: 75px; grid-column-gap: 48px;
} }
.columns.two { .columns.two {
......
...@@ -8,7 +8,7 @@ Default theme for the [Shower](https://github.com/shower/shower/) presentation e ...@@ -8,7 +8,7 @@ Default theme for the [Shower](https://github.com/shower/shower/) presentation e
Get the Shower template where Ribbon is already included. Download the [template archive](https://shwr.me/shower.zip) or install the package: Get the Shower template where Ribbon is already included. Download the [template archive](https://shwr.me/shower.zip) or install the package:
npm install shower npm install @shower/shower
If you want to install Ribbon separately you can install the package: If you want to install Ribbon separately you can install the package:
......
...@@ -14,4 +14,5 @@ ...@@ -14,4 +14,5 @@
width: var(--slide-width); width: var(--slide-width);
height: var(--slide-height); height: var(--slide-height);
background-color: black; background-color: black;
transform: scale(var(--shower-full-scale));
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment