From dd5e9467547abdc32ad016c3bbe15774ee1fb5ad Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 10:57:49 -0700 Subject: [PATCH 1/7] Add shortcuts and other fixes, start changelog --- CHANGELOG.md | 28 +++++++ README.md | 23 ++++++ index.html | 72 ++++++++++++---- slide-deck.js | 221 ++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 291 insertions(+), 53 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9887249 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changes + +**⚠️ This is a pre-release**: +Breaking changes will be allowed in minor versions +until we achieve a stable v1.0 release + +## v0.1.1 - unreleased + +- 💥 BREAKING: Updated keyboard shortcuts + to match [PowerPoint](https://support.microsoft.com/en-us/office/use-keyboard-shortcuts-to-deliver-powerpoint-presentations-1524ffce-bd2a-45f4-9a7f-f18b992b93a0#bkmk_frequent_macos), + including `command-.` as 'end presentation' + rather than 'toggle full-screen' (now `command-shift-f`) +- 🚀 NEW: Support for blank-screen shortcuts + (inspired by [Curtis Wilcox](https://codepen.io/ccwilcox/details/NWJWwOE)) +- 🚀 NEW: Control panel includes toggle for keyboard controls +- 🚀 NEW: Control panel buttons have `aria-pressed` styles +- 🚀 NEW: All slide-event buttons that toggle a boolean state + get `aria-pressed` values that update with the state +- 🐞 FIXED: Scroll to the active slide when changing views +- 🐞 FIXED: Control panel view toggles were broken +- 🐞 FIXED: Control panel prevents propagation of keyboard shortcuts +- 👀 INTERNAL: The current slide is stored in an `activeSlide` property + +## v0.1.0 - 2023-12-22 + +Initial draft +based on +[Miriam's Proof of Concept](https://codepen.io/miriamsuzanne/pen/eYXOLjE?editors=1010). diff --git a/README.md b/README.md index 3d6692d..82d7b82 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,29 @@ This Web Component allows you to: - Follow along in a second tab (speaker view) - Toggle full-screen mode +## Keyboard Shortcuts + +Always available: + +- `command-k`: Toggle control panel +- `command-shift-enter`: Start presentation (from first slide) +- `command-enter`: Resume presentation (from active slide) +- `command-shift-f`: Toggle full-screen mode + +When presenting (key-control is active): + +- `N`/`rightArrow`/`downArrow`/`pageDown`: Next slide +- `P`/`leftArrow`/`upArrow`/`pageUp`: Previous slide +- `home`: First slide +- `end`: Last slide +- `W`/`,`: Toggle white screen +- `B`/`.`: Toggle black screen +- `escape`: Blur focused element, close control panel, or end presentation +- `command-.`: End presentation + +These are based on +the [PowerPoint shortcuts](https://support.microsoft.com/en-us/office/use-keyboard-shortcuts-to-deliver-powerpoint-presentations-1524ffce-bd2a-45f4-9a7f-f18b992b93a0#bkmk_frequent_macos). + ## Installation You have a few options (choose one of these): diff --git a/index.html b/index.html index db3c5f5..e213db2 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,12 @@ -

Slide-Deck Web Component

+
+

Slide-Deck Web Component

+

+ github.com/oddbird/slide-deck/ +

+

No Dependencies

Progressive Enhancement

Just HTML

@@ -18,20 +23,54 @@

Always available:

    -
  • command-k: control panel
  • -
  • command-period: fullscreen
  • +
  • command-k: Toggle control panel
  • +
  • command-shift-enter: Start presentation (from first slide)
  • +
  • command-enter: Resume presentation (from active slide)
  • +
  • command-shift-f: Toggle full-screen mode

Active Presentation:

  • - right-arrow/page-down: - next slide + N + / right-arrow + / down-arrow + / page-down: + Next slide
  • - left-arrow/page-up: - previous slide + P + / left-arrow + / up-arrow + / page-up: + Previous slide +
  • +
  • + home: + First slide +
  • +
  • + end: + Last slide +
  • +
  • + W + / ,: + Toggle white screen +
  • +
  • + B + / .: + Toggle black screen +
  • +
  • + escape: + Blur focused element, close control panel, or end presentation +
  • +
  • + command-.: + End presentation
