p-slides
Version:
Presentations made simple with Web Components
200 lines (179 loc) • 5.72 kB
JavaScript
/// <reference lib="es2023.array" />
import {
collectNotes,
fireEvent,
FRAGMENTS,
getSequencedFragments,
INITIALLY_VISIBLE,
isFragmentActivated,
setCurrentFragments,
setFragmentActivation,
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', 'p-group']
};
/**
* 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(FRAGMENTS).forEach(fragment => {
if (fragment.ariaHidden === null) {
fragment.ariaHidden = String(!fragment.hasAttribute(INITIALLY_VISIBLE));
} else {
fragment.toggleAttribute(INITIALLY_VISIBLE, fragment.ariaHidden === 'false');
}
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.
* @type {Element[]}
*/
get fragments() {
return this.querySelectorAll(FRAGMENTS);
}
#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 activated when advancing the presentation, if any.
*/
get nextInactiveFragments() {
return this.fragmentSequence.find(fragments => !fragments.every(isFragmentActivated));
}
/**
* The last group of fragments that has been activated when advancing the presentation, if any.
*/
get lastActivatedFragments() {
return this.fragmentSequence.findLast(fragments => fragments.every(isFragmentActivated));
}
#notes;
/**
* The list of the speaker notes as they appear in the slide's fragment sequence.
* @type {Array<Element | Comment>}
*/
get notes() {
return this.#notes ?? (this.#notes = collectNotes(this));
}
/**
* 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 inactiveFragments = this.nextInactiveFragments;
if (inactiveFragments) {
setFragmentActivation(true)(...inactiveFragments);
setCurrentFragments(this);
fireEvent(this, 'fragmenttoggle', {
fragments: inactiveFragments,
areActivated: 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 activatedFragments = this.lastActivatedFragments;
if (activatedFragments) {
setFragmentActivation(false)(...activatedFragments);
setCurrentFragments(this);
fireEvent(this, 'fragmenttoggle', {
fragments: activatedFragments,
areActivated: false
});
this.deck?.broadcastState();
return false;
}
this.isPrevious = false;
this.isActive = false;
return true;
}
}