UNPKG

@webwriter/slides

Version:

WIP - Present content as a sequence of screens (slides, tabs, etc.).

183 lines (152 loc) 5.47 kB
/** config */ import {html, css, PropertyValueMap} from "lit" import {LitElementWw} from "@webwriter/lit" import {customElement, property, queryAll, queryAssignedElements} from "lit/decorators.js" import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js" import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js" import "@shoelace-style/shoelace/dist/themes/light.css" import fullscreenIcon from "bootstrap-icons/icons/fullscreen.svg" import fullscreenExitIcon from "bootstrap-icons/icons/fullscreen-exit.svg" import plusSquareIcon from "bootstrap-icons/icons/plus-square.svg" import minusSquareIcon from "bootstrap-icons/icons/dash-square.svg" import chevronLeftIcon from "bootstrap-icons/icons/chevron-left.svg" import chevronRightIcon from "bootstrap-icons/icons/chevron-right.svg" import { WebwriterSlide } from "./webwriter-slide" @customElement("webwriter-slides") export class WebwriterSlides extends LitElementWw { constructor() { super() this.addEventListener("fullscreenchange", () => this.requestUpdate()) document.addEventListener("selectionchange", e => { const selectedSlideIndex = this.slides?.findIndex(slide => document.getSelection().containsNode(slide, true)) if(selectedSlideIndex !== -1) { this.activeSlideIndex = selectedSlideIndex this.requestUpdate() } }, {passive: true}) } protected firstUpdated(): void { this.requestUpdate() } static scopedElements = { "sl-button": SlButton, "sl-icon-button": SlIconButton } @property({attribute: false, state: true}) accessor activeSlideIndex = 0 get activeSlide() { return this.slides[this.activeSlideIndex] } static styles = css` :host { position: relative; background: white; display: block; } :host(:not(:fullscreen)) { border: 1px solid darkgray; aspect-ratio: 16/9; width: 100%; } :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only { display: none; } [part=actions] { position: absolute; right: 8px; bottom: 8px; display: flex; flex-direction: row; gap: 4px; } .slides-index { user-select: none; font-size: 1rem; color: var(--sl-color-gray-800); display: flex; flex-direction: row; align-items: center; gap: 0.5ch; } :host(:fullscreen) [part=actions] sl-icon-button { color: black; } ::slotted(webwriter-slide:not([active])) { display: none !important; } slot:not([name]) { display: block; width: 100%; height: 100%; &::slotted { width: 100%; height: 100%; } } ` get isFullscreen() { return this.ownerDocument.fullscreenElement === this } get iconSrc() { return this.isFullscreen? fullscreenExitIcon: fullscreenIcon } @queryAssignedElements() accessor slides: WebwriterSlide[] addSlide() { const slide = this.ownerDocument.createElement("webwriter-slide") as WebwriterSlide const p = this.ownerDocument.createElement("p") slide.appendChild(p) this.appendChild(slide) this.activeSlideIndex = this.slides.indexOf(slide) document.getSelection().setBaseAndExtent(p, 0, p, 0) } removeSlide() { this.slides[this.activeSlideIndex].remove() this.nextSlide() } nextSlide(backwards=false, step=1) { const i = this.activeSlideIndex const n = this.slides?.length - 1 this.activeSlideIndex = backwards ? Math.max(0, i - step) : Math.min(n, i + step) } updated(changed: any) { super.updated(changed) this.slides?.forEach((slide, i) => slide.active = i === this.activeSlideIndex) } get hasNextSlide() { return this.activeSlideIndex < this.slides?.length - 1 } get hasPreviousSlide() { return this.activeSlideIndex > 0 } handleNextSlideClick(e: MouseEvent, backwards=false) { if(e.shiftKey) { this.nextSlide(backwards, this.slides.length) } else if(e.ctrlKey) { this.nextSlide(backwards, 10) } else { this.nextSlide(backwards) } } render() { return html` <slot></slot> <aside part="options"> </aside> <aside part="actions"> <sl-icon-button class="author-only" ?disabled=${this.slides.length <= 1} @click=${() => this.removeSlide()} src=${minusSquareIcon}></sl-icon-button> <sl-icon-button class="author-only" @click=${() => this.addSlide()} src=${plusSquareIcon}></sl-icon-button> <sl-icon-button @click=${(e: MouseEvent) => this.handleNextSlideClick(e, true)} src=${chevronLeftIcon} ?disabled=${!this.hasPreviousSlide}></sl-icon-button> <div class="slides-index"> <span>${this.activeSlideIndex + 1}</span> / <span>${this.slides?.length}</span> </div> <sl-icon-button @click=${(e: MouseEvent) => this.handleNextSlideClick(e)} src=${chevronRightIcon} ?disabled=${!this.hasNextSlide}></sl-icon-button> <sl-icon-button id="fullscreen" src=${this.iconSrc} @click=${() => !this.isFullscreen? this.requestFullscreen(): this.ownerDocument.exitFullscreen()}></sl-icon-button> </aside> ` } }