UNPKG

p-slides

Version:

Presentations made simple with Web Components

202 lines (181 loc) 5.77 kB
/// <reference lib="es2023.array" /> import { fireEvent, getNotes, getSequencedFragments, isFragmentVisible, setCurrentFragments, setFragmentVisibility, whenAllDefined } from '../utils.js'; let allDefined = false; whenAllDefined().then(() => (allDefined = true)); /** @typedef {import('../declarations.js').PresentationFragmentToggleEvent} PresentationFragmentToggleEvent */ /** @type {MutationObserverInit} */ const mutationOptions = { subtree: true, childList: true, attributes: true, attributeFilter: ['p-fragment', 'index'] }; /** * The class corresponding to the `<p-slide>` element. * @tag p-slide * @attribute {string} aria-current - When set to `'page'`, the slide is the current one in the presentation. It's discouraged to set it manually * @attribute {string} effect - Effect name for entering the slide from, or exiting to, the previous slide * @fires {PresentationFragmentToggleEvent} p-slides.fragmenttoggle - When a fragment has been shown or hidden */ export class PresentationSlideElement extends HTMLElement { /** @internal */ static get observedAttributes() { return ['aria-current']; } #mutations = new MutationObserver(() => { this.#fragmentSequence = null; this.#notes = null; }); /** @internal */ attributeChangedCallback(attribute, _, value) { if (attribute === 'aria-current') { const isActive = value === 'page'; this.ariaHidden = `${!isActive}`; if (isActive) { setCurrentFragments(this); if (this.deck) this.deck.currentSlide = this; } } } /** @internal */ connectedCallback() { this.ariaHidden = `${!this.isActive}`; this.querySelectorAll('p-fragment, [p-fragment]').forEach(fragment => { fragment.ariaHidden ??= 'true'; fragment.ariaCurrent ??= 'false'; }); this.#mutations.observe(this, mutationOptions); } /** @internal */ disconnectedCallback() { this.#mutations.disconnect(); } /** * The parent presentation deck. */ get deck() { return allDefined ? this.closest('p-deck') : null; } /** * Whether the slide is the current one in the presentation. This will set the `aria-current` attribute to either * `'page'` or `'false'`. * * It's discouraged to set it manually. */ get isActive() { return this.ariaCurrent === 'page'; } set isActive(isActive) { this.ariaCurrent = isActive ? 'page' : 'false'; if (isActive) setCurrentFragments(this); } /** * Whether the slide is past the current one in the presentation. This will set a `previous` attribute on the * `<p-slide>` element, that can be used for styling purposes. A slide can be the current one _and_ marked as * "previous" when going backward in the presentation. * * It's discouraged to set it manually. */ get isPrevious() { return this.hasAttribute('previous'); } set isPrevious(isPrevious) { this.toggleAttribute('previous', isPrevious); } /** * The list of the fragment elements as they appear in the slide's markup. */ get fragments() { return this.querySelectorAll('p-fragment, [p-fragment]'); } #fragmentSequence; /** * The fragments grouped using their indexes. * @type {Element[][]} */ get fragmentSequence() { if (!this.#fragmentSequence) { this.#fragmentSequence = getSequencedFragments(this.fragments); } return this.#fragmentSequence; } /** * The next group of fragments that will be shown when advancing the presentation, if any. */ get nextHiddenFragments() { return this.fragmentSequence.find(fragments => !fragments.every(isFragmentVisible)); } /** * The last group of fragments that has been shown when advancing the presentation, if any. */ get lastVisibleFragments() { return this.fragmentSequence.findLast(fragments => fragments.every(isFragmentVisible)); } #notes; /** * The list of the speaker notes as they appear in the slide's fragment sequence. * @type {Array<Element | Comment>} */ get notes() { if (!this.#notes) { const notes = getNotes(this); const { fragmentSequence } = this; const noteFragments = new Map( notes.map(note => [note, fragmentSequence.findIndex(frags => frags.some(fragment => fragment.contains(note)))]) ); notes.sort((a, b) => noteFragments.get(a) - noteFragments.get(b)); this.#notes = notes; } return this.#notes; } /** * Attempts to advance the presentation by showing a new block of fragments on the current slide. It returns `true` if * no fragments are left to show in the current slide (the deck will advance to the next slide). * @fires {PresentationFragmentToggleEvent} p-slides.fragmenttoggle - If a set of fragments has been toggled */ next() { const hiddenFragments = this.nextHiddenFragments; if (hiddenFragments) { setFragmentVisibility(true)(...hiddenFragments); setCurrentFragments(this); fireEvent(this, 'fragmenttoggle', { fragments: hiddenFragments, areVisible: true }); this.deck?.broadcastState(); return false; } this.isPrevious = true; this.isActive = false; return true; } /** * Attempts to bring the presentation back by hiding the last shown block of fragments on the current slide. It * returns `true` if no fragments are left to hide in the current slide (the deck will go back to the previous slide). * @fires {PresentationFragmentToggleEvent} p-slides.fragmenttoggle - If a set of fragments has been toggled */ previous() { const visibleFragments = this.lastVisibleFragments; if (visibleFragments) { setFragmentVisibility(false)(...visibleFragments); setCurrentFragments(this); fireEvent(this, 'fragmenttoggle', { fragments: visibleFragments, areVisible: false }); this.deck?.broadcastState(); return false; } this.isPrevious = false; this.isActive = false; return true; } }