@oddbird/slide-deck
Advanced tools
Comparing version 0.1.1 to 0.1.2
@@ -7,2 +7,42 @@ # Changes | ||
## v0.1.2 - 2024-01-16 | ||
- 💥 BREAKING: Disabled the full-screen keyboard shortcut, | ||
until we have a chance to address the various | ||
fullscreen browser issues | ||
- 💥 BREAKING: Removed the 'end presentation' event | ||
and keyboard shortcuts, which were more confusing than useful | ||
- 💥 BREAKING: Removed the shadow DOM content wrapper, | ||
and all shadow DOM styles for slide layout | ||
- 💥 BREAKING: The `reset` event targets the first slide | ||
rather than the slide-deck container | ||
- 🚀 NEW / 💥 BREAKING: Renamed and added control-panel parts, | ||
to allow for more customization of the default panel | ||
including pressed buttons with `:part(button pressed)` | ||
- 🚀 NEW: Set `aria-current='true'` on active slide | ||
- 🚀 NEW: View settings are maintained across page refresh | ||
using `sessionStorage` | ||
- 🚀 NEW: Add support for slide parts – `slide-canvas` & `slide-note` | ||
- 🚀 NEW: Each slide is labeled with either | ||
`slide-item='container'` (if it has nested parts) | ||
or `slide-item='canvas' slide-canvas` (if there are no nested parts) | ||
- 🚀 NEW: The slide-deck has a `--slide-count` property, | ||
and each slide has a `--slide-index` | ||
- 🚀 NEW: Default styles are in `slide-deck.css` | ||
and can be applied from the light DOM | ||
- 🚀 NEW: The entire control panel can be replaced | ||
from the light DOM using `slot=control-panel` | ||
on a slotted `dialog` element | ||
- 🚀 NEW: Blank slides are implemented as shadow DOM dialogues, | ||
which can be replaced from the light DOM using `slot=blank-slide` | ||
on a slotted `dialog` element | ||
- 🚀 NEW: When `key-control` is activated (including on-load), | ||
we target the stored active slide (or the first slide) | ||
- 🚀 NEW: Support for keyboard shortcuts on Windows/Linux | ||
using `control` instead of `command` | ||
- 🐞 FIXED: Slotted controls are no longer treated as slides | ||
- 🐞 FIXED: When restoring the active slide from memory, | ||
we go to the first slide if there's no stored state | ||
- 🐞 FIXED: Use any key to exit a blank-slide mode | ||
## v0.1.1 - 2023-12-26 | ||
@@ -9,0 +49,0 @@ |
{ | ||
"name": "@oddbird/slide-deck", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "A Web Component for web presentations", | ||
@@ -5,0 +5,0 @@ "main": "slide-deck.js", |
@@ -82,4 +82,5 @@ # `slide-deck` | ||
- `command-.`: End presentation | ||
- `command-shift-f`: Toggle full-screen mode | ||
*Windows and Linux users can use Ctrl instead of Command.* | ||
When presenting (key-control is active): | ||
@@ -112,2 +113,3 @@ | ||
<!-- Host yourself --> | ||
<link rel="stylesheet" href="slide-deck.css"> | ||
<script type="module" src="slide-deck.js"></script> | ||
@@ -118,5 +120,6 @@ ``` | ||
<!-- 3rd party CDN, not recommended for production use --> | ||
<link rel="stylesheet" href="https://www.unpkg.com/@oddbird/slide-deck/slide-deck.css"> | ||
<script | ||
type="module" | ||
src="https://www.unpkg.com/@oddbird/slide-deck@0.1.0/slide-deck.js" | ||
src="https://www.unpkg.com/@oddbird/slide-deck/slide-deck.js" | ||
></script> | ||
@@ -127,5 +130,6 @@ ``` | ||
<!-- 3rd party CDN, not recommended for production use --> | ||
<link rel="stylesheet" href="https://esm.sh/@oddbird/slide-deck/slide-deck.css"> | ||
<script | ||
type="module" | ||
src="https://esm.sh/@oddbird/slide-deck@0.1.0" | ||
src="https://esm.sh/@oddbird/slide-deck" | ||
></script> | ||
@@ -132,0 +136,0 @@ ``` |
class slideDeck extends HTMLElement { | ||
// template | ||
static appendShadowTemplate = (node) => { | ||
static #appendShadowTemplate = (node) => { | ||
const template = document.createElement("template"); | ||
template.innerHTML = ` | ||
<dialog part="controls"> | ||
<form method="dialog"><button>close</button></form> | ||
<div> | ||
<slot name="slide-controls"> | ||
<button slide-event='toggleControl'>keyboard controls</button> | ||
<slot></slot> | ||
<slot name="control-panel"> | ||
<dialog part="control-panel"> | ||
<div part="panel-header"> | ||
<strong>Slide-Deck Controls</strong> | ||
<form method="dialog"> | ||
<button part="button close-controls">close</button> | ||
</form> | ||
</div> | ||
<div part="controls"> | ||
<button part="button" slide-event='toggleControl'> | ||
keyboard navigation | ||
</button> | ||
<hr> | ||
<p><strong>Presentation:</strong></p> | ||
<button slide-event>start</button> | ||
<button slide-event>end</button> | ||
<button slide-event="joinWithNotes">speaker view</button> | ||
<button part="button event" slide-event> | ||
start | ||
</button> | ||
<button part="button event" slide-event> | ||
reset | ||
</button> | ||
<button part="button event" slide-event="joinWithNotes"> | ||
speaker view | ||
</button> | ||
<hr> | ||
<p><strong>View:</strong></p> | ||
<button set-view>grid</button> | ||
<button set-view>list</button> | ||
</slot> | ||
</div> | ||
</dialog> | ||
<div part="contents"> | ||
<slot></slot> | ||
</div> | ||
<button part="button view" set-view> | ||
grid | ||
</button> | ||
<button part="button view" set-view> | ||
solo | ||
</button> | ||
<button part="button view" set-view> | ||
script | ||
</button> | ||
</div> | ||
</dialog> | ||
</slot> | ||
<slot name="blank-slide"> | ||
<dialog part="blank-slide"> | ||
<form method="dialog"> | ||
<button part="close-blank-slide"> | ||
<span aria-label="close">x<span> | ||
</button> | ||
</form> | ||
</dialog> | ||
</slot> | ||
`; | ||
@@ -32,88 +63,34 @@ const shadowRoot = node.attachShadow({ mode: "open" }); | ||
// css | ||
static adoptShadowStyles = (node) => { | ||
static #adoptShadowStyles = (node) => { | ||
const shadowStyle = new CSSStyleSheet(); | ||
shadowStyle.replaceSync(` | ||
:host { | ||
position: relative; | ||
[part=blank-slide], | ||
::slotted([slot=blank-slide]) { | ||
block-size: 100%; | ||
border: 0; | ||
color-scheme: var(--sd-blank-slide-scheme, dark); | ||
inline-size: 100%; | ||
max-block-size: unset; | ||
max-inline-size: unset; | ||
} | ||
:host:not(:fullscreen) { | ||
container: host / inline-size; | ||
} | ||
[part=blank-slide] { | ||
padding: 0; | ||
:host(:fullscreen) { | ||
background-color: white; | ||
overflow-x: clip; | ||
overflow-y: auto; | ||
&[open] { display: grid; } | ||
form { display: grid; } | ||
} | ||
:host([slide-view=grid]) { | ||
---slide-grid-ratio: 16/9; | ||
---slide-grid-border: var(--slide-grid-border, thin solid); | ||
---slide-grid-active-outline: medium dotted hotpink; | ||
---slide-grid-scroll-margin: clamp(10px, 4cqi, 40px); | ||
---slide-list-border: var(--slide-grid-border, thin solid); | ||
:host([blank-slide=white]) { | ||
--sd-blank-slide-scheme: light; | ||
} | ||
:host([slide-view=list]) { | ||
---slide-list-border: var(--slide-list-border, thin solid); | ||
} | ||
:host([blank-slide])::after { | ||
content: ''; | ||
position: absolute; | ||
inset: 0; | ||
background-color: var(--blank-slide-color, black); | ||
} | ||
:host([blank-slide='white'])::after { | ||
--blank-slide-color: white; | ||
} | ||
[part=contents] { | ||
---slide-gap: clamp(5px, 1.5cqi, 15px); | ||
[part=close-blank-slide] { | ||
background: transparent; | ||
border: 0; | ||
color: inherit; | ||
display: grid; | ||
:host([slide-view=grid]) & { | ||
grid-template-columns: var( | ||
--slide-grid-columns, | ||
repeat(auto-fit, minmax(min(50ch, 100%), 1fr)) | ||
); | ||
gap: var(--slide-grid-gap, var(---slide-gap)); | ||
padding: var(--slide-grid-padding, var(---slide-gap)); | ||
} | ||
:host([slide-view=list]) & { | ||
grid-auto-rows: var(--slide-list-rows, 100svh); | ||
} | ||
font: inherit; | ||
place-items: start end; | ||
} | ||
::slotted([id^=slide_]) { | ||
aspect-ratio: var(--slide-grid-ratio, var(---slide-grid-ratio)); | ||
container-name: slide; | ||
container-type: var(--slide-container, inline-size); | ||
border: var(---slide-grid-border); | ||
border-block-end: var(---slide-list-border); | ||
padding: var(---slide-gap); | ||
scroll-margin: var( | ||
--slide-grid-scroll-margin, | ||
var(---slide-grid-scroll-margin) | ||
); | ||
} | ||
::slotted([id^=slide_]:target) { | ||
outline: var( | ||
--slide-grid-active-outline, | ||
var(---slide-grid-active-outline) | ||
); | ||
outline-offset: var(--slide-active-outline-offset, 3px); | ||
} | ||
button[aria-pressed=true] { | ||
box-shadow: inset 0 0 2px black; | ||
&::before { | ||
content: ' ✅ '; | ||
} | ||
} | ||
`); | ||
@@ -131,8 +108,14 @@ node.shadowRoot.adoptedStyleSheets = [shadowStyle]; | ||
static attrToPropMap = { | ||
'key-control': 'keyControl', | ||
'follow-active': 'followActive', | ||
'full-screen': 'fullScreen', | ||
'slide-view': 'slideView', | ||
}; | ||
get keyControl(){ | ||
return this.hasAttribute('key-control'); | ||
} | ||
get followActive(){ | ||
return this.hasAttribute('follow-active'); | ||
} | ||
get fullScreen(){ | ||
return this.hasAttribute('full-screen'); | ||
} | ||
get slideView(){ | ||
return this.getAttribute('slide-view'); | ||
} | ||
@@ -148,3 +131,4 @@ static storageKeys = [ | ||
'grid', | ||
'list', | ||
'solo', | ||
'script', | ||
]; | ||
@@ -175,27 +159,28 @@ | ||
',': 'whiteOut', | ||
// end | ||
'-': 'endPresentation' | ||
} | ||
// dynamic | ||
store = {}; | ||
#store = {}; | ||
slides; | ||
slideCount; | ||
controlPanel; | ||
eventButtons; | ||
viewButtons; | ||
activeSlide; | ||
body; | ||
#controlPanel; | ||
#blankSlide; | ||
#eventButtons; | ||
#viewButtons; | ||
#body; | ||
// callbacks | ||
attributeChangedCallback(name, oldValue, newValue) { | ||
this[slideDeck.attrToPropMap[name]] = newValue || this.hasAttribute(name); | ||
attributeChangedCallback(name) { | ||
switch (name) { | ||
case 'key-control': | ||
this.#keyControlChange(); | ||
break; | ||
case 'follow-active': | ||
this.followActiveChange(); | ||
this.#followActiveChange(); | ||
this.#updateEventButtons(); | ||
break; | ||
case 'slide-view': | ||
this.updateViewButtons(); | ||
this.scrollToActive(); | ||
this.#onViewChange(); | ||
break; | ||
@@ -206,3 +191,3 @@ default: | ||
this.updateEventButtons(); | ||
this.#updateEventButtons(); | ||
} | ||
@@ -214,30 +199,39 @@ | ||
// shadow dom and ID | ||
slideDeck.appendShadowTemplate(this); | ||
slideDeck.adoptShadowStyles(this); | ||
this.setDeckID(); | ||
slideDeck.#appendShadowTemplate(this); | ||
slideDeck.#adoptShadowStyles(this); | ||
this.#setDeckID(); | ||
// relevant nodes | ||
this.body = document.querySelector('body'); | ||
this.controlPanel = this.shadowRoot.querySelector(`[part="controls"]`); | ||
this.#body = document.querySelector('body'); | ||
this.#controlPanel = this.querySelector(`[slot="control-panel"]`) ?? | ||
this.shadowRoot.querySelector(`[part="control-panel"]`); | ||
this.#blankSlide = this.querySelector(`[slot="blank-slide"]`) ?? | ||
this.shadowRoot.querySelector(`[part="blank-slide"]`); | ||
// initial setup | ||
this.slideCount = this.childElementCount; | ||
this.defaultAttrs(); | ||
this.setSlideIDs(); | ||
this.#defaultAttrs(); | ||
this.#setupSlides(); | ||
this.goTo(); | ||
// buttons | ||
this.setupEventButtons(); | ||
this.setupViewButtons(); | ||
this.#setupEventButtons(); | ||
this.#setupViewButtons(); | ||
// event listeners | ||
// shadow DOM event listeners | ||
this.shadowRoot.addEventListener('keydown', (event) => { | ||
event.stopPropagation(); | ||
if ((event.key === 'k' && event.metaKey) || event.key === 'Escape') { | ||
if (this.hasAttribute('blank-slide')) { | ||
event.preventDefault(); | ||
this.controlPanel.close(); | ||
this.blankSlideEvent(); | ||
} else if ((event.key === 'k' && this.#cmdOrCtrl(event)) || event.key === 'Escape') { | ||
event.preventDefault(); | ||
this.#controlPanel.close(); | ||
} | ||
}); | ||
this.#blankSlide.addEventListener('close', (event) => { | ||
this.removeAttribute('blank-slide'); | ||
}) | ||
// custom events | ||
@@ -247,5 +241,2 @@ this.addEventListener('toggleControl', (e) => this.toggleAttribute('key-control')); | ||
this.addEventListener('toggleFullscreen', (e) => this.fullScreenEvent()); | ||
this.addEventListener('toggleView', (e) => this.toggleView()); | ||
this.addEventListener('grid', (e) => this.toggleView('grid')); | ||
this.addEventListener('list', (e) => this.toggleView('list')); | ||
@@ -256,21 +247,20 @@ this.addEventListener('join', (e) => this.joinEvent()); | ||
this.addEventListener('resume', (e) => this.resumeEvent()); | ||
this.addEventListener('end', (e) => this.endEvent()); | ||
this.addEventListener('reset', (e) => this.resetEvent()); | ||
this.addEventListener('blankSlide', (e) => this.blankSlideEvent()); | ||
this.addEventListener('nextSlide', (e) => this.move(1)); | ||
this.addEventListener('next', (e) => this.move(1)); | ||
this.addEventListener('savedSlide', (e) => this.goToSaved()); | ||
this.addEventListener('previousSlide', (e) => this.move(-1)); | ||
this.addEventListener('previous', (e) => this.move(-1)); | ||
}; | ||
connectedCallback() { | ||
this.body.addEventListener('keydown', this.keyEventActions); | ||
this.#body.addEventListener('keydown', this.#keyEventActions); | ||
} | ||
disconnectedCallback() { | ||
this.body.removeEventListener('keydown', this.keyEventActions); | ||
this.#body.removeEventListener('keydown', this.#keyEventActions); | ||
} | ||
// setup methods | ||
newDeckId = (from, count) => { | ||
#newDeckId = (from, count) => { | ||
const base = from || window.location.pathname.split('.')[0]; | ||
@@ -280,3 +270,3 @@ const ID = count ? `${base}-${count}` : base; | ||
if (document.getElementById(ID)) { | ||
return this.newDeckId(base, (count || 0) + 1); | ||
return this.#newDeckId(base, (count || 0) + 1); | ||
} | ||
@@ -287,25 +277,46 @@ | ||
setDeckID = () => { | ||
this.id = this.id || this.newDeckId(); | ||
#setDeckID = () => { | ||
this.id = this.id || this.#newDeckId(); | ||
// storage keys based on slide ID | ||
slideDeck.storageKeys.forEach((key) => { | ||
this.store[key] = `${this.id}.${key}`; | ||
this.#store[key] = `${this.id}.${key}`; | ||
}); | ||
} | ||
slideId = (n) => `slide_${this.id}-${n}`; | ||
#slideId = (n) => `slide_${this.id}-${n}`; | ||
setSlideIDs = () => { | ||
const slides = this.querySelectorAll(':scope > *'); | ||
#setupSlides = () => { | ||
this.slides = this.querySelectorAll(':scope > :not([slot])'); | ||
this.slideCount = this.slides.length; | ||
this.style.setProperty('--slide-count', this.slideCount); | ||
slides.forEach((slide, index) => { | ||
slide.id = this.slideId(index + 1); | ||
this.slides.forEach((slide, index) => { | ||
const slideIndex = index + 1; | ||
slide.id = this.#slideId(slideIndex); | ||
slide.style.setProperty('--slide-index', slideIndex); | ||
if (slide.querySelector(':scope [slide-canvas]')) { | ||
if (!slide.hasAttribute('slide-item')) { | ||
slide.setAttribute('slide-item', 'container'); | ||
} | ||
} else { | ||
if (!slide.hasAttribute('slide-item')) { | ||
slide.setAttribute('slide-item', 'canvas'); | ||
} | ||
if (!slide.hasAttribute('slide-canvas')) { | ||
slide.toggleAttribute('slide-canvas', true); | ||
} | ||
} | ||
}); | ||
}; | ||
defaultAttrs = () => { | ||
#defaultAttrs = () => { | ||
// view required | ||
if (!this.hasAttribute('slide-view')) { | ||
this.setAttribute('slide-view', 'grid'); | ||
if (!this.slideView) { | ||
this.setAttribute( | ||
'slide-view', | ||
sessionStorage.getItem(this.#store.view) || 'grid' | ||
); | ||
} | ||
@@ -318,57 +329,84 @@ | ||
// buttons | ||
getButtonEvent = (btn) => btn.getAttribute('slide-event') || btn.innerText; | ||
#findButtons = (attr) => [ | ||
...this.querySelectorAll(`:scope button[${attr}]`), | ||
...this.shadowRoot.querySelectorAll(`button[${attr}]`), | ||
]; | ||
updateEventButtons = () => { | ||
this.eventButtons.forEach((btn) => { | ||
const btnEvent = this.getButtonEvent(btn); | ||
let isActive = { | ||
'toggleControl': this.keyControl, | ||
'toggleFollow': this.followActive, | ||
'toggleFullscreen': this.fullScreen, | ||
#getButtonValue = (btn, attr) => btn.getAttribute(attr) || btn.innerText; | ||
#setButtonPressed = (btn, isPressed) => { | ||
btn.setAttribute('aria-pressed', isPressed); | ||
if (btn.hasAttribute('part')) { | ||
const currentNames = btn.getAttribute('part').split(' '); | ||
let newNames; | ||
if (isPressed) { | ||
newNames = currentNames.includes('pressed') | ||
? currentNames | ||
: [...currentNames, 'pressed']; | ||
} else if (!isPressed) { | ||
newNames = currentNames.filter((name) => name !== 'pressed') | ||
} | ||
if (Object.keys(isActive).includes(btnEvent)) { | ||
btn.setAttribute('aria-pressed', isActive[btnEvent]); | ||
} | ||
}); | ||
btn.setAttribute('part', newNames.join(' ')); | ||
} | ||
} | ||
setupEventButtons = () => { | ||
this.eventButtons = [ | ||
...this.querySelectorAll(`button[slide-event]`), | ||
...this.shadowRoot.querySelectorAll(`button[slide-event]`), | ||
]; | ||
#setToggleState = (btn, attr, state) => { | ||
const isActive = this.#getButtonValue(btn, attr) === state; | ||
this.#setButtonPressed(btn, isActive); | ||
} | ||
this.eventButtons.forEach((btn) => { | ||
#setupViewButtons = () => { | ||
this.#viewButtons = this.#findButtons('set-view'); | ||
this.#viewButtons.forEach((btn) => { | ||
btn.addEventListener('click', (e) => { | ||
const event = this.getButtonEvent(btn); | ||
this.dispatchEvent(new Event(event, { view: window, bubbles: false })); | ||
this.setAttribute('slide-view', this.#getButtonValue(btn, 'set-view')); | ||
}); | ||
this.#setToggleState(btn, 'set-view', this.slideView); | ||
}); | ||
} | ||
this.updateEventButtons(); | ||
#updateViewButtons = () => { | ||
this.#viewButtons.forEach((btn) => { | ||
this.#setToggleState(btn, 'set-view', this.slideView); | ||
}); | ||
} | ||
getButtonView = (btn) => btn.getAttribute('set-view') || btn.innerText; | ||
// attribute changes | ||
#onViewChange = () => { | ||
this.#updateViewButtons(); | ||
this.scrollToActive(); | ||
sessionStorage.setItem(this.#store.view, this.slideView); | ||
} | ||
setupViewButtons = () => { | ||
this.viewButtons = [ | ||
...this.querySelectorAll(`button[set-view]`), | ||
...this.shadowRoot.querySelectorAll(`button[set-view]`), | ||
]; | ||
// event buttons | ||
#setupEventButtons = () => { | ||
this.#eventButtons = this.#findButtons('slide-event'); | ||
this.viewButtons.forEach((btn) => { | ||
this.#eventButtons.forEach((btn) => { | ||
btn.addEventListener('click', (e) => { | ||
this.setAttribute('slide-view', this.getButtonView(btn)); | ||
const event = this.#getButtonValue(btn, 'slide-event'); | ||
this.dispatchEvent(new Event(event, { view: window, bubbles: false })); | ||
}); | ||
}); | ||
this.slideView = this.slideView || this.getAttribute('slide-view'); | ||
this.updateViewButtons(); | ||
this.#updateEventButtons(); | ||
} | ||
updateViewButtons = () => { | ||
this.viewButtons.forEach((btn) => { | ||
const isActive = this.getButtonView(btn) === this.slideView; | ||
btn.setAttribute('aria-pressed', isActive); | ||
#updateEventButtons = () => { | ||
this.#eventButtons.forEach((btn) => { | ||
const btnEvent = this.#getButtonValue(btn, 'slide-event'); | ||
let isActive = { | ||
'toggleControl': this.keyControl, | ||
'toggleFollow': this.followActive, | ||
'toggleFullscreen': this.fullScreen, | ||
} | ||
if (Object.keys(isActive).includes(btnEvent)) { | ||
this.#setButtonPressed(btn, isActive[btnEvent]); | ||
} | ||
}); | ||
@@ -378,11 +416,6 @@ } | ||
// event handlers | ||
toggleView = (to) => { | ||
if (!to) { | ||
const current = this.getAttribute('slide-view'); | ||
const l = slideDeck.slideViews - 1; // adjust for 0-index | ||
const i = slideDeck.slideViews.indexOf(current); | ||
const next = slideDeck.slideViews[(i + 1) % l]; | ||
} | ||
this.setAttribute('slide-view', to || next || 'grid'); | ||
#startPresenting = () => { | ||
this.setAttribute('slide-view', 'solo'); | ||
this.setAttribute('key-control', ''); | ||
this.setAttribute('follow-active', ''); | ||
} | ||
@@ -392,3 +425,3 @@ | ||
this.goTo(1); | ||
this.startPresenting(); | ||
this.#startPresenting(); | ||
} | ||
@@ -398,13 +431,7 @@ | ||
this.goToSaved(); | ||
this.startPresenting(); | ||
this.#startPresenting(); | ||
} | ||
startPresenting = () => { | ||
this.setAttribute('slide-view', 'list'); | ||
this.setAttribute('key-control', ''); | ||
this.setAttribute('follow-active', ''); | ||
} | ||
joinWithNotesEvent = () => { | ||
this.setAttribute('slide-view', 'grid'); | ||
this.setAttribute('slide-view', 'script'); | ||
this.setAttribute('key-control', ''); | ||
@@ -419,18 +446,12 @@ this.setAttribute('follow-active', ''); | ||
endEvent = () => { | ||
this.setAttribute('slide-view', 'grid'); | ||
this.removeAttribute('key-control'); | ||
this.removeAttribute('follow-active'); | ||
this.resetEvent(); | ||
} | ||
resetEvent = () => { | ||
window.location.hash = this.id; | ||
this.resetActive(); | ||
this.goTo(1); | ||
} | ||
blankSlideEvent = (color) => { | ||
if (this.hasAttribute('blank-slide')) { | ||
if (this.#blankSlide.open) { | ||
this.#blankSlide.close(); | ||
this.removeAttribute('blank-slide'); | ||
} else { | ||
this.#blankSlide.showModal(); | ||
this.setAttribute('blank-slide', color || 'black'); | ||
@@ -451,3 +472,9 @@ } | ||
// dynamic attribute methods | ||
followActiveChange = () => { | ||
#keyControlChange = () => { | ||
if (this.keyControl) { | ||
this.goToSaved(); | ||
} | ||
} | ||
#followActiveChange = () => { | ||
if (this.followActive) { | ||
@@ -462,21 +489,23 @@ this.goToSaved(); | ||
// storage | ||
asSlideInt = (string) => parseInt(string, 10); | ||
#asSlideInt = (string) => parseInt(string, 10); | ||
#indexFromId = (string) => this.#asSlideInt(string.split('-').pop()); | ||
slideFromHash = () => window.location.hash.startsWith('#slide_') | ||
? this.asSlideInt(window.location.hash.split('-').pop()) | ||
#slideFromHash = () => window.location.hash.startsWith('#slide_') | ||
? this.#indexFromId(window.location.hash) | ||
: null; | ||
slideFromStore = () => this.asSlideInt( | ||
localStorage.getItem(this.store.slide) | ||
); | ||
slideToHash = (to) => { | ||
#slideFromStore = (fallback = 1) => this.#asSlideInt( | ||
localStorage.getItem(this.#store.slide) | ||
) || fallback; | ||
#slideToHash = (to) => { | ||
if (to) { | ||
window.location.hash = this.slideId(to); | ||
window.location.hash = this.#slideId(to); | ||
} | ||
}; | ||
slideToStore = (to) => { | ||
#slideToStore = (to) => { | ||
if (to) { | ||
localStorage.setItem(this.store.slide, to); | ||
localStorage.setItem(this.#store.slide, to); | ||
} else { | ||
localStorage.removeItem(this.store.slide); | ||
localStorage.removeItem(this.#store.slide); | ||
} | ||
@@ -486,7 +515,7 @@ }; | ||
// navigation | ||
inRange = (slide) => slide >= 1 && slide <= this.slideCount; | ||
getActive = () => this.slideFromHash() || this.activeSlide; | ||
#inRange = (slide) => slide >= 1 && slide <= this.slideCount; | ||
#getActive = () => this.#slideFromHash() || this.activeSlide; | ||
scrollToActive = () => { | ||
const activeEl = document.getElementById(this.slideId(this.activeSlide)); | ||
const activeEl = document.getElementById(this.#slideId(this.activeSlide)); | ||
@@ -499,23 +528,26 @@ if (activeEl) { | ||
goTo = (to) => { | ||
const fromHash = this.slideFromHash(); | ||
const setTo = to || this.getActive(); | ||
const fromHash = this.#slideFromHash(); | ||
const setTo = to || this.#getActive(); | ||
if (setTo && this.inRange(setTo)) { | ||
if (setTo && this.#inRange(setTo)) { | ||
this.activeSlide = setTo; | ||
this.slideToStore(setTo); | ||
this.#slideToStore(setTo); | ||
if (setTo !== fromHash) { | ||
this.slideToHash(setTo); | ||
this.#slideToHash(setTo); | ||
} | ||
// update aria-current | ||
this.querySelectorAll( | ||
':scope [slide-item][aria-current]' | ||
).forEach((slide) => { | ||
slide.removeAttribute('aria-current'); | ||
}); | ||
this.querySelector(`:scope #${this.#slideId(setTo)}`).setAttribute('aria-current', 'true'); | ||
} | ||
} | ||
resetActive = () => { | ||
this.activeSlide = null; | ||
window.location.hash = this.id; | ||
localStorage.removeItem(this.store.slide); | ||
}; | ||
move = (by) => { | ||
const to = (this.getActive() || 0) + by; | ||
const to = (this.#getActive() || 0) + by; | ||
this.goTo(to); | ||
@@ -525,19 +557,23 @@ }; | ||
goToSaved = () => { | ||
this.goTo(this.slideFromStore()); | ||
this.goTo(this.#slideFromStore()); | ||
} | ||
keyEventActions = (event) => { | ||
// Detect Ctrl / Cmd modifiers in a platform-agnostic way | ||
#cmdOrCtrl = (event) => event.ctrlKey || event.metaKey; | ||
#keyEventActions = (event) => { | ||
// exit from blank slide | ||
if (this.hasAttribute('blank-slide')) { | ||
event.preventDefault(); | ||
this.removeAttribute('blank-slide'); | ||
return; | ||
} | ||
// always available | ||
if (event.metaKey) { | ||
if (this.#cmdOrCtrl(event)) { | ||
switch (event.key) { | ||
case 'k': | ||
event.preventDefault(); | ||
this.controlPanel.showModal(); | ||
this.#controlPanel.showModal(); | ||
break; | ||
case 'f': | ||
if (event.shiftKey) { | ||
event.preventDefault(); | ||
this.fullScreenEvent(); | ||
} | ||
break; | ||
case 'Enter': | ||
@@ -552,6 +588,3 @@ if (event.shiftKey) { | ||
break; | ||
case '.': | ||
event.preventDefault(); | ||
this.endEvent(); | ||
break; | ||
default: | ||
@@ -570,7 +603,4 @@ break; | ||
if (event.key === 'Escape') { | ||
if (event.target !== this.body) { | ||
if (event.target !== this.#body) { | ||
event.target.blur(); | ||
} else { | ||
event.preventDefault(); | ||
this.endEvent(); | ||
} | ||
@@ -605,6 +635,2 @@ return; | ||
break; | ||
case 'endPresentation': | ||
event.preventDefault(); | ||
this.endEvent(); | ||
break; | ||
default: | ||
@@ -611,0 +637,0 @@ break; |
Sorry, the diff of this file is not supported yet
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
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
76576
13
622
155
2