@oddbird/slide-deck
Advanced tools
Comparing version 0.1.4 to 0.2.0-rc.1
@@ -7,2 +7,54 @@ # Changes | ||
## v0.2.0 - UNRELEASED (`-rc.1` 2024-11-21) | ||
- 🚀 NEW: The `key-control` attribute | ||
accepts values of `none` or `escape`, | ||
to turn off keyboard navigation | ||
(and optionally leave escape-to-blur intact). | ||
This attribute can be applied to individual elements in a deck, | ||
for more detailed control -- | ||
eg turning off navigation while a form has focus. | ||
- 🚀 NEW: Provide a `slide-deck.webc` component. | ||
- 🚀 NEW: All attributes have associated getters and setters: | ||
- `key-control` -> `keyControl` (boolean | 'none' | 'escape') | ||
- `follow-active` -> `followActive` (boolean) | ||
- `full-screen` -> `fullScreen` (boolean) | ||
- `slide-view` -> `slideView` (string) | ||
- 🚀 NEW: Use the `?slide-view=viewName` query parameter | ||
to create links to specific slide views. | ||
When present on load, the query parameter will override | ||
the session storage as well as any hardcoded attribute value. | ||
- 💥 BREAKING: The `slideView` property setter | ||
should be used for changing views, | ||
rather than manipulating the `slide-view` attribute directly. | ||
This will also update session storage and the url query parameter. | ||
- 💥 BREAKING: When the `start` and `resume` events are fired, | ||
the slide-deck is put into a `publicView` | ||
(the default is `slideshow`). | ||
When the `join-as-speaker` event is fired, | ||
the slide-deck is put into a `privateView` | ||
(the default is `speaker`). | ||
These can be changed by setting the | ||
`publicView` and `privateView` properties with JS, | ||
or by setting the `public-view` and `private-view` attributes in HTML. | ||
- 💥 BREAKING: Renamed the custom event handlers and matching public methods: | ||
- `reset` = `reset()` | ||
- `join` = `join()` | ||
- `resume` = `resume()` | ||
- `start` = `start()` | ||
- `join-as-speaker` = `joinAsSpeaker()` | ||
- `blank-slide` = `blankSlide()` | ||
- `next` = `next()` | ||
- `previous` = `previous()` | ||
- `to-slide` = `toSlide()` | ||
- `to-saved` = `toSavedSlide()` | ||
- `scroll-to-active` = `scrollToActive()` | ||
- `full-screen` = `toggleFullScreen()` | ||
- `key-control` = `toggleKeyControl()` | ||
- `follow-active` = `toggleFollowActive()` | ||
- 🐞 FIXED: Keyboard events are given proper priority, so that | ||
(for example) you can open the control panel from a blank slide | ||
- 👀 INTERNAL: Renamed static `storageKeys` to `storeValues`, | ||
and static `controlKeys` to `navKeys` for clarity. | ||
## v0.1.4 - 2024-02-28 | ||
@@ -9,0 +61,0 @@ |
@@ -21,7 +21,17 @@ # Contributing to Slide-Deck | ||
# command line | ||
npm start | ||
npm run serve | ||
``` | ||
Then go to | ||
http://localhost:8080/ | ||
http://localhost:6000/ | ||
in a web browser. | ||
If | ||
[localias](https://github.com/peterldowns/localias?tab=readme-ov-file#-localias) | ||
is configured and running, add an alias for this project: | ||
``` | ||
localias set slide-deck.local 6000 | ||
``` | ||
This will allow you to visit the project at <https://slide-deck.local>. |
{ | ||
"name": "@oddbird/slide-deck", | ||
"version": "0.1.4", | ||
"version": "0.2.0-rc.1", | ||
"description": "A Web Component for web presentations", | ||
"main": "slide-deck.js", | ||
"scripts": { | ||
"start": "npx http-server ." | ||
"serve": "npx http-server . --port 6000" | ||
}, | ||
@@ -9,0 +9,0 @@ "repository": { |
@@ -81,7 +81,7 @@ # `slide-deck` | ||
- `command-enter`: Resume presentation (from active slide) | ||
- `command-.`: End presentation | ||
- `alt-enter`: Join presentation in speaker view (from active slide) | ||
*Windows and Linux users can use Ctrl instead of Command.* | ||
When presenting (key-control is active): | ||
When presenting (`key-control` is active): | ||
@@ -94,3 +94,3 @@ - `N`/`rightArrow`/`downArrow`/`pageDown`: Next slide | ||
- `B`/`.`: Toggle black screen | ||
- `escape`: Blur focused element, close control panel, or end presentation | ||
- `escape`: Blur focused element or close control panel | ||
@@ -97,0 +97,0 @@ These are based on |
class slideDeck extends HTMLElement { | ||
// template | ||
static knownViews = { | ||
public: 'slideshow', | ||
private: 'speaker', | ||
}; | ||
// -------------------------------------------------------------------------- | ||
// shadow DOM (control panel & blank slide) | ||
static #appendShadowTemplate = (node) => { | ||
@@ -16,17 +24,10 @@ const template = document.createElement("template"); | ||
<div part="controls"> | ||
<button part="button" slide-event='toggle-control'> | ||
keyboard navigation | ||
<button part="button event" slide-event="start"> | ||
start slideshow | ||
</button> | ||
<hr> | ||
<p><strong>Presentation:</strong></p> | ||
<button part="button event" slide-event> | ||
start | ||
</button> | ||
<button part="button event" slide-event> | ||
resume | ||
</button> | ||
<button part="button event" slide-event> | ||
reset | ||
join as speaker | ||
</button> | ||
@@ -38,9 +39,11 @@ | ||
<button part="button view" set-view> | ||
grid | ||
${slideDeck.knownViews.public} | ||
</button> | ||
<button part="button view" set-view> | ||
solo | ||
${slideDeck.knownViews.private} | ||
</button> | ||
<button part="button view" set-view> | ||
script | ||
<hr> | ||
<button part="button" slide-event='key-control'> | ||
keyboard navigation | ||
</button> | ||
@@ -64,3 +67,2 @@ </div> | ||
// css | ||
static #adoptShadowStyles = (node) => { | ||
@@ -137,3 +139,5 @@ const shadowStyle = new CSSStyleSheet(); | ||
// static | ||
// -------------------------------------------------------------------------- | ||
// static properties | ||
static observedAttributes = [ | ||
@@ -146,16 +150,3 @@ 'key-control', | ||
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'); | ||
} | ||
static storageKeys = [ | ||
static storeValues = [ | ||
'control', | ||
@@ -167,9 +158,3 @@ 'follow', | ||
static slideViews = [ | ||
'grid', | ||
'solo', | ||
'script', | ||
]; | ||
static controlKeys = { | ||
static navKeys = { | ||
'Home': 'firstSlide', | ||
@@ -199,7 +184,9 @@ 'End': 'lastSlide', | ||
// dynamic | ||
#store = {}; | ||
// -------------------------------------------------------------------------- | ||
// dynamic properties | ||
slides; | ||
slideCount; | ||
activeSlide; | ||
#controlPanel; | ||
@@ -211,4 +198,91 @@ #blankSlide; | ||
#body; | ||
#store = {}; | ||
// -------------------------------------------------------------------------- | ||
// get 'n set | ||
// attrs | ||
get keyControl() { return this.#keyControl(this); } | ||
set keyControl(on) { | ||
if (on) { | ||
this.setAttribute('key-control', on); | ||
} else { | ||
this.removeAttribute('key-control'); | ||
} | ||
} | ||
get followActive() { | ||
return this.hasAttribute('follow-active'); | ||
} | ||
set followActive(on) { | ||
if (on) { | ||
this.setAttribute('follow-active', ''); | ||
} else { | ||
this.removeAttribute('follow-active'); | ||
} | ||
} | ||
get fullScreen() { | ||
return this.hasAttribute('full-screen'); | ||
} | ||
set fullScreen(on) { | ||
if (on) { | ||
this.setAttribute('full-screen', ''); | ||
} else { | ||
this.removeAttribute('full-screen'); | ||
} | ||
} | ||
// params | ||
get urlParams() { | ||
return new URLSearchParams(window.location.search); | ||
} | ||
// update individual parameters | ||
updateUrlParams(update) { | ||
const params = this.urlParams; | ||
Object.keys(update).forEach((name) => { params.set(name, update[name]) }); | ||
history.pushState({}, '', `?${params.toString()}`); | ||
} | ||
// views | ||
get publicView() { | ||
return this.getAttribute('public-view') || slideDeck.knownViews.public; | ||
} | ||
set publicView(string) { | ||
this.setAttribute('public-view', string); | ||
} | ||
get privateView() { | ||
return this.getAttribute('private-view') || slideDeck.knownViews.private; | ||
} | ||
set privateView(string) { | ||
this.setAttribute('private-view', string); | ||
} | ||
get slideView() { | ||
return this.urlParams.get('slide-view') | ||
|| sessionStorage.getItem(this.#store.view) | ||
|| this.getAttribute('slide-view') | ||
|| this.publicView; | ||
} | ||
set slideView(view) { | ||
// Don't update the URL if not needed, for instance after a back/forward | ||
// navigation. Otherwise, we overwrite the forward history. | ||
if (this.urlParams.get('slide-view') !== view) { | ||
this.updateUrlParams({ 'slide-view': view }); | ||
} | ||
this.setAttribute('slide-view', view); | ||
sessionStorage.setItem(this.#store.view, view); | ||
} | ||
// -------------------------------------------------------------------------- | ||
// callbacks | ||
attributeChangedCallback(name) { | ||
@@ -218,6 +292,6 @@ | ||
case 'key-control': | ||
this.#keyControlChange(); | ||
this.#onKeyControlChange(); | ||
break; | ||
case 'follow-active': | ||
this.#followActiveChange(); | ||
this.#onFollowActiveChange(); | ||
break; | ||
@@ -242,2 +316,22 @@ case 'slide-view': | ||
// custom events | ||
this.addEventListener('key-control', this.toggleKeyControl); | ||
this.addEventListener('follow-active', this.toggleFollowActive); | ||
this.addEventListener('full-screen', this.toggleFullScreen); | ||
this.addEventListener('join', this.join); | ||
this.addEventListener('start', this.start); | ||
this.addEventListener('resume', this.resume); | ||
this.addEventListener('reset', this.reset); | ||
this.addEventListener('blank-slide', this.blankSlide); | ||
this.addEventListener('join-as-speaker', this.joinAsSpeaker); | ||
this.addEventListener('next', this.next); | ||
this.addEventListener('previous', this.previous); | ||
this.addEventListener('to-slide', (e) => this.toSlide(e.detail)); | ||
this.addEventListener('to-saved', this.toSavedSlide); | ||
this.addEventListener('scroll-to-active', this.scrollToActive); | ||
}; | ||
connectedCallback() { | ||
// relevant nodes | ||
@@ -253,3 +347,3 @@ this.#body = document.querySelector('body'); | ||
this.#setupSlides(); | ||
this.goTo(); | ||
this.toSlide(); | ||
@@ -261,48 +355,19 @@ // buttons | ||
// shadow DOM event listeners | ||
this.shadowRoot.addEventListener('keydown', (event) => { | ||
event.stopPropagation(); | ||
if (this.hasAttribute('blank-slide')) { | ||
event.preventDefault(); | ||
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 | ||
this.addEventListener('toggle-control', (e) => this.toggleAttribute('key-control')); | ||
this.addEventListener('toggle-follow', (e) => this.toggleAttribute('follow-active')); | ||
this.addEventListener('toggle-fullscreen', (e) => this.fullScreenEvent()); | ||
this.addEventListener('join', (e) => this.joinEvent()); | ||
this.addEventListener('start', (e) => this.startEvent()); | ||
this.addEventListener('resume', (e) => this.resumeEvent()); | ||
this.addEventListener('reset', (e) => this.resetEvent()); | ||
this.addEventListener('blank-slide', (e) => this.blankSlideEvent()); | ||
this.addEventListener('join-with-notes', (e) => this.joinWithNotesEvent()); | ||
this.addEventListener('next', (e) => this.move(1)); | ||
this.addEventListener('saved-slide', (e) => this.goToSaved()); | ||
this.addEventListener('previous', (e) => this.move(-1)); | ||
this.addEventListener('to-slide', (e) => this.goTo(e.detail)); | ||
}; | ||
connectedCallback() { | ||
this.#body.addEventListener('keydown', this.#keyEventActions); | ||
// events | ||
this.#body.addEventListener('keydown', this.#bodyKeyEvents); | ||
this.#blankSlide.addEventListener('close', this.#onBlankSlideClosed); | ||
window.addEventListener('popstate', this.#syncViewOnLocationChange); | ||
} | ||
disconnectedCallback() { | ||
this.#body.removeEventListener('keydown', this.#keyEventActions); | ||
this.#body.removeEventListener('keydown', this.#bodyKeyEvents); | ||
this.#blankSlide.removeEventListener('close', this.#onBlankSlideClosed); | ||
window.removeEventListener('popstate', this.#syncViewOnLocationChange); | ||
} | ||
// -------------------------------------------------------------------------- | ||
// setup methods | ||
#cleanString = (str) => str.trim().toLowerCase(); | ||
#cleanString = (str) => str.trim().toLowerCase().replaceAll(' ', '-'); | ||
#newDeckId = (from, count) => { | ||
@@ -323,3 +388,3 @@ const base = from || window.location.pathname.split('.')[0]; | ||
// storage keys based on slide ID | ||
slideDeck.storageKeys.forEach((key) => { | ||
slideDeck.storeValues.forEach((key) => { | ||
this.#store[key] = `${this.id}.${key}`; | ||
@@ -361,6 +426,3 @@ }); | ||
// view required | ||
this.setAttribute( | ||
'slide-view', | ||
sessionStorage.getItem(this.#store.view) || this.slideView || 'grid' | ||
); | ||
this.setAttribute('slide-view', this.slideView); | ||
@@ -371,3 +433,5 @@ // fullscreen must be set by user interaction | ||
// buttons | ||
// -------------------------------------------------------------------------- | ||
// button setup | ||
#findButtons = (attr) => [ | ||
@@ -401,3 +465,3 @@ ...this.querySelectorAll(`:scope button[${attr}]`), | ||
#setToggleState = (btn, attr, state) => { | ||
#setButtonToggleState = (btn, attr, state) => { | ||
const isActive = this.#getButtonValue(btn, attr) === state; | ||
@@ -407,2 +471,3 @@ this.#setButtonPressed(btn, isActive); | ||
// go-to buttons | ||
#setupGoToButtons = () => { | ||
@@ -420,3 +485,3 @@ this.#goToButtons = this.#findButtons('to-slide'); | ||
btn.addEventListener('click', (e) => { | ||
this.goTo(toSlide); | ||
this.toSlide(toSlide); | ||
}); | ||
@@ -426,2 +491,3 @@ }); | ||
// view buttons | ||
#setupViewButtons = () => { | ||
@@ -432,5 +498,5 @@ this.#viewButtons = this.#findButtons('set-view'); | ||
btn.addEventListener('click', (e) => { | ||
this.setAttribute('slide-view', this.#getButtonValue(btn, 'set-view')); | ||
this.slideView = this.#getButtonValue(btn, 'set-view'); | ||
}); | ||
this.#setToggleState(btn, 'set-view', this.slideView); | ||
this.#setButtonToggleState(btn, 'set-view', this.slideView); | ||
}); | ||
@@ -440,14 +506,7 @@ } | ||
#updateViewButtons = () => { | ||
this.#viewButtons.forEach((btn) => { | ||
this.#setToggleState(btn, 'set-view', this.slideView); | ||
this.#viewButtons?.forEach((btn) => { | ||
this.#setButtonToggleState(btn, 'set-view', this.slideView); | ||
}); | ||
} | ||
// attribute changes | ||
#onViewChange = () => { | ||
this.#updateViewButtons(); | ||
this.scrollToActive(); | ||
sessionStorage.setItem(this.#store.view, this.slideView); | ||
} | ||
// event buttons | ||
@@ -468,9 +527,9 @@ #setupEventButtons = () => { | ||
#updateEventButtons = () => { | ||
this.#eventButtons.forEach((btn) => { | ||
this.#eventButtons?.forEach((btn) => { | ||
const btnEvent = this.#getButtonValue(btn, 'slide-event'); | ||
let isActive = { | ||
'toggle-control': this.keyControl, | ||
'toggle-follow': this.followActive, | ||
'toggle-fullscreen': this.fullScreen, | ||
'key-control': this.keyControl, | ||
'follow-active': this.followActive, | ||
'full-screen': this.fullScreen, | ||
} | ||
@@ -484,38 +543,39 @@ | ||
// event handlers | ||
#startPresenting = () => { | ||
this.setAttribute('slide-view', 'solo'); | ||
this.setAttribute('key-control', ''); | ||
this.setAttribute('follow-active', ''); | ||
#syncViewOnLocationChange = () => { | ||
const queryView = this.urlParams.get('slide-view'); | ||
if (queryView && queryView !== this.getAttribute('slide-view')) { | ||
this.slideView = this.urlParams.get('slide-view'); | ||
} | ||
} | ||
startEvent = () => { | ||
this.goTo(1); | ||
this.#startPresenting(); | ||
// -------------------------------------------------------------------------- | ||
// event handlers | ||
reset = () => { | ||
this.toSlide(1); | ||
} | ||
resumeEvent = () => { | ||
this.goToSaved(); | ||
this.#startPresenting(); | ||
join = () => { | ||
this.keyControl = true; | ||
this.followActive = true; | ||
} | ||
resetEvent = () => { | ||
this.goTo(1); | ||
resume = () => { | ||
this.slideView = this.publicView; | ||
this.join(); | ||
} | ||
joinWithNotesEvent = () => { | ||
this.setAttribute('slide-view', 'script'); | ||
this.setAttribute('key-control', ''); | ||
this.setAttribute('follow-active', ''); | ||
start = () => { | ||
this.reset(); | ||
this.resume(); | ||
} | ||
joinEvent = () => { | ||
this.setAttribute('key-control', ''); | ||
this.setAttribute('follow-active', ''); | ||
joinAsSpeaker = () => { | ||
this.slideView = this.privateView; | ||
this.join(); | ||
} | ||
blankSlideEvent = (color) => { | ||
blankSlide = (color) => { | ||
if (this.#blankSlide.open) { | ||
this.#blankSlide.close(); | ||
this.removeAttribute('blank-slide'); | ||
} else { | ||
@@ -527,3 +587,3 @@ this.#blankSlide.showModal(); | ||
fullScreenEvent = () => { | ||
toggleFullScreen = () => { | ||
this.toggleAttribute('full-screen'); | ||
@@ -538,19 +598,35 @@ | ||
// dynamic attribute methods | ||
#keyControlChange = () => { | ||
toggleKeyControl = () => this.toggleAttribute('key-control'); | ||
toggleFollowActive = () => this.toggleAttribute('follow-active'); | ||
// -------------------------------------------------------------------------- | ||
// attribute-change methods | ||
#onBlankSlideClosed = () => { | ||
this.removeAttribute('blank-slide'); | ||
} | ||
#onViewChange = () => { | ||
this.scrollToActive(); | ||
this.#updateViewButtons(); | ||
} | ||
#onKeyControlChange = () => { | ||
if (this.keyControl) { | ||
this.goToSaved(); | ||
this.toSavedSlide(); | ||
} | ||
} | ||
#followActiveChange = () => { | ||
#onFollowActiveChange = () => { | ||
if (this.followActive) { | ||
this.goToSaved(); | ||
window.addEventListener('storage', (e) => this.goToSaved()); | ||
this.toSavedSlide(); | ||
window.addEventListener('storage', (e) => this.toSavedSlide()); | ||
} else { | ||
window.removeEventListener('storage', (e) => this.goToSaved()); | ||
window.removeEventListener('storage', (e) => this.toSavedSlide()); | ||
} | ||
} | ||
// -------------------------------------------------------------------------- | ||
// storage | ||
#asSlideInt = (string) => parseInt(string, 10); | ||
@@ -572,2 +648,3 @@ #indexFromId = (string) => this.#asSlideInt(string.split('-').pop()); | ||
}; | ||
#slideToStore = (to) => { | ||
@@ -581,3 +658,5 @@ if (to) { | ||
// navigation | ||
// -------------------------------------------------------------------------- | ||
// slide navigation | ||
#inRange = (slide) => slide >= 1 && slide <= this.slideCount; | ||
@@ -594,3 +673,3 @@ #getActive = () => this.#slideFromHash() || this.activeSlide; | ||
goTo = (to) => { | ||
toSlide = (to) => { | ||
const fromHash = this.#slideFromHash(); | ||
@@ -620,64 +699,137 @@ const setTo = to || this.#getActive(); | ||
const to = (this.#getActive() || 0) + by; | ||
this.goTo(to); | ||
this.toSlide(to); | ||
}; | ||
goToSaved = () => { | ||
this.goTo(this.#slideFromStore()); | ||
next = () => this.move(1); | ||
previous = () => this.move(-1); | ||
toSavedSlide = () => { | ||
this.toSlide(this.#slideFromStore()); | ||
} | ||
// -------------------------------------------------------------------------- | ||
// keyboard control | ||
// Get the keyControl value of an element | ||
#keyControl = (el) => { | ||
if (!el) return false; | ||
if (!el.hasAttribute('key-control')) return false; | ||
const controlValue = el.getAttribute('key-control'); | ||
const trueValues = ['true', '', 'key-control']; | ||
if (trueValues.includes(controlValue)) return true; | ||
return controlValue; | ||
} | ||
#nearestKeyControl = (el) => { | ||
const ancestor = el.closest('[key-control]'); | ||
return this.#keyControl(ancestor); | ||
} | ||
// 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')) { | ||
#escToBlur = (event) => { | ||
if (event.key === 'Escape') { | ||
event.preventDefault(); | ||
this.removeAttribute('blank-slide'); | ||
return; | ||
event.target.blur(); | ||
return true; | ||
} | ||
} | ||
// always available | ||
if (this.#cmdOrCtrl(event)) { | ||
switch (event.key) { | ||
case 'k': | ||
event.preventDefault(); | ||
this.#controlPanel.showModal(); | ||
break; | ||
case 'Enter': | ||
if (event.shiftKey) { | ||
event.preventDefault(); | ||
this.startEvent(); | ||
} else { | ||
event.preventDefault(); | ||
this.resumeEvent(); | ||
} | ||
break; | ||
#isPrivateKeydown = (event) => { | ||
// it's only private if the focus is somewhere else | ||
if (event.target === this.#body) return; | ||
default: | ||
break; | ||
} | ||
const controlSetting = this.#nearestKeyControl(event.target); | ||
if (['none', 'false'].includes(controlSetting)) return true; | ||
// esc to blur, anywhere | ||
if (this.#escToBlur(event)) return true; | ||
if (controlSetting === 'escape') return true; | ||
// anything in an iframe is private | ||
if (event.target.ownerDocument !== document) return true; | ||
// anything in contentEditable is private | ||
if (event.target.getAttribute('contenteditable')) return true; | ||
// scrollable elements are private | ||
const overflowStyle = window | ||
.getComputedStyle(event.target) | ||
.getPropertyValue('overflow'); | ||
const hasOverflow = (event.target.scrollHeight > event.target.clientHeight | ||
|| event.target.scrollWidth > event.target.clientWidth); | ||
if (hasOverflow && overflowStyle !== 'hidden') return true; | ||
// most form inputs are private… | ||
switch (event.target.tagName) { | ||
case 'TEXTAREA': | ||
case 'SELECT': | ||
case 'SUMMARY': | ||
return true; | ||
case 'A': | ||
return event.key === 'Enter'; | ||
case 'INPUT': | ||
const clickOnly = ['button', 'checkbox']; | ||
const type = event.target.getAttribute('type').toLowerCase(); | ||
if (!clickOnly.includes(type)) return true; | ||
break; | ||
default: | ||
break; | ||
} | ||
// click events are private by default | ||
if ([' ', 'Enter'].includes(event.key)) return true; | ||
// todo: | ||
// - tab panels and tree menus? | ||
// - non-summary disclosures? (should use buttons) | ||
// https://webaim.org/techniques/keyboard/ | ||
} | ||
#bodyKeyEvents = (event) => { | ||
// modal events | ||
if (event.key === 'k' && this.#cmdOrCtrl(event)) { | ||
event.preventDefault(); | ||
this.#controlPanel.open | ||
? this.#controlPanel.close() | ||
: this.#controlPanel.showModal(); | ||
} else if (this.#controlPanel.open) { | ||
this.#escToBlur(event) && this.#controlPanel.close(); | ||
return; | ||
} else if (event.altKey && event.key === 'Enter') { | ||
} else if (this.#blankSlide.open && !this.#cmdOrCtrl(event)) { | ||
event.preventDefault(); | ||
this.joinWithNotesEvent(); | ||
this.blankSlide(); | ||
return; | ||
} | ||
// always available, quickstart | ||
if (event.key === 'Enter') { | ||
if (this.#cmdOrCtrl(event)) { | ||
event.preventDefault(); | ||
event.shiftKey ? this.start(): this.resume(); | ||
} else if (event.altKey) { | ||
event.preventDefault(); | ||
this.joinAsSpeaker(); | ||
} | ||
return; | ||
} | ||
// only while key-control is active | ||
if (this.keyControl) { | ||
if (event.key === 'Escape') { | ||
if (event.target !== this.#body) { | ||
event.target.blur(); | ||
} | ||
return; | ||
} | ||
if (this.#isPrivateKeydown(event)) return; | ||
switch (slideDeck.controlKeys[event.key]) { | ||
switch (slideDeck.navKeys[event.key]) { | ||
case 'firstSlide': | ||
event.preventDefault(); | ||
this.goTo(1); | ||
this.toSlide(1); | ||
break; | ||
case 'lastSlide': | ||
event.preventDefault(); | ||
this.goTo(this.slideCount); | ||
this.toSlide(this.slideCount); | ||
break; | ||
@@ -694,7 +846,7 @@ case 'nextSlide': | ||
event.preventDefault(); | ||
this.blankSlideEvent('black'); | ||
this.blankSlide('black'); | ||
break; | ||
case 'whiteOut': | ||
event.preventDefault(); | ||
this.blankSlideEvent('white'); | ||
this.blankSlide('white'); | ||
break; | ||
@@ -701,0 +853,0 @@ default: |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
86888
14
757