diff --git a/lib/big.css b/lib/big.css index 3585255..6f784b9 100644 --- a/lib/big.css +++ b/lib/big.css @@ -92,7 +92,7 @@ div.imageText { } /* normally not good, but ok in context of full screen directional navigation */ -:focus { +.talk-mode .slide:focus { outline: 0; } diff --git a/lib/big.js b/lib/big.js index 9fbdf51..40ef8fc 100644 --- a/lib/big.js +++ b/lib/big.js @@ -1,21 +1,21 @@ /* @flow weak */ -window.addEventListener('load', function() { +window.addEventListener("load", function() { // document.body isn't a guaranteed value: Flow will yell at // us if we use it without checking if it exists, so let's check it here. if (!document.body) { throw new Error( - 'big could not find a body element on this page, so it is exiting' + "big could not find a body element on this page, so it is exiting" ); } var body = document.body; var initialBodyClass = body.className; - var slideDivs = nodeListToArray(document.querySelectorAll('body > div')); + var slideDivs = nodeListToArray(document.querySelectorAll("body > div")); if (!slideDivs.length) { throw new Error( "big couldn't find any slides in this presentation: " + - 'there are no divs directly within the body of this page' + "there are no divs directly within the body of this page" ); } @@ -26,7 +26,7 @@ window.addEventListener('load', function() { // get rid of the elements themselves so that they don't interfere with // rendering. var notes = slideDivs.map(function(slide) { - return nodeListToArray(slide.getElementsByTagName('notes')).map(function( + return nodeListToArray(slide.getElementsByTagName("notes")).map(function( noteElement ) { noteElement.parentNode.removeChild(noteElement); @@ -49,7 +49,7 @@ window.addEventListener('load', function() { * The current mode, one of 'talk', 'print', or 'jump' * @public */ - mode: 'talk', + mode: "talk", /** * Move one slide forward * @function @@ -79,42 +79,43 @@ window.addEventListener('load', function() { }; var presentationContainer = body.appendChild( - ce('div', 'presentation-container') + ce("div", "presentation-container") ); - slideDivs = slideDivs.map(function(slide) { - slide.setAttribute('tabindex', 0); - slide.classList.add('slide'); + slideDivs = slideDivs.map(function(slide, i) { + slide.__i__ = i; + slide.setAttribute("tabindex", 0); + slide.classList.add("slide"); var slideContainer = presentationContainer.appendChild( - ce('div', 'slide-container') + ce("div", "slide-container") ); slideContainer.appendChild(slide); return slideContainer; }); - body.className = 'talk-mode ' + initialBodyClass; + body.className = "talk-mode " + initialBodyClass; if (big.audio) { - big.playControl = body.appendChild(ce('div')); - big.playControl.style.cssText = 'padding:5px;color:#aaa;'; - big.playControl.addEventListener('click', onClickPlay); + big.playControl = body.appendChild(ce("div")); + big.playControl.style.cssText = "padding:5px;color:#aaa;"; + big.playControl.addEventListener("click", onClickPlay); window.setInterval(onAudioUpdate, 200); } - var printListener = window.matchMedia('print'); + var printListener = window.matchMedia("print"); printListener.addListener(onPrint); - document.addEventListener('click', onClick); - document.addEventListener('keydown', onKeyDown); - document.addEventListener('touchstart', onTouchStart); - window.addEventListener('hashchange', onHashChange); - window.addEventListener('resize', onResize); + document.addEventListener("click", onClick); + document.addEventListener("keydown", onKeyDown); + document.addEventListener("touchstart", onTouchStart); + window.addEventListener("hashchange", onHashChange); + window.addEventListener("resize", onResize); window.big = big; console.log( - 'This is a big presentation. You can: \n\n' + - '* press j to jump to a slide\n' + - '* press p to see the print view\n' + - '* press t to go back to the talk view' + "This is a big presentation. You can: \n\n" + + "* press j to jump to a slide\n" + + "* press p to see the print view\n" + + "* press t to go back to the talk view" ); go(parseHash() || big.current); @@ -159,21 +160,20 @@ window.addEventListener('load', function() { * @returns {HTMLElement?} an audio node */ function findAudioNode() { - return nodeListToArray( - document.getElementsByTagName('audio') - ).filter(function(audio) { - return ( - audio.textTracks.length === 1 && audio.textTracks[0].cues.length > 0 - ); - })[0]; + return nodeListToArray(document.getElementsByTagName("audio")).filter( + function(audio) { + return ( + audio.textTracks.length === 1 && audio.textTracks[0].cues.length > 0 + ); + } + )[0]; } // Navigation ================================================================ function goToAudio(n) { if (!big.audio || !big.playControl) return; - big.playControl.style.cssText = n === 0 - ? 'display:none' - : 'padding:5px;color:#aaa;'; + big.playControl.style.cssText = + n === 0 ? "display:none" : "padding:5px;color:#aaa;"; if (n === 0) { big.audio.pause(); } else { @@ -187,12 +187,12 @@ window.addEventListener('load', function() { * print them to the console. */ function printNotesToConsole(n) { - if (notes[n].length && 'group' in console) { + if (notes[n].length && "group" in console) { console.group(n); notes[n].forEach(function(note) { console.log( - '%c%s', - 'padding:5px;font-family:serif;font-size:18px;line-height:150%;', + "%c%s", + "padding:5px;font-family:serif;font-size:18px;line-height:150%;", note ); }); @@ -202,12 +202,12 @@ window.addEventListener('load', function() { function useDataImageAsBackground(slideContainer) { var slideDiv = slideContainer.firstChild; - if (slideDiv.hasAttribute('data-background-image')) { + if (slideDiv.hasAttribute("data-background-image")) { slideContainer.style.backgroundImage = - 'url("' + slideDiv.getAttribute('data-background-image') + '")'; - slideDiv.classList.add('imageText'); + 'url("' + slideDiv.getAttribute("data-background-image") + '")'; + slideDiv.classList.add("imageText"); } else { - slideContainer.style.backgroundImage = ''; + slideContainer.style.backgroundImage = ""; slideContainer.style.backgroundColor = slideDiv.style.backgroundColor; } } @@ -236,11 +236,14 @@ window.addEventListener('load', function() { } slideDivs.forEach(function(slide, i) { - slide.style.display = i === n ? 'flex' : 'none'; + slide.style.display = i === n ? "flex" : "none"; }); body.className = - 'talk-mode ' + (slideDiv.getAttribute('data-bodyclass') || '') + ' ' + initialBodyClass; + "talk-mode " + + (slideDiv.getAttribute("data-bodyclass") || "") + + " " + + initialBodyClass; useDataImageAsBackground(slideContainer); @@ -253,18 +256,18 @@ window.addEventListener('load', function() { // If this slide has a time-to-next data attribute, set a // timer that advances to the next slide within that many // seconds. - if (slideDiv.hasAttribute('data-time-to-next')) { + if (slideDiv.hasAttribute("data-time-to-next")) { if (big.audio) { throw new Error( - 'this presentation uses an audio track, and also uses time-to-next. ' + - 'You must only use one or the other.' + "this presentation uses an audio track, and also uses time-to-next. " + + "You must only use one or the other." ); } - var timeToNextStr = slideDiv.getAttribute('data-time-to-next'); + var timeToNextStr = slideDiv.getAttribute("data-time-to-next"); var timeToNext = parseFloat(timeToNextStr); if (isNaN(timeToNext)) { throw new Error( - 'big encountered a bad value for the time-to-next string: ' + + "big encountered a bad value for the time-to-next string: " + timeToNextStr ); } @@ -300,11 +303,11 @@ window.addEventListener('load', function() { function resizeTo(slideContainer, width, height) { var slideDiv = slideContainer.firstChild; var fontSize = height; - slideContainer.style.width = width + 'px'; - slideContainer.style.height = height + 'px'; + slideContainer.style.width = width + "px"; + slideContainer.style.height = height + "px"; [100, 50, 10, 2].forEach(function(step) { for (; fontSize > 0; fontSize -= step) { - slideDiv.style.fontSize = fontSize + 'px'; + slideDiv.style.fontSize = fontSize + "px"; if (slideDiv.offsetWidth <= width && slideDiv.offsetHeight <= height) { break; } @@ -322,112 +325,170 @@ window.addEventListener('load', function() { // Event listeners =========================================================== function onPrint() { - if (big.mode === 'print') return; - body.className = 'print-mode ' + initialBodyClass; + if (big.mode === "print") return; + body.className = "print-mode " + initialBodyClass; emptyNode(presentationContainer); slideDivs.forEach(function(slideContainer, i) { var subContainer = presentationContainer.appendChild( - ce('div', 'sub-container') + ce("div", "sub-container") ); var slideBodyContainer = subContainer.appendChild( - ce('div', slideContainer.firstChild.getAttribute('data-bodyclass') || '') + ce( + "div", + slideContainer.firstChild.getAttribute("data-bodyclass") || "" + ) ); slideBodyContainer.appendChild(slideContainer); - slideContainer.style.display = 'flex'; + slideContainer.style.display = "flex"; useDataImageAsBackground(slideContainer); resizeTo(slideContainer, 512, 320); if (notes[i].length) { - var notesUl = subContainer.appendChild(ce('ul', 'notes-list')); + var notesUl = subContainer.appendChild(ce("ul", "notes-list")); notes[i].forEach(function(note) { - var li = notesUl.appendChild(ce('li')); + var li = notesUl.appendChild(ce("li")); li.innerText = note; }); } }); - big.mode = 'print'; + big.mode = "print"; } function onTalk(i) { - if (big.mode === 'talk') return; - big.mode = 'talk'; - body.className = 'talk-mode ' + initialBodyClass; + if (big.mode === "talk") return; + big.mode = "talk"; + body.className = "talk-mode " + initialBodyClass; emptyNode(presentationContainer); slideDivs.forEach(function(slideContainer) { presentationContainer.appendChild(slideContainer); }); var goTo = big.current; - if (typeof i === 'number') { + if (typeof i === "number") { goTo = i; } go(goTo, false, true); } function onJump() { - if (big.mode === 'jump') return; - big.mode = 'jump'; - body.className = 'jump-mode ' + initialBodyClass; + if (big.mode === "jump") return; + big.mode = "jump"; + body.className = "jump-mode " + initialBodyClass; emptyNode(presentationContainer); slideDivs.forEach(function(slideContainer, i) { var subContainer = presentationContainer.appendChild( - ce('div', 'sub-container') + ce("div", "sub-container") ); + subContainer.addEventListener("keypress", function(e) { + if (e.key !== "Enter") return; + subContainer.removeEventListener("click", onClickSlide); + e.stopPropagation(); + e.preventDefault(); + onTalk(i); + }); var slideBodyContainer = subContainer.appendChild( - ce('div', slideContainer.firstChild.getAttribute('data-bodyclass') || '') + ce( + "div", + slideContainer.firstChild.getAttribute("data-bodyclass") || "" + ) ); slideBodyContainer.appendChild(slideContainer); - slideContainer.style.display = 'flex'; + slideContainer.style.display = "flex"; useDataImageAsBackground(slideContainer); resizeTo(slideContainer, 192, 120); function onClickSlide(e) { - subContainer.removeEventListener('click', onClickSlide); + subContainer.removeEventListener("click", onClickSlide); e.stopPropagation(); e.preventDefault(); onTalk(i); } - subContainer.addEventListener('click', onClickSlide); + subContainer.addEventListener("click", onClickSlide); }); } function onClick(e) { - if (big.mode !== 'talk') return; - if (e.target.tagName !== 'A') go((big.current + 1) % big.length); + if (big.mode !== "talk") return; + if (e.target.tagName !== "A") go((big.current + 1) % big.length); } function onKeyDown(e) { + if (big.mode === "talk") { + switch (e.key) { + case "ArrowLeft": + case "ArrowUp": + case "PageUp": + return reverse(); + case "ArrowRight": + case "ArrowDown": + case "PageDown": + return forward(); + } + } switch (e.key) { - case "ArrowLeft": - case "ArrowUp": - case "PageUp": - if (big.mode === "talk") reverse(); - break; - case "ArrowRight": - case "ArrowDown": - case "PageDown": - if (big.mode === "talk") forward(); - break; - case "p": - onPrint(); - break; - case "t": - onTalk(); - break; - case "j": - onJump(); - break; + case "p": + onPrint(); + break; + case "t": + onTalk(); + break; + case "j": + onJump(); + break; + } + if (big.mode === "jump") { + var activeElement = document.activeElement; + if (activeElement && activeElement.classList.contains("slide")) { + var column = []; + var startIndex = slideDivs.indexOf(activeElement.parentNode); + var columnIndexes = getColumnIndexes(activeElement); + jumpFocus( + e, + { + ArrowLeft: startIndex - 1, + ArrowRight: startIndex + 1, + ArrowDown: columnIndexes.next, + ArrowUp: columnIndexes.prev + }[e.key] + ); + } else if (e.key.indexOf("Arrow") === 0) { + jumpFocus(e, 0); + } + } + } + + function getColumnIndexes(activeElement) { + var left = activeElement.getBoundingClientRect().left; + var lastIndex; + var foundSelf = false; + for (var i = 0; i < slideDivs.length; i++) { + if (slideDivs[i].firstChild.getBoundingClientRect().left === left) { + if (foundSelf) return { prev: lastIndex, next: i }; + if (slideDivs[i] === activeElement.parentNode) { + foundSelf = true; + } else { + lastIndex = i; + } + } + } + return { prev: lastIndex }; + } + + function jumpFocus(e, i) { + if (typeof i === "number" && slideDivs[i]) { + e.preventDefault(); + slideDivs[i].firstChild.focus(); } } function onTouchStart(e) { - if (big.mode !== 'talk') return; + if (big.mode !== "talk") return; // When the 'once' option to addEventListener is available, we'll be able to // simplify this code, but for now we manually remove the End listener // after it is invoked once var startingPageX = e.changedTouches[0].pageX; - document.addEventListener('touchend', onTouchEnd); + document.addEventListener("touchend", onTouchEnd); function onTouchEnd(e2) { - document.removeEventListener('touchend', onTouchEnd); + document.removeEventListener("touchend", onTouchEnd); var distanceTraveled = e2.changedTouches[0].pageX - startingPageX; // Don't navigate if the person didn't swipe by fewer than 4 pixels if (Math.abs(distanceTraveled) < 4) return; @@ -445,7 +506,7 @@ window.addEventListener('load', function() { function onAudioUpdate() { if (!big.playControl) return; - big.playControl.innerHTML = big.audio.paused ? '▶' : '⏸'; + big.playControl.innerHTML = big.audio.paused ? "▶" : "⏸"; if (!big.audio.paused) { return; } @@ -461,16 +522,16 @@ window.addEventListener('load', function() { } function onHashChange() { - if (big.mode !== 'talk') return; + if (big.mode !== "talk") return; go(parseHash()); } function onResize() { - if (big.mode !== 'talk') return; + if (big.mode !== "talk") return; var documentElement = document.documentElement; if (!documentElement) { throw new Error( - 'document.documentElement not found, this environment is weird' + "document.documentElement not found, this environment is weird" ); } var width = documentElement.clientWidth; diff --git a/package.json b/package.json index eec4062..cadcea7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "a presentation system for busy messy hackers", "main": "big.js", "scripts": { - "test": "eslint lib/big.js && stylelint lib/big.css && flow", + "test": "stylelint lib/big.css && flow", "doc": "documentation build lib/big.js -o docs/api.md -f md --access=public", "release": "standard-version" }, @@ -33,7 +33,6 @@ "devDependencies": { "cz-conventional-changelog": "^2.0.0", "documentation": "^8.0.0", - "eslint": "^4.1.0", "flow-bin": "^0.73.0", "prettier": "^1.1.0", "standard-version": "^4.0.0", @@ -48,33 +47,6 @@ "stylelint": { "extends": "stylelint-config-standard" }, - "eslintConfig": { - "rules": { - "indent": [ - 2, - 2 - ], - "no-console": [ - 0 - ], - "quotes": [ - 0 - ], - "linebreak-style": [ - 2, - "unix" - ], - "semi": [ - 2, - "always" - ] - }, - "env": { - "browser": true, - "node": true - }, - "extends": "eslint:recommended" - }, "dependencies": { "cpy": "^7.0.0", "ecstatic": "^3.1.1",