Add controls and SPA routing

This commit is contained in:
Jeremy Likness 2019-11-22 16:37:45 -08:00
parent 3ef6273404
commit d66f8c5db4
10 changed files with 185 additions and 50 deletions

View File

@ -20,28 +20,39 @@ img {
height: auto;
}
div#main {
.footer {
float: left;
height: 15vh;
font-size: 1.5em;
vertical-align: middle;
width: 32vw;
}
.right {
text-align: right;
}
.left {
text-align: left;
}
.center {
text-align: center;
}
slide-deck div {
height: 80vh;
padding: 1em;
background: lightgray;
}
div#footer-left {
vertical-align: middle;
slide-controls {
height: 15vh;
width: 33vw;
text-align: center;
float: left;
}
div#footer-left img {
display: inline-block;
}
div#footer-right {
font-size: 2em;
height: 15vh;
float: right;
}
nextslide {
display: none;
}

View File

@ -15,14 +15,15 @@
<body>
<script type="module" src="./js/app.js"></script>
<div id="main">
<h1>DevNexus</h1>
<h2>We are getting things ready...</h2>
</div>
<div id="footer-left">
<slide-deck id="main" start="001-title">
<h1>DevNexus | Vanilla.js: Modern 1st Party JavaScript</h1>
<h2>Setting things up ...</h2>
</slide-deck>
<div class="footer left">
<img src="./images/devnexus.png" alt="DevNexus logo" title="DevNexus logo" />
</div>
<div id="footer-right">
<slide-controls deck="main" class="footer center">---</slide-controls>
<div class="footer right">
<a href="https://twitter.com/JeremyLikness">@JeremyLikness</a>
</div>
</body>

View File

@ -1,23 +1,10 @@
import { getJson } from "./jsonLoader.js"
import { loadSlides } from "./slideLoader.js"
import { Navigator } from "./navigator.js"
const state = {
manifest: {}
};
import { registerDeck } from "./navigator.js"
import { registerControls } from "./controls.js"
const app = async () => {
state.deck = document.getElementById("main");
// load the manifest
state.manifest = await getJson("slides/manifest.json");
// load the slides
state.slides = await loadSlides(state.manifest.start);
// initialize the navigation
state.navigator = new Navigator(state.slides, state.deck);
registerDeck();
registerControls();
};
document.addEventListener("DOMContentLoaded", app);

54
js/controls.js vendored Normal file
View File

@ -0,0 +1,54 @@
class Controls extends HTMLElement {
constructor() {
super();
this._controlRef = null;
this._deck = null;
}
async connectedCallback() {
const response = await fetch("/templates/controls.html");
const template = await response.text();
this.innerHTML = "";
const host = document.createElement("div");
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")
};
this._controlRef.first.addEventListener("click", () => this._deck.jumpTo(0));
this._controlRef.prev.addEventListener("click", () => this._deck.previous());
this._controlRef.next.addEventListener("click", () => this._deck.next());
this._controlRef.last.addEventListener("click", () => this._deck.jumpTo(this._deck.totalSlides - 1));
this.refreshState();
}
static get observedAttributes() {
return ["deck"];
}
async attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "deck") {
if (oldVal !== newVal) {
this._deck = document.getElementById(newVal);
this._deck.addEventListener("slideschanged", () => this.refreshState());
}
}
}
refreshState() {
const next = this._deck.hasNext;
const prev = this._deck.hasPrevious;
this._controlRef.first.disabled = !prev;
this._controlRef.prev.disabled = !prev;
this._controlRef.next.disabled = !next;
this._controlRef.last.disabled = !next;
this._controlRef.pos.innerText = `${this._deck.currentIndex + 1} / ${this._deck.totalSlides}`;
}
}
export const registerControls = () => customElements.define('slide-controls', Controls);

View File

@ -1,9 +1,43 @@
export class Navigator {
import { loadSlides } from "./slideLoader.js"
import { Router } from "./router.js"
constructor(slides, deck) {
this._deck = deck;
this._slides = slides;
this.jumpTo(0);
class Navigator extends HTMLElement {
constructor() {
super();
this._router = new Router();
this._route = this._router.getRoute();
this.slidesChangedEvent = new CustomEvent("slideschanged", {
bubbles: true,
cancelable: false
});
this._router.eventSource.addEventListener("routechanged", () => {
if (this._route !== this._router.getRoute()) {
this._route = this._router.getRoute();
if (this._route) {
var slide = parseInt(this._route) - 1;
this.jumpTo(slide);
}
}
});
}
static get observedAttributes() {
return ["start"];
}
async attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "start") {
if (oldVal !== newVal) {
this._slides = await loadSlides(newVal);
this._route = this._router.getRoute();
var slide = 0;
if (this._route) {
slide = parseInt(this._route) - 1;
}
this.jumpTo(slide);
}
}
}
get currentIndex() {
@ -11,11 +45,11 @@ export class Navigator {
}
get currentSlide() {
return this._slides[this._currentIndex];
return this._slides ? this._slides[this._currentIndex] : null;
}
get totalSlides() {
return this._slides.length;
return this._slides ? this._slides.length : 0;
}
get hasPrevious() {
@ -29,8 +63,11 @@ export class Navigator {
jumpTo(slideIdx) {
if (slideIdx >= 0 && slideIdx < this.totalSlides) {
this._currentIndex = slideIdx;
this._deck.innerHTML = '';
this._deck.appendChild(this.currentSlide.html);
this.innerHTML = '';
this.appendChild(this.currentSlide.html);
this._router.setRoute(slideIdx+1);
this._route = this._router.getRoute();
this.dispatchEvent(this.slidesChangedEvent);
}
}
@ -45,4 +82,6 @@ export class Navigator {
this.jumpTo(this.currentIndex - 1);
}
}
}
}
export const registerDeck = () => customElements.define('slide-deck', Navigator);

30
js/router.js Normal file
View File

@ -0,0 +1,30 @@
export class Router {
constructor() {
this._eventSource = document.createElement("div");
this._routeChanged = new CustomEvent("routechanged", {
bubbles: true,
cancelable: false
});
this._route = null;
window.addEventListener("popstate", () => {
if (this.getRoute() !== this._route) {
this._route = this.getRoute();
this._eventSource.dispatchEvent(this._routeChanged);
}
});
}
get eventSource() {
return this._eventSource;
}
setRoute(route) {
window.location.hash = route;
this._route = route;
}
getRoute() {
return window.location.hash.substr(1).replace(/\//ig, "/");
}
}

View File

@ -11,10 +11,10 @@ export async function loadSlides(start) {
const slides = [];
const cycle = {};
while (next) {
const nextSlide = await loadSlide(next);
if (!cycle[nextSlide.title]) {
if (!cycle[next]) {
cycle[next] = true;
const nextSlide = await loadSlide(next);
slides.push(nextSlide);
cycle[nextSlide.title] = nextSlide;
next = nextSlide.nextSlide;
}
else {

View File

@ -1,2 +1,3 @@
<title>More Stuff</title>
<h1>More Stuff</h1>
<h1>More Stuff</h1>
<nextslide>003-morestuff</nextslide>

View File

@ -0,0 +1,7 @@
<title>More Stuff</title>
<h1>This is even more stuff</h1>
<p>Lorem jalskdjflkasdjflkasd jkjdsaflkdsj kldsjflksdjdlk klsdjflkasjlksj kjdsf
asdklfjlkfjk laskdjfladskjf
asldfkjlkddj
asldfjlkfjjkjlff
</p>

5
templates/controls.html Normal file
View File

@ -0,0 +1,5 @@
<button id="ctrlFirst"></button>
<button id="ctrlPrevious"></button>
<span id="position">?/?</span>
<button id="ctrlNext"></button>
<button id="ctrlLast"></button>