diff --git a/css/animations.css b/css/animations.css index 9c43cc3..1db5edb 100644 --- a/css/animations.css +++ b/css/animations.css @@ -1,11 +1,19 @@ @keyframes slide-left { - from { margin-left: 0vw; } - to { margin-left: -100vw; } + from { + margin-left: 0vw; + } + to { + margin-left: -100vw; + } } @keyframes enter-right { - from { margin-left: 100vw; } - to { margin-left: 0vw; } + from { + margin-left: 100vw; + } + to { + margin-left: 0vw; + } } .anim-slide-left-begin { @@ -21,8 +29,12 @@ } @keyframes spin { - from { transform: rotate(0deg) } - to { transform: rotate(360deg) } + from { + transform: rotate(0deg) + } + to { + transform: rotate(360deg) + } } .anim-spin { diff --git a/js/animator.js b/js/animator.js index ca3d2c2..f4f973f 100644 --- a/js/animator.js +++ b/js/animator.js @@ -1,18 +1,43 @@ +// @ts-check + +/** + * Handles animations and transitions + * @property {boolean} _transitioning True when animation in effect + * @property {string} _begin The beginning animation class + * @property {string} _end The ending animation class + */ export class Animator { + /** + * Create an instance of the animation helper + */ constructor() { this._transitioning = false; this._begin = null; this._end = null; } + /** + * True when animation in effect + * @returns {boolean} + */ get transitioning() { return this._transitioning; } + /** + * True when ready to complete second part of animation + * @returns {boolean} + */ get animationReady() { return !!this._end; } + /** + * Kicks off a new animation (old slide) + * @param {string} animationName Name of the animation + * @param {HTMLDivElement} host The div to be animated + * @param {Function} callback Function to call when the animation completes + */ beginAnimation(animationName, host, callback) { this._transitioning = true; this._begin = `anim-${animationName}-begin`; @@ -27,6 +52,10 @@ export class Animator { host.classList.add(this._begin); } + /** + * Kicks off the final animation (new slide) + * @param {HTMLDivElement} host The div to animate + */ endAnimation(host) { this._transitioning = true; const animationEnd = () => { diff --git a/js/app.js b/js/app.js index 80bd951..875dcd9 100644 --- a/js/app.js +++ b/js/app.js @@ -2,11 +2,13 @@ import { registerDeck } from "./navigator.js" import { registerControls } from "./controls.js" import { registerKeyHandler } from "./keyhandler.js" +/** + * Main application element, simply registers the web components + */ const app = async () => { registerDeck(); registerControls(); registerKeyHandler(); }; -document.addEventListener("DOMContentLoaded", app); - +document.addEventListener("DOMContentLoaded", app); \ No newline at end of file diff --git a/js/controls.js b/js/controls.js index 029c101..8f3b2da 100644 --- a/js/controls.js +++ b/js/controls.js @@ -1,11 +1,35 @@ -class Controls extends HTMLElement { +// @ts-check +import { Navigator } from "./navigator.js" + +/** + * @typedef {object} CustomRef + * @property {HTMLButtonElement} first The button to jump to the first slide + * @property {HTMLButtonElement} prev The button to move to the previous slide + * @property {HTMLButtonElement} next The button to advance to the next slide + * @property {HTMLButtonElement} last The button to advance to the last slide + * @property {HTMLSpanElement} pos The span for the positional information + */ + +/** + * Custom element that renders controls to navigate the deck + */ +export class Controls extends HTMLElement { + + /** + * Create a new instance of controls + */ constructor() { super(); + /** @type {CustomRef} */ this._controlRef = null; + /** @type {Navigator} */ this._deck = null; } + /** + * Called when the element is inserted into the DOM. Used to fetch the template and wire into the related Navigator instance. + */ async connectedCallback() { const response = await fetch("/templates/controls.html"); const template = await response.text(); @@ -14,11 +38,11 @@ class Controls extends HTMLElement { host.innerHTML = template; this.appendChild(host); this._controlRef = { - first: document.getElementById("ctrlFirst"), - prev: document.getElementById("ctrlPrevious"), - next: document.getElementById("ctrlNext"), - last: document.getElementById("ctrlLast"), - pos: document.getElementById("position") + first: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlFirst")), + prev: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlPrevious")), + next: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlNext")), + last: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlLast")), + pos: /** @type {HTMLSpanElement} **/(document.getElementById("position")) }; this._controlRef.first.addEventListener("click", () => this._deck.jumpTo(0)); this._controlRef.prev.addEventListener("click", () => this._deck.previous()); @@ -27,20 +51,33 @@ class Controls extends HTMLElement { this.refreshState(); } + /** + * Get the list of attributes to watch + * @returns {string[]} + */ static get observedAttributes() { return ["deck"]; } + /** + * Called when the attribute is set + * @param {string} attrName Name of the attribute that was set + * @param {string} oldVal The old attribute value + * @param {string} newVal The new attribute value + */ async attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === "deck") { if (oldVal !== newVal) { - this._deck = document.getElementById(newVal); - this._deck.addEventListener("slideschanged", () => this.refreshState()); + this._deck = /** @type {Navigator} */(document.getElementById(newVal)); + this._deck.addEventListener("slideschanged", () => this.refreshState()); } } } - refreshState() { + /** + * Enables/disables buttons and updates position based on index in the deck + */ + refreshState() { const next = this._deck.hasNext; const prev = this._deck.hasPrevious; this._controlRef.first.disabled = !prev; @@ -51,4 +88,5 @@ class Controls extends HTMLElement { } } +/** Register the custom slide-controls element */ export const registerControls = () => customElements.define('slide-controls', Controls); \ No newline at end of file diff --git a/js/jsonLoader.js b/js/jsonLoader.js deleted file mode 100644 index cddfb51..0000000 --- a/js/jsonLoader.js +++ /dev/null @@ -1,4 +0,0 @@ -export async function getJson(path) { - const response = await fetch(path); - return await response.json(); -}; \ No newline at end of file diff --git a/js/keyhandler.js b/js/keyhandler.js index 4808326..3ff4fa1 100644 --- a/js/keyhandler.js +++ b/js/keyhandler.js @@ -1,18 +1,42 @@ -class KeyHandler extends HTMLElement { +// @ts-check +import { Navigator } from "./navigator.js" + +/** + * Custom element to handle key press + */ +export class KeyHandler extends HTMLElement { + + /** + * Create a key handler instance + */ constructor() { super(); + /** + * The related Navigator element + * @type {Navigator} + */ this._deck = null; } + /** + * Gets the attributes being watched + * @returns {string[]} The attributes to watch + */ static get observedAttributes() { return ["deck"]; } + /** + * Called when attributes change + * @param {string} attrName The attribute that changed + * @param {string} oldVal The old value + * @param {string} newVal The new value + */ async attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === "deck") { if (oldVal !== newVal) { - this._deck = document.getElementById(newVal); + this._deck = /** @type {Navigator} */(document.getElementById(newVal)); this._deck.parentElement.addEventListener("keydown", key => { if (key.keyCode == 39 || key.keyCode == 32) { this._deck.next(); @@ -26,4 +50,7 @@ class KeyHandler extends HTMLElement { } } +/** + * Registers the custom key-handler element + */ export const registerKeyHandler = () => customElements.define('key-handler', KeyHandler); \ No newline at end of file diff --git a/js/navigator.js b/js/navigator.js index 087dae8..5f47ce5 100644 --- a/js/navigator.js +++ b/js/navigator.js @@ -1,9 +1,22 @@ +// @ts-check + import { loadSlides } from "./slideLoader.js" +import { Slide } from "./slide.js" import { Router } from "./router.js" import { Animator } from "./animator.js" -class Navigator extends HTMLElement { +/** + * The main class that handles rendering the slide decks + * @property {Animator} _animator Animation helper + * @property {Router} _router Routing helper + * @property {string} _route The current route + * @property {CustomEvent} slidesChangedEvent Event fired when slide changes + */ +export class Navigator extends HTMLElement { + /** + * Create an instance of the custom navigator element + */ constructor() { super(); this._animator = new Animator(); @@ -19,15 +32,25 @@ class Navigator extends HTMLElement { if (this._route) { var slide = parseInt(this._route) - 1; this.jumpTo(slide); - } + } } }); } + /** + * Get the list of observed attributes + * @returns {string[]} + */ static get observedAttributes() { return ["start"]; } + /** + * Called when an attribute changes + * @param {string} attrName + * @param {string} oldVal + * @param {string} newVal + */ async attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === "start") { if (oldVal !== newVal) { @@ -43,26 +66,50 @@ class Navigator extends HTMLElement { } } + /** + * Current slide index + * @returns {number} + */ get currentIndex() { return this._currentIndex; } + /** + * Current slide + * @returns {Slide} + */ get currentSlide() { return this._slides ? this._slides[this._currentIndex] : null; } + /** + * Total number of slides + * @returns {number} + */ get totalSlides() { return this._slides ? this._slides.length : 0; } + /** + * True if a previous slide exists + * @returns {boolean} + */ get hasPrevious() { return this._currentIndex > 0; } + /** + * True if a next slide exists + * @returns {boolean} + */ get hasNext() { return this._currentIndex < (this.totalSlides - 1); } + /** + * Main slide navigation: jump to specific slide + * @param {number} slideIdx + */ jumpTo(slideIdx) { if (this._animator.transitioning) { return; @@ -71,9 +118,9 @@ class Navigator extends HTMLElement { this._currentIndex = slideIdx; this.innerHTML = ''; this.appendChild(this.currentSlide.html); - this._router.setRoute(slideIdx+1); + this._router.setRoute((slideIdx + 1).toString()); this._route = this._router.getRoute(); - document.title = `${this.currentIndex+1}/${this.totalSlides}: ${this.currentSlide.title}`; + document.title = `${this.currentIndex + 1}/${this.totalSlides}: ${this.currentSlide.title}`; this.dispatchEvent(this.slidesChangedEvent); if (this._animator.animationReady) { this._animator.endAnimation(this.querySelector("div")); @@ -81,13 +128,16 @@ class Navigator extends HTMLElement { } } + /** + * Advance to next slide, if it exists. Applies animation if transition is specified + */ next() { if (this.hasNext) { if (this.currentSlide.transition !== null) { this._animator.beginAnimation( - this.currentSlide.transition, + this.currentSlide.transition, this.querySelector("div"), - () => this.jumpTo(this.currentIndex+1)); + () => this.jumpTo(this.currentIndex + 1)); } else { this.jumpTo(this.currentIndex + 1); @@ -95,6 +145,9 @@ class Navigator extends HTMLElement { } } + /** + * Move to previous slide, if it exists + */ previous() { if (this.hasPrevious) { this.jumpTo(this.currentIndex - 1); @@ -102,4 +155,7 @@ class Navigator extends HTMLElement { } } +/** + * Register the custom slide-deck component + */ export const registerDeck = () => customElements.define('slide-deck', Navigator); \ No newline at end of file diff --git a/js/router.js b/js/router.js index b0d0d46..81e8c2c 100644 --- a/js/router.js +++ b/js/router.js @@ -1,11 +1,25 @@ +// @ts-check + +/** + * Handles routing for the app + */ export class Router { constructor() { + /** + * @property {HTMLDivElement} _eventSource Arbitrary DIV used to generate events + */ this._eventSource = document.createElement("div"); + /** + * @property {CustomEvent} _routeChanged Custom event raised when the route changes + */ this._routeChanged = new CustomEvent("routechanged", { bubbles: true, cancelable: false }); + /** + * @property {string} _route The current route + */ this._route = null; window.addEventListener("popstate", () => { if (this.getRoute() !== this._route) { @@ -15,15 +29,27 @@ export class Router { }); } + /** + * Get the event source + * @returns {HTMLDivElement} The event source div + */ get eventSource() { return this._eventSource; } - + + /** + * Set the current route + * @param {string} route The route name + */ setRoute(route) { window.location.hash = route; this._route = route; } + /** + * Get the current route + * @returns {string} The current route name + */ getRoute() { return window.location.hash.substr(1).replace(/\//ig, "/"); } diff --git a/js/slide.js b/js/slide.js index 74260e3..42e7299 100644 --- a/js/slide.js +++ b/js/slide.js @@ -1,38 +1,65 @@ +// @ts-check +/** Represents a slide */ export class Slide { + /** + * @constructor + * @param {string} text - The content of the slide + */ constructor(text) { + /** @property {string} _text - internal text representation */ this._text = text; + /** @property {HTMLDivElement} _html - host div */ this._html = document.createElement('div'); this._html.innerHTML = text; + /** @property {string} _title - title of the slide */ this._title = this._html.querySelectorAll("title")[0].innerText; - const transition = this._html.querySelectorAll("transition"); + /** @type{NodeListOf} */ + const transition = (this._html.querySelectorAll("transition")); if (transition.length) { this._transition = transition[0].innerText; } else { this._transition = null; } + /** @type{NodeListOf} */ const hasNext = this._html.querySelectorAll("nextslide"); if (hasNext.length > 0) { this._nextSlideName = hasNext[0].innerText; - } + } else { this._nextSlideName = null; - } + } } + /** + * The slide transition + * @return{string} The transition name + */ get transition() { return this._transition; } + /** + * The slide title + * @return{string} The slide title + */ get title() { return this._title; } + /** + * The HTML DOM node for the slide + * @return{HTMLDivElement} The HTML content + */ get html() { return this._html; } + /** + * The name of the next slide (filename without the .html extension) + * @return{string} The name of the next slide + */ get nextSlide() { return this._nextSlideName; } diff --git a/js/slideLoader.js b/js/slideLoader.js index 4dfc08e..9773499 100644 --- a/js/slideLoader.js +++ b/js/slideLoader.js @@ -1,11 +1,22 @@ +//@ts-check import { Slide } from "./slide.js" +/** + * Load a single slide + * @param {string} slideName The name of the slide + * @returns {Promise} The slide + */ async function loadSlide(slideName) { const response = await fetch(`./slides/${slideName}.html`); const slide = await response.text(); return new Slide(slide); } +/** + * + * @param {string} start The name of the slide to begin with + * @returns {Promise} The array of loaded slides + */ export async function loadSlides(start) { var next = start; const slides = []; @@ -20,6 +31,6 @@ export async function loadSlides(start) { else { break; } - } + } return slides; } \ No newline at end of file diff --git a/slides/manifest.json b/slides/manifest.json deleted file mode 100644 index 4c9ac8c..0000000 --- a/slides/manifest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Vanilla.js: Modern 1st Party JavaScript", - "start": "001-title" -} \ No newline at end of file