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

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 {
/**
* 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 = () => {

View File

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

56
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() {
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);

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() {
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);

View File

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

View File

@ -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, "/");
}

View File

@ -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<HTMLElement>} */
const transition = (this._html.querySelectorAll("transition"));
if (transition.length) {
this._transition = transition[0].innerText;
}
else {
this._transition = null;
}
/** @type{NodeListOf<HTMLElement>} */
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;
}

View File

@ -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<Slide>} 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<Slide[]>} 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;
}

View File

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