@@ -53,18 +92,17 @@

<button set-view>list<button>

Speaker View

-

Open Source

-

ToDo: Github Repo

-

(and NPM package??)

+

Open Source

+

+ github.com/oddbird/slide-deck/ +

-

ToDo: Full Documentation

-

ToDo: Improved

-

ToDo: Speaker Notes

-

ToDo: Slide Templates

-

ToDo: CSS Themes

-

ToDo: More Shortcuts

-

ToDo: More Better Good Stuff

+

To Do…

+

… Speaker Notes

+

… Slide Templates

+

… CSS Themes

+

… More Better Good Stuff

diff --git a/slide-deck.js b/slide-deck.js index 4ac2a74..f008acb 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -7,14 +7,16 @@ class slideDeck extends HTMLElement {
+ +

Presentation:

View:

- - + +
@@ -30,12 +32,15 @@ class slideDeck extends HTMLElement { static adoptShadowStyles = (node) => { const shadowStyle = new CSSStyleSheet(); shadowStyle.replaceSync(` + :host { + position: relative; + } + :host:not(:fullscreen) { container: host / inline-size; } :host(:fullscreen) { - container-type: auto; background-color: white; overflow-x: clip; overflow-y: auto; @@ -53,6 +58,17 @@ class slideDeck extends HTMLElement { ---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); display: grid; @@ -91,6 +107,14 @@ class slideDeck extends HTMLElement { ); outline-offset: var(--slide-active-outline-offset, 3px); } + + button[aria-pressed=true] { + box-shadow: inset 0 0 2px black; + + &::before { + content: ' ✅ '; + } + } `); node.shadowRoot.adoptedStyleSheets = [shadowStyle]; } @@ -122,12 +146,41 @@ class slideDeck extends HTMLElement { 'list', ]; + static controlKeys = { + 'Home': 'firstSlide', + 'End': 'lastSlide', + + // next slide + 'ArrowRight': 'nextSlide', + 'ArrowDown': 'nextSlide', + 'PageDown': 'nextSlide', + 'N': 'nextSlide', + ' ': 'nextSlide', + + // previous slide + 'ArrowLeft': 'previousSlide', + 'ArrowUp': 'previousSlide', + 'PageUp': 'previousSlide', + 'P': 'previousSlide', + 'Delete': 'previousSlide', + + // blank slide + 'B': 'blackOut', + '.': 'blackOut', + 'W': 'whiteOut', + ',': 'whiteOut', + + // end + '-': 'endPresentation' + } + // dynamic store = {}; slideCount; controlPanel; eventButtons; viewButtons; + activeSlide; body; // callbacks @@ -143,10 +196,13 @@ class slideDeck extends HTMLElement { break; case 'slide-view': this.updateViewButtons(); + this.scrollToActive(); break; default: break; } + + this.updateEventButtons(); } constructor() { @@ -165,7 +221,7 @@ class slideDeck extends HTMLElement { this.slideCount = this.childElementCount; this.defaultAttrs(); this.setSlideIDs(); - this.slideToStore(); + this.goTo(); // buttons this.setupEventButtons(); @@ -173,9 +229,10 @@ class slideDeck extends HTMLElement { // event listeners this.shadowRoot.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'k' && e.metaKey) { e.preventDefault(); - e.stopPropagation(); this.controlPanel.close(); } }); @@ -250,6 +307,23 @@ class slideDeck extends HTMLElement { }; // buttons + getButtonEvent = (btn) => btn.getAttribute('slide-event') || btn.innerText; + + updateEventButtons = () => { + this.eventButtons.forEach((btn) => { + const btnEvent = this.getButtonEvent(btn); + let isActive = { + 'toggleControl': this.keyControl, + 'toggleFollow': this.followActive, + 'toggleFullscreen': this.fullScreen, + } + + if (Object.keys(isActive).includes(btnEvent)) { + btn.setAttribute('aria-pressed', isActive[btnEvent]); + } + }); + } + setupEventButtons = () => { this.eventButtons = [ ...this.querySelectorAll(`button[slide-event]`), @@ -258,10 +332,12 @@ class slideDeck extends HTMLElement { this.eventButtons.forEach((btn) => { btn.addEventListener('click', (e) => { - const event = btn.getAttribute('slide-event') || btn.innerText; + const event = this.getButtonEvent(btn); this.dispatchEvent(new Event(event, { view: window, bubbles: false })); }); }); + + this.updateEventButtons(); } getButtonView = (btn) => btn.getAttribute('set-view') || btn.innerText; @@ -335,6 +411,14 @@ class slideDeck extends HTMLElement { this.resetActive(); } + toggleBlank = (color) => { + if (this.hasAttribute('blank-slide')) { + this.removeAttribute('blank-slide'); + } else { + this.setAttribute('blank-slide', color || 'black'); + } + } + // dynamic attribute methods followActiveChange = () => { if (this.followActive) { @@ -354,36 +438,62 @@ class slideDeck extends HTMLElement { } // storage - asSlideInt = (string) => parseInt(string, 10) || 1; + asSlideInt = (string) => parseInt(string, 10); - slideFromHash = (hash) => this.asSlideInt(hash.split('-').pop()); + slideFromHash = () => window.location.hash.startsWith('#slide_') + ? this.asSlideInt(window.location.hash.split('-').pop()) + : null; slideFromStore = () => this.asSlideInt( localStorage.getItem(this.store.slide) ); - slideToHash = (to) => { window.location.hash = this.slideId(to) }; - slideToStore = (to) => { - const active = to || this.slideFromHash(window.location.hash); - localStorage.setItem(this.store.slide, active); + slideToHash = (to) => { + if (to) { + window.location.hash = this.slideId(to); + } }; - - resetActive = () => { - window.location.hash = this.id; - localStorage.removeItem(this.store.slide); + slideToStore = (to) => { + if (to) { + localStorage.setItem(this.store.slide, to); + } else { + localStorage.removeItem(this.store.slide); + } }; // navigation inRange = (slide) => slide >= 1 && slide <= this.slideCount; + getActive = () => this.slideFromHash() || this.activeSlide; + + scrollToActive = () => { + const activeEl = document.getElementById(this.slideId(this.activeSlide)); - goTo = (slide) => { - if (this.inRange(slide)) { - this.slideToHash(slide); - this.slideToStore(slide); + if (activeEl) { + activeEl.scrollIntoView(true); + } + }; + + goTo = (to) => { + const fromHash = this.slideFromHash(); + const setTo = to || this.getActive(); + + if (setTo && this.inRange(setTo)) { + this.activeSlide = setTo; + this.slideToStore(setTo); + + if (setTo !== fromHash) { + this.slideToHash(setTo); + } } + } + + resetActive = () => { + this.activeSlide = null; + window.location.hash = this.id; + localStorage.removeItem(this.store.slide); }; move = (by) => { - const to = this.slideFromHash(window.location.hash) + by; + const to = (this.getActive() || 0) + by; this.goTo(to); }; @@ -392,45 +502,84 @@ class slideDeck extends HTMLElement { } keyEventActions = (event) => { + // always available if (event.metaKey) { switch (event.key) { case 'k': event.preventDefault(); this.controlPanel.showModal(); break; - case '.': - event.preventDefault(); - this.toggleAttribute('full-screen'); + case 'f': + if (event.shiftKey) { + event.preventDefault(); + this.toggleAttribute('full-screen'); + } + break; + case 'Enter': + if (event.shiftKey) { + event.preventDefault(); + this.startEvent(); + } else { + event.preventDefault(); + this.resumeEvent(); + } break; default: break; } + return; + } else if (event.altKey && event.key === 'Enter') { + event.preventDefault(); + this.joinWithNotesEvent(); + return; } - if (event.target !== this.body) { + // only while key-control is active + if (this.keyControl) { if (event.key === 'Escape') { - event.target.blur(); + if (event.target !== this.body) { + event.target.blur(); + } else { + event.preventDefault(); + this.endEvent(); + } + return; } - return; - } - if (this.keyControl) { - switch (event.key) { - case 'ArrowRight': + if (event.metaKey && event.key === '.') { + event.preventDefault(); + this.endEvent(); + return; + } + + switch (slideDeck.controlKeys[event.key]) { + case 'firstSlide': event.preventDefault(); - this.move(1); + this.goTo(1); + break; + case 'lastSlide': + event.preventDefault(); + this.goTo(this.slideCount); break; - case 'PageDown': + case 'nextSlide': event.preventDefault(); this.move(1); break; - case 'ArrowLeft': + case 'previousSlide': event.preventDefault(); this.move(-1); break; - case 'PageUp': + case 'blackOut': event.preventDefault(); - this.move(-1); + this.toggleBlank('black'); + break; + case 'whiteOut': + event.preventDefault(); + this.toggleBlank('white'); + break; + case 'endPresentation': + event.preventDefault(); + this.endEvent(); break; default: break; From bba444aeb87b79e23099b9e985c3dfe7a4280a43 Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 11:14:52 -0700 Subject: [PATCH 2/7] Escape should close modal before ending presentation --- slide-deck.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/slide-deck.js b/slide-deck.js index f008acb..493b5b7 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -228,11 +228,11 @@ class slideDeck extends HTMLElement { this.setupViewButtons(); // event listeners - this.shadowRoot.addEventListener('keydown', (e) => { - e.stopPropagation(); + this.shadowRoot.addEventListener('keydown', (event) => { + event.stopPropagation(); - if (e.key === 'k' && e.metaKey) { - e.preventDefault(); + if ((event.key === 'k' && event.metaKey) || event.key === 'Escape') { + event.preventDefault(); this.controlPanel.close(); } }); From 75bb749e314388ebcc0b8712f54a81a9f05b98bc Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 11:26:29 -0700 Subject: [PATCH 3/7] Fix command-. shortcut getting skipped --- README.md | 2 +- index.html | 8 ++++---- slide-deck.js | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 82d7b82..6b1e3ba 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Always available: - `command-k`: Toggle control panel - `command-shift-enter`: Start presentation (from first slide) - `command-enter`: Resume presentation (from active slide) +- `command-.`: End presentation - `command-shift-f`: Toggle full-screen mode When presenting (key-control is active): @@ -85,7 +86,6 @@ When presenting (key-control is active): - `W`/`,`: Toggle white screen - `B`/`.`: Toggle black screen - `escape`: Blur focused element, close control panel, or end presentation -- `command-.`: End presentation These are based on the [PowerPoint shortcuts](https://support.microsoft.com/en-us/office/use-keyboard-shortcuts-to-deliver-powerpoint-presentations-1524ffce-bd2a-45f4-9a7f-f18b992b93a0#bkmk_frequent_macos). diff --git a/index.html b/index.html index e213db2..e3624c5 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,10 @@

Always available:

  • command-shift-enter: Start presentation (from first slide)
  • command-enter: Resume presentation (from active slide)
  • command-shift-f: Toggle full-screen mode
  • +
  • + command-.: + End presentation +
  • @@ -68,10 +72,6 @@

    Active Presentation:

    escape: Blur focused element, close control panel, or end presentation -
  • - command-.: - End presentation -
  • Works with Presentation Remotes

    diff --git a/slide-deck.js b/slide-deck.js index 493b5b7..23edac4 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -524,6 +524,10 @@ class slideDeck extends HTMLElement { this.resumeEvent(); } break; + case '.': + event.preventDefault(); + this.endEvent(); + break; default: break; } @@ -546,12 +550,6 @@ class slideDeck extends HTMLElement { return; } - if (event.metaKey && event.key === '.') { - event.preventDefault(); - this.endEvent(); - return; - } - switch (slideDeck.controlKeys[event.key]) { case 'firstSlide': event.preventDefault(); From 70a041193666daa968d227063969005ba8220007 Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 11:39:33 -0700 Subject: [PATCH 4/7] Dont rout fullscreen request through attribute toggle --- slide-deck.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/slide-deck.js b/slide-deck.js index 23edac4..653ea74 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -188,9 +188,6 @@ class slideDeck extends HTMLElement { this[slideDeck.attrToPropMap[name]] = newValue || this.hasAttribute(name); switch (name) { - case 'full-screen': - this.fullScreenChange(); - break; case 'follow-active': this.followActiveChange(); break; @@ -240,7 +237,7 @@ class slideDeck extends HTMLElement { // custom events this.addEventListener('toggleControl', (e) => this.toggleAttribute('key-control')); this.addEventListener('toggleFollow', (e) => this.toggleAttribute('follow-active')); - this.addEventListener('toggleFullscreen', (e) => this.toggleAttribute('full-screen')); + 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')); @@ -251,6 +248,7 @@ class slideDeck extends HTMLElement { 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('savedSlide', (e) => this.goToSaved()); @@ -411,7 +409,7 @@ class slideDeck extends HTMLElement { this.resetActive(); } - toggleBlank = (color) => { + blankSlideEvent = (color) => { if (this.hasAttribute('blank-slide')) { this.removeAttribute('blank-slide'); } else { @@ -419,6 +417,16 @@ class slideDeck extends HTMLElement { } } + fullScreenEvent = () => { + this.toggleAttribute('full-screen'); + + if (this.fullScreen && this.requestFullscreen) { + this.requestFullscreen(); + } else if (document.fullscreenElement) { + document.exitFullscreen(); + } + } + // dynamic attribute methods followActiveChange = () => { if (this.followActive) { @@ -429,14 +437,6 @@ class slideDeck extends HTMLElement { } } - fullScreenChange = () => { - if (this.fullScreen && this.requestFullscreen) { - this.requestFullscreen(); - } else if (document.fullscreenElement) { - document.exitFullscreen(); - } - } - // storage asSlideInt = (string) => parseInt(string, 10); @@ -512,7 +512,7 @@ class slideDeck extends HTMLElement { case 'f': if (event.shiftKey) { event.preventDefault(); - this.toggleAttribute('full-screen'); + this.fullScreenEvent(); } break; case 'Enter': @@ -569,11 +569,11 @@ class slideDeck extends HTMLElement { break; case 'blackOut': event.preventDefault(); - this.toggleBlank('black'); + this.blankSlideEvent('black'); break; case 'whiteOut': event.preventDefault(); - this.toggleBlank('white'); + this.blankSlideEvent('white'); break; case 'endPresentation': event.preventDefault(); From 91dfa53337c48008815bcfb3b5d542a6914118e1 Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 11:46:34 -0700 Subject: [PATCH 5/7] Start/resume events both target active slides --- slide-deck.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/slide-deck.js b/slide-deck.js index 653ea74..dc8b5e4 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -377,10 +377,15 @@ class slideDeck extends HTMLElement { startEvent = () => { this.goTo(1); - this.resumeEvent(); + this.startPresenting(); } resumeEvent = () => { + this.goToSaved(); + this.startPresenting(); + } + + startPresenting = () => { this.setAttribute('slide-view', 'list'); this.setAttribute('key-control', ''); this.setAttribute('follow-active', ''); From bd613bc76af266c30c5f6535ed52232fac4491de Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 11:50:41 -0700 Subject: [PATCH 6/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9887249..5e85b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ until we achieve a stable v1.0 release rather than 'toggle full-screen' (now `command-shift-f`) - 🚀 NEW: Support for blank-screen shortcuts (inspired by [Curtis Wilcox](https://codepen.io/ccwilcox/details/NWJWwOE)) +- 🚀 NEW: Both start/resume events target active slides - 🚀 NEW: Control panel includes toggle for keyboard controls - 🚀 NEW: Control panel buttons have `aria-pressed` styles - 🚀 NEW: All slide-event buttons that toggle a boolean state From e21e131ded30169693ee25fc8047fd1a40e2da5a Mon Sep 17 00:00:00 2001 From: Miriam Suzanne Date: Tue, 26 Dec 2023 12:00:31 -0700 Subject: [PATCH 7/7] Add pre-release notice to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6b1e3ba..985dc55 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A Web Component for web presentations. **[Demo](https://slide-deck.netlify.app)** +**⚠️ This is a pre-release**: +Breaking changes will be allowed in minor versions +until we achieve a stable v1.0 release + ## Examples General usage example: