Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@oddbird/slide-deck

Package Overview
Dependencies
Maintainers
0
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@oddbird/slide-deck - npm Package Compare versions

Comparing version 0.1.4 to 0.2.0-rc.1

slide-deck.webc

52

CHANGELOG.md

@@ -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 @@

14

CONTRIBUTING.md

@@ -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>.

4

package.json
{
"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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc