Add JSDoc comments

This commit is contained in:
Jeremy Likness 2019-11-23 16:02:31 -08:00
parent 5b0c9fbc15
commit 5daad9fcaa
11 changed files with 258 additions and 38 deletions

View File

@ -1,11 +1,19 @@
@keyframes slide-left { @keyframes slide-left {
from { margin-left: 0vw; } from {
to { margin-left: -100vw; } margin-left: 0vw;
}
to {
margin-left: -100vw;
}
} }
@keyframes enter-right { @keyframes enter-right {
from { margin-left: 100vw; } from {
to { margin-left: 0vw; } margin-left: 100vw;
}
to {
margin-left: 0vw;
}
} }
.anim-slide-left-begin { .anim-slide-left-begin {
@ -21,8 +29,12 @@
} }
@keyframes spin { @keyframes spin {
from { transform: rotate(0deg) } from {
to { transform: rotate(360deg) } transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
} }
.anim-spin { .anim-spin {

View File

@ -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 { export class Animator {
/**
* Create an instance of the animation helper
*/
constructor() { constructor() {
this._transitioning = false; this._transitioning = false;
this._begin = null; this._begin = null;
this._end = null; this._end = null;
} }
/**
* True when animation in effect
* @returns {boolean}
*/
get transitioning() { get transitioning() {
return this._transitioning; return this._transitioning;
} }
/**
* True when ready to complete second part of animation
* @returns {boolean}
*/
get animationReady() { get animationReady() {
return !!this._end; 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) { beginAnimation(animationName, host, callback) {
this._transitioning = true; this._transitioning = true;
this._begin = `anim-${animationName}-begin`; this._begin = `anim-${animationName}-begin`;
@ -27,6 +52,10 @@ export class Animator {
host.classList.add(this._begin); host.classList.add(this._begin);
} }
/**
* Kicks off the final animation (new slide)
* @param {HTMLDivElement} host The div to animate
*/
endAnimation(host) { endAnimation(host) {
this._transitioning = true; this._transitioning = true;
const animationEnd = () => { const animationEnd = () => {

View File

@ -2,6 +2,9 @@ import { registerDeck } from "./navigator.js"
import { registerControls } from "./controls.js" import { registerControls } from "./controls.js"
import { registerKeyHandler } from "./keyhandler.js" import { registerKeyHandler } from "./keyhandler.js"
/**
* Main application element, simply registers the web components
*/
const app = async () => { const app = async () => {
registerDeck(); registerDeck();
registerControls(); registerControls();
@ -9,4 +12,3 @@ const app = async () => {
}; };
document.addEventListener("DOMContentLoaded", app); document.addEventListener("DOMContentLoaded", app);

52
js/controls.js vendored
View File

@ -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() { constructor() {
super(); super();
/** @type {CustomRef} */
this._controlRef = null; this._controlRef = null;
/** @type {Navigator} */
this._deck = null; 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() { async connectedCallback() {
const response = await fetch("/templates/controls.html"); const response = await fetch("/templates/controls.html");
const template = await response.text(); const template = await response.text();
@ -14,11 +38,11 @@ class Controls extends HTMLElement {
host.innerHTML = template; host.innerHTML = template;
this.appendChild(host); this.appendChild(host);
this._controlRef = { this._controlRef = {
first: document.getElementById("ctrlFirst"), first: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlFirst")),
prev: document.getElementById("ctrlPrevious"), prev: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlPrevious")),
next: document.getElementById("ctrlNext"), next: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlNext")),
last: document.getElementById("ctrlLast"), last: /** @type {HTMLButtonElement} **/(document.getElementById("ctrlLast")),
pos: document.getElementById("position") pos: /** @type {HTMLSpanElement} **/(document.getElementById("position"))
}; };
this._controlRef.first.addEventListener("click", () => this._deck.jumpTo(0)); this._controlRef.first.addEventListener("click", () => this._deck.jumpTo(0));
this._controlRef.prev.addEventListener("click", () => this._deck.previous()); this._controlRef.prev.addEventListener("click", () => this._deck.previous());
@ -27,19 +51,32 @@ class Controls extends HTMLElement {
this.refreshState(); this.refreshState();
} }
/**
* Get the list of attributes to watch
* @returns {string[]}
*/
static get observedAttributes() { static get observedAttributes() {
return ["deck"]; 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) { async attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "deck") { if (attrName === "deck") {
if (oldVal !== newVal) { if (oldVal !== newVal) {
this._deck = document.getElementById(newVal); this._deck = /** @type {Navigator} */(document.getElementById(newVal));
this._deck.addEventListener("slideschanged", () => this.refreshState()); this._deck.addEventListener("slideschanged", () => this.refreshState());
} }
} }
} }
/**
* Enables/disables buttons and updates position based on index in the deck
*/
refreshState() { refreshState() {
const next = this._deck.hasNext; const next = this._deck.hasNext;
const prev = this._deck.hasPrevious; const prev = this._deck.hasPrevious;
@ -51,4 +88,5 @@ class Controls extends HTMLElement {
} }
} }
/** Register the custom slide-controls element */
export const registerControls = () => customElements.define('slide-controls', Controls); export const registerControls = () => customElements.define('slide-controls', Controls);

View File

@ -1,4 +0,0 @@
export async function getJson(path) {
const response = await fetch(path);
return await response.json();
};

View File

@ -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() { constructor() {
super(); super();
/**
* The related Navigator element
* @type {Navigator}
*/
this._deck = null; this._deck = null;
} }
/**
* Gets the attributes being watched
* @returns {string[]} The attributes to watch
*/
static get observedAttributes() { static get observedAttributes() {
return ["deck"]; 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) { async attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "deck") { if (attrName === "deck") {
if (oldVal !== newVal) { if (oldVal !== newVal) {
this._deck = document.getElementById(newVal); this._deck = /** @type {Navigator} */(document.getElementById(newVal));
this._deck.parentElement.addEventListener("keydown", key => { this._deck.parentElement.addEventListener("keydown", key => {
if (key.keyCode == 39 || key.keyCode == 32) { if (key.keyCode == 39 || key.keyCode == 32) {
this._deck.next(); 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); export const registerKeyHandler = () => customElements.define('key-handler', KeyHandler);

View File

@ -1,9 +1,22 @@
// @ts-check
import { loadSlides } from "./slideLoader.js" import { loadSlides } from "./slideLoader.js"
import { Slide } from "./slide.js"
import { Router } from "./router.js" import { Router } from "./router.js"
import { Animator } from "./animator.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() { constructor() {
super(); super();
this._animator = new Animator(); this._animator = new Animator();
@ -24,10 +37,20 @@ class Navigator extends HTMLElement {
}); });
} }
/**
* Get the list of observed attributes
* @returns {string[]}
*/
static get observedAttributes() { static get observedAttributes() {
return ["start"]; return ["start"];
} }
/**
* Called when an attribute changes
* @param {string} attrName
* @param {string} oldVal
* @param {string} newVal
*/
async attributeChangedCallback(attrName, oldVal, newVal) { async attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "start") { if (attrName === "start") {
if (oldVal !== newVal) { if (oldVal !== newVal) {
@ -43,26 +66,50 @@ class Navigator extends HTMLElement {
} }
} }
/**
* Current slide index
* @returns {number}
*/
get currentIndex() { get currentIndex() {
return this._currentIndex; return this._currentIndex;
} }
/**
* Current slide
* @returns {Slide}
*/
get currentSlide() { get currentSlide() {
return this._slides ? this._slides[this._currentIndex] : null; return this._slides ? this._slides[this._currentIndex] : null;
} }
/**
* Total number of slides
* @returns {number}
*/
get totalSlides() { get totalSlides() {
return this._slides ? this._slides.length : 0; return this._slides ? this._slides.length : 0;
} }
/**
* True if a previous slide exists
* @returns {boolean}
*/
get hasPrevious() { get hasPrevious() {
return this._currentIndex > 0; return this._currentIndex > 0;
} }
/**
* True if a next slide exists
* @returns {boolean}
*/
get hasNext() { get hasNext() {
return this._currentIndex < (this.totalSlides - 1); return this._currentIndex < (this.totalSlides - 1);
} }
/**
* Main slide navigation: jump to specific slide
* @param {number} slideIdx
*/
jumpTo(slideIdx) { jumpTo(slideIdx) {
if (this._animator.transitioning) { if (this._animator.transitioning) {
return; return;
@ -71,9 +118,9 @@ class Navigator extends HTMLElement {
this._currentIndex = slideIdx; this._currentIndex = slideIdx;
this.innerHTML = ''; this.innerHTML = '';
this.appendChild(this.currentSlide.html); this.appendChild(this.currentSlide.html);
this._router.setRoute(slideIdx+1); this._router.setRoute((slideIdx + 1).toString());
this._route = this._router.getRoute(); 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); this.dispatchEvent(this.slidesChangedEvent);
if (this._animator.animationReady) { if (this._animator.animationReady) {
this._animator.endAnimation(this.querySelector("div")); 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() { next() {
if (this.hasNext) { if (this.hasNext) {
if (this.currentSlide.transition !== null) { if (this.currentSlide.transition !== null) {
this._animator.beginAnimation( this._animator.beginAnimation(
this.currentSlide.transition, this.currentSlide.transition,
this.querySelector("div"), this.querySelector("div"),
() => this.jumpTo(this.currentIndex+1)); () => this.jumpTo(this.currentIndex + 1));
} }
else { else {
this.jumpTo(this.currentIndex + 1); this.jumpTo(this.currentIndex + 1);
@ -95,6 +145,9 @@ class Navigator extends HTMLElement {
} }
} }
/**
* Move to previous slide, if it exists
*/
previous() { previous() {
if (this.hasPrevious) { if (this.hasPrevious) {
this.jumpTo(this.currentIndex - 1); 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); export const registerDeck = () => customElements.define('slide-deck', Navigator);

View File

@ -1,11 +1,25 @@
// @ts-check
/**
* Handles routing for the app
*/
export class Router { export class Router {
constructor() { constructor() {
/**
* @property {HTMLDivElement} _eventSource Arbitrary DIV used to generate events
*/
this._eventSource = document.createElement("div"); this._eventSource = document.createElement("div");
/**
* @property {CustomEvent} _routeChanged Custom event raised when the route changes
*/
this._routeChanged = new CustomEvent("routechanged", { this._routeChanged = new CustomEvent("routechanged", {
bubbles: true, bubbles: true,
cancelable: false cancelable: false
}); });
/**
* @property {string} _route The current route
*/
this._route = null; this._route = null;
window.addEventListener("popstate", () => { window.addEventListener("popstate", () => {
if (this.getRoute() !== this._route) { if (this.getRoute() !== this._route) {
@ -15,15 +29,27 @@ export class Router {
}); });
} }
/**
* Get the event source
* @returns {HTMLDivElement} The event source div
*/
get eventSource() { get eventSource() {
return this._eventSource; return this._eventSource;
} }
/**
* Set the current route
* @param {string} route The route name
*/
setRoute(route) { setRoute(route) {
window.location.hash = route; window.location.hash = route;
this._route = route; this._route = route;
} }
/**
* Get the current route
* @returns {string} The current route name
*/
getRoute() { getRoute() {
return window.location.hash.substr(1).replace(/\//ig, "/"); return window.location.hash.substr(1).replace(/\//ig, "/");
} }

View File

@ -1,17 +1,28 @@
// @ts-check
/** Represents a slide */
export class Slide { export class Slide {
/**
* @constructor
* @param {string} text - The content of the slide
*/
constructor(text) { constructor(text) {
/** @property {string} _text - internal text representation */
this._text = text; this._text = text;
/** @property {HTMLDivElement} _html - host div */
this._html = document.createElement('div'); this._html = document.createElement('div');
this._html.innerHTML = text; this._html.innerHTML = text;
/** @property {string} _title - title of the slide */
this._title = this._html.querySelectorAll("title")[0].innerText; this._title = this._html.querySelectorAll("title")[0].innerText;
const transition = this._html.querySelectorAll("transition"); /** @type{NodeListOf<HTMLElement>} */
const transition = (this._html.querySelectorAll("transition"));
if (transition.length) { if (transition.length) {
this._transition = transition[0].innerText; this._transition = transition[0].innerText;
} }
else { else {
this._transition = null; this._transition = null;
} }
/** @type{NodeListOf<HTMLElement>} */
const hasNext = this._html.querySelectorAll("nextslide"); const hasNext = this._html.querySelectorAll("nextslide");
if (hasNext.length > 0) { if (hasNext.length > 0) {
this._nextSlideName = hasNext[0].innerText; this._nextSlideName = hasNext[0].innerText;
@ -21,18 +32,34 @@ export class Slide {
} }
} }
/**
* The slide transition
* @return{string} The transition name
*/
get transition() { get transition() {
return this._transition; return this._transition;
} }
/**
* The slide title
* @return{string} The slide title
*/
get title() { get title() {
return this._title; return this._title;
} }
/**
* The HTML DOM node for the slide
* @return{HTMLDivElement} The HTML content
*/
get html() { get html() {
return this._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() { get nextSlide() {
return this._nextSlideName; return this._nextSlideName;
} }

View File

@ -1,11 +1,22 @@
//@ts-check
import { Slide } from "./slide.js" import { Slide } from "./slide.js"
/**
* Load a single slide
* @param {string} slideName The name of the slide
* @returns {Promise<Slide>} The slide
*/
async function loadSlide(slideName) { async function loadSlide(slideName) {
const response = await fetch(`./slides/${slideName}.html`); const response = await fetch(`./slides/${slideName}.html`);
const slide = await response.text(); const slide = await response.text();
return new Slide(slide); return new Slide(slide);
} }
/**
*
* @param {string} start The name of the slide to begin with
* @returns {Promise<Slide[]>} The array of loaded slides
*/
export async function loadSlides(start) { export async function loadSlides(start) {
var next = start; var next = start;
const slides = []; const slides = [];

View File

@ -1,4 +0,0 @@
{
"title": "Vanilla.js: Modern 1st Party JavaScript",
"start": "001-title"
}