Exchanged revealjs with slidev
19
LICENSE
@ -1,19 +0,0 @@
|
|||||||
Copyright (C) 2011-2024 Hakim El Hattab, http://hakim.se, and reveal.js contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
BIN
assets/pictures/Uganda/2023-12-13 09.11.35.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/pictures/Uganda/2023-12-13 17.20.42.jpg
Normal file
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/pictures/Uganda/2023-12-14 01.48.30.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/pictures/Uganda/2023-12-14 02.15.31.jpg
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/pictures/Uganda/2023-12-14 05.15.34.jpg
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/pictures/Uganda/2023-12-14 12.13.44.jpg
Normal file
After Width: | Height: | Size: 3.7 MiB |
BIN
assets/pictures/Uganda/2023-12-14 12.33.21.jpg
Normal file
After Width: | Height: | Size: 4.0 MiB |
BIN
assets/pictures/Uganda/2023-12-14 17.44.43.jpg
Normal file
After Width: | Height: | Size: 4.1 MiB |
BIN
assets/pictures/Uganda/Tausi_Coaches_Bus_Terminal.JPG
Normal file
After Width: | Height: | Size: 128 KiB |
@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* Layout helpers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Stretch an element vertically based on available space
|
|
||||||
.reveal .stretch,
|
|
||||||
.reveal .r-stretch {
|
|
||||||
max-width: none;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal pre.stretch code,
|
|
||||||
.reveal pre.r-stretch code {
|
|
||||||
height: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text that auto-fits its container
|
|
||||||
.reveal .r-fit-text {
|
|
||||||
display: inline-block; // https://github.com/rikschennink/fitty#performance
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack multiple elements on top of each other
|
|
||||||
.reveal .r-stack {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .r-stack > * {
|
|
||||||
grid-area: 1/1;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal and vertical stacks
|
|
||||||
.reveal .r-vstack,
|
|
||||||
.reveal .r-hstack {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
img, video {
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .r-vstack {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .r-hstack {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Naming based on tailwindcss
|
|
||||||
.reveal .items-stretch { align-items: stretch; }
|
|
||||||
.reveal .items-start { align-items: flex-start; }
|
|
||||||
.reveal .items-center { align-items: center; }
|
|
||||||
.reveal .items-end { align-items: flex-end; }
|
|
||||||
|
|
||||||
.reveal .justify-between { justify-content: space-between; }
|
|
||||||
.reveal .justify-around { justify-content: space-around; }
|
|
||||||
.reveal .justify-start { justify-content: flex-start; }
|
|
||||||
.reveal .justify-center { justify-content: center; }
|
|
||||||
.reveal .justify-end { justify-content: flex-end; }
|
|
@ -1,166 +0,0 @@
|
|||||||
|
|
||||||
@media print {
|
|
||||||
html:not(.print-pdf) {
|
|
||||||
overflow: visible;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.print-pdf) .reveal {
|
|
||||||
background: #fff;
|
|
||||||
font-size: 20pt;
|
|
||||||
|
|
||||||
.controls,
|
|
||||||
.state-background,
|
|
||||||
.progress,
|
|
||||||
.backgrounds,
|
|
||||||
.slide-number {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
p, td, li {
|
|
||||||
font-size: 20pt!important;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,h2,h3,h4,h5,h6 {
|
|
||||||
color: #000!important;
|
|
||||||
height: auto;
|
|
||||||
line-height: normal;
|
|
||||||
text-align: left;
|
|
||||||
letter-spacing: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: 28pt !important; }
|
|
||||||
h2 { font-size: 24pt !important; }
|
|
||||||
h3 { font-size: 22pt !important; }
|
|
||||||
h4 { font-size: 22pt !important; font-variant: small-caps; }
|
|
||||||
h5 { font-size: 21pt !important; }
|
|
||||||
h6 { font-size: 20pt !important; font-style: italic; }
|
|
||||||
|
|
||||||
a:link,
|
|
||||||
a:visited {
|
|
||||||
color: #000 !important;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol, div, p {
|
|
||||||
visibility: visible;
|
|
||||||
position: static;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
overflow: visible;
|
|
||||||
margin: 0;
|
|
||||||
text-align: left !important;
|
|
||||||
}
|
|
||||||
pre,
|
|
||||||
table {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slides {
|
|
||||||
position: static !important;
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
|
|
||||||
left: 0 !important;
|
|
||||||
top: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
zoom: 1 !important;
|
|
||||||
transform: none !important;
|
|
||||||
|
|
||||||
overflow: visible !important;
|
|
||||||
display: block !important;
|
|
||||||
|
|
||||||
text-align: left !important;
|
|
||||||
perspective: none;
|
|
||||||
|
|
||||||
perspective-origin: 50% 50%;
|
|
||||||
}
|
|
||||||
.slides section {
|
|
||||||
visibility: visible !important;
|
|
||||||
position: static !important;
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
display: block !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
|
|
||||||
left: 0 !important;
|
|
||||||
top: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
padding: 60px 20px !important;
|
|
||||||
z-index: auto !important;
|
|
||||||
|
|
||||||
opacity: 1 !important;
|
|
||||||
|
|
||||||
page-break-after: always !important;
|
|
||||||
|
|
||||||
transform-style: flat !important;
|
|
||||||
transform: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
.slides section.stack {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.slides section:last-of-type {
|
|
||||||
page-break-after: avoid !important;
|
|
||||||
}
|
|
||||||
.slides section .fragment {
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.r-fit-text {
|
|
||||||
white-space: normal !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
section img {
|
|
||||||
display: block;
|
|
||||||
margin: 15px 0px;
|
|
||||||
background: rgba(255,255,255,1);
|
|
||||||
border: 1px solid #666;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
section small {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
max-height: 100%;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 15pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs .hljs-ln-numbers {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs td {
|
|
||||||
font-size: inherit !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
/**
|
|
||||||
* This stylesheet is used to print reveal.js
|
|
||||||
* presentations to PDF.
|
|
||||||
*
|
|
||||||
* https://revealjs.com/pdf-export/
|
|
||||||
*/
|
|
||||||
|
|
||||||
html.reveal-print {
|
|
||||||
* {
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
& {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0 auto !important;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
float: none !important;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove any elements not needed in print. */
|
|
||||||
.nestedarrow,
|
|
||||||
.reveal .controls,
|
|
||||||
.reveal .progress,
|
|
||||||
.reveal .playback,
|
|
||||||
.reveal.overview,
|
|
||||||
.state-background {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal pre code {
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal {
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
.reveal .slides {
|
|
||||||
position: static;
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto !important;
|
|
||||||
zoom: 1 !important;
|
|
||||||
pointer-events: initial;
|
|
||||||
|
|
||||||
left: auto;
|
|
||||||
top: auto;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
|
|
||||||
overflow: visible;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
perspective: none;
|
|
||||||
perspective-origin: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .slides .pdf-page {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
page-break-after: always;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .slides .pdf-page:last-of-type {
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .slides section {
|
|
||||||
visibility: visible !important;
|
|
||||||
display: block !important;
|
|
||||||
position: absolute !important;
|
|
||||||
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
min-height: 1px;
|
|
||||||
|
|
||||||
opacity: 1 !important;
|
|
||||||
|
|
||||||
transform-style: flat !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal section.stack {
|
|
||||||
position: relative !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
page-break-after: avoid !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal img {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slide backgrounds are placed inside of their slide when exporting to PDF */
|
|
||||||
.reveal .backgrounds {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.reveal .slide-background {
|
|
||||||
display: block !important;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Display slide speaker notes when 'showNotes' is enabled */
|
|
||||||
.reveal.show-notes {
|
|
||||||
max-width: none;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
.reveal .speaker-notes-pdf {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: none;
|
|
||||||
top: auto;
|
|
||||||
right: auto;
|
|
||||||
bottom: auto;
|
|
||||||
left: auto;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout option which makes notes appear on a separate page */
|
|
||||||
.reveal .speaker-notes-pdf[data-layout="separate-page"] {
|
|
||||||
position: relative;
|
|
||||||
color: inherit;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 20px;
|
|
||||||
page-break-after: always;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Display slide numbers when 'slideNumber' is enabled */
|
|
||||||
.reveal .slide-number-pdf {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
font-size: 14px;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This accessibility tool is not useful in PDF and breaks it visually */
|
|
||||||
.aria-status {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
2109
css/reveal.scss
@ -1,21 +0,0 @@
|
|||||||
## Dependencies
|
|
||||||
|
|
||||||
Themes are written using Sass to keep things modular and reduce the need for repeated selectors across files. Make sure that you have the reveal.js development environment installed before proceeding: https://revealjs.com/installation/#full-setup
|
|
||||||
|
|
||||||
## Creating a Theme
|
|
||||||
|
|
||||||
To create your own theme, start by duplicating a ```.scss``` file in [/css/theme/source](https://github.com/hakimel/reveal.js/blob/master/css/theme/source). It will be automatically compiled from Sass to CSS (see the [gulpfile](https://github.com/hakimel/reveal.js/blob/master/gulpfile.js)) when you run `npm run build -- css-themes`.
|
|
||||||
|
|
||||||
Each theme file does four things in the following order:
|
|
||||||
|
|
||||||
1. **Include [/css/theme/template/mixins.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/mixins.scss)**
|
|
||||||
Shared utility functions.
|
|
||||||
|
|
||||||
2. **Include [/css/theme/template/settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss)**
|
|
||||||
Declares a set of custom variables that the template file (step 4) expects. Can be overridden in step 3.
|
|
||||||
|
|
||||||
3. **Override**
|
|
||||||
This is where you override the default theme. Either by specifying variables (see [settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss) for reference) or by adding any selectors and styles you please.
|
|
||||||
|
|
||||||
4. **Include [/css/theme/template/theme.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/theme.scss)**
|
|
||||||
The template theme file which will generate final CSS output based on the currently defined variables.
|
|
@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Beige theme for reveal.js.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/league-gothic/league-gothic.css);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainColor: #333;
|
|
||||||
$headingColor: #333;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$backgroundColor: #f7f3de;
|
|
||||||
$linkColor: #8b743d;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: rgba(79, 64, 28, 0.99);
|
|
||||||
$heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15);
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Background generator
|
|
||||||
@mixin bodyBackground() {
|
|
||||||
@include radial-gradient( rgba(247,242,211,1), rgba(255,255,255,1) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Black compact & high contrast reveal.js theme, with headers not in capitals.
|
|
||||||
*
|
|
||||||
* By Peter Kehl. Based on black.(s)css by Hakim El Hattab, http://hakim.se
|
|
||||||
*
|
|
||||||
* - Keep the source similar to black.css - for easy comparison.
|
|
||||||
* - $mainFontSize controls code blocks, too (although under some ratio).
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/source-sans-pro/source-sans-pro.css);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$backgroundColor: #000000;
|
|
||||||
|
|
||||||
$mainColor: #fff;
|
|
||||||
$headingColor: #fff;
|
|
||||||
|
|
||||||
$mainFontSize: 42px;
|
|
||||||
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingLetterSpacing: normal;
|
|
||||||
$headingTextTransform: uppercase;
|
|
||||||
$headingFontWeight: 600;
|
|
||||||
$linkColor: #42affa;
|
|
||||||
$linkColorHover: lighten( $linkColor, 15% );
|
|
||||||
$selectionBackgroundColor: lighten( $linkColor, 25% );
|
|
||||||
|
|
||||||
$heading1Size: 2.5em;
|
|
||||||
$heading2Size: 1.6em;
|
|
||||||
$heading3Size: 1.3em;
|
|
||||||
$heading4Size: 1.0em;
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color(#000);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Black theme for reveal.js. This is the opposite of the 'white' theme.
|
|
||||||
*
|
|
||||||
* By Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/source-sans-pro/source-sans-pro.css);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$backgroundColor: #191919;
|
|
||||||
|
|
||||||
$mainColor: #fff;
|
|
||||||
$headingColor: #fff;
|
|
||||||
|
|
||||||
$mainFontSize: 42px;
|
|
||||||
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingLetterSpacing: normal;
|
|
||||||
$headingTextTransform: uppercase;
|
|
||||||
$headingFontWeight: 600;
|
|
||||||
$linkColor: #42affa;
|
|
||||||
$linkColorHover: lighten( $linkColor, 15% );
|
|
||||||
$selectionBackgroundColor: rgba( $linkColor, 0.75 );
|
|
||||||
|
|
||||||
$heading1Size: 2.5em;
|
|
||||||
$heading2Size: 1.6em;
|
|
||||||
$heading3Size: 1.3em;
|
|
||||||
$heading4Size: 1.0em;
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color(#222);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,87 +0,0 @@
|
|||||||
/**
|
|
||||||
* Blood theme for reveal.js
|
|
||||||
* Author: Walther http://github.com/Walther
|
|
||||||
*
|
|
||||||
* Designed to be used with highlight.js theme
|
|
||||||
* "monokai_sublime.css" available from
|
|
||||||
* https://github.com/isagalaev/highlight.js/
|
|
||||||
*
|
|
||||||
* For other themes, change $codeBackground accordingly.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,700,300italic,700italic);
|
|
||||||
|
|
||||||
// Colors used in the theme
|
|
||||||
$blood: #a23;
|
|
||||||
$coal: #222;
|
|
||||||
$codeBackground: #23241f;
|
|
||||||
|
|
||||||
$backgroundColor: $coal;
|
|
||||||
|
|
||||||
// Main text
|
|
||||||
$mainFont: Ubuntu, 'sans-serif';
|
|
||||||
$mainColor: #eee;
|
|
||||||
|
|
||||||
// Headings
|
|
||||||
$headingFont: Ubuntu, 'sans-serif';
|
|
||||||
$headingTextShadow: 2px 2px 2px $coal;
|
|
||||||
|
|
||||||
// h1 shadow, borrowed humbly from
|
|
||||||
// (c) Default theme by Hakim El Hattab
|
|
||||||
$heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15);
|
|
||||||
|
|
||||||
// Links
|
|
||||||
$linkColor: $blood;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
|
|
||||||
// Text selection
|
|
||||||
$selectionBackgroundColor: $blood;
|
|
||||||
$selectionColor: #fff;
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include light-bg-text-color(#222);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
// some overrides after theme template import
|
|
||||||
|
|
||||||
.reveal p {
|
|
||||||
font-weight: 300;
|
|
||||||
text-shadow: 1px 1px $coal;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.has-light-background {
|
|
||||||
p, h1, h2, h3, h4 {
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal h1,
|
|
||||||
.reveal h2,
|
|
||||||
.reveal h3,
|
|
||||||
.reveal h4,
|
|
||||||
.reveal h5,
|
|
||||||
.reveal h6 {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal p code {
|
|
||||||
background-color: $codeBackground;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal small code {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dracula Dark theme for reveal.js.
|
|
||||||
* Based on https://draculatheme.com
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
$systemFontsSansSerif: -apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
avenir next,
|
|
||||||
avenir,
|
|
||||||
segoe ui,
|
|
||||||
helvetica neue,
|
|
||||||
helvetica,
|
|
||||||
Cantarell,
|
|
||||||
Ubuntu,
|
|
||||||
roboto,
|
|
||||||
noto,
|
|
||||||
arial,
|
|
||||||
sans-serif;
|
|
||||||
$systemFontsMono: Menlo,
|
|
||||||
Consolas,
|
|
||||||
Monaco,
|
|
||||||
Liberation Mono,
|
|
||||||
Lucida Console,
|
|
||||||
monospace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dracula colors by Zeno Rocha
|
|
||||||
* https://draculatheme.com/contribute
|
|
||||||
*/
|
|
||||||
html * {
|
|
||||||
color-profile: sRGB;
|
|
||||||
rendering-intent: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
$background: #282A36;
|
|
||||||
$foreground: #F8F8F2;
|
|
||||||
$selection: #44475A;
|
|
||||||
$comment: #6272A4;
|
|
||||||
$red: #FF5555;
|
|
||||||
$orange: #FFB86C;
|
|
||||||
$yellow: #F1FA8C;
|
|
||||||
$green: #50FA7B;
|
|
||||||
$purple: #BD93F9;
|
|
||||||
$cyan: #8BE9FD;
|
|
||||||
$pink: #FF79C6;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainColor: $foreground;
|
|
||||||
$headingColor: $purple;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingTextTransform: none;
|
|
||||||
$backgroundColor: $background;
|
|
||||||
$linkColor: $pink;
|
|
||||||
$linkColorHover: $cyan;
|
|
||||||
$selectionBackgroundColor: $selection;
|
|
||||||
$inlineCodeColor: $green;
|
|
||||||
$listBulletColor: $cyan;
|
|
||||||
|
|
||||||
$mainFont: $systemFontsSansSerif;
|
|
||||||
$codeFont: "Fira Code", $systemFontsMono;
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color($background);
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
// Define additional color effects based on Dracula spec
|
|
||||||
// https://spec.draculatheme.com/
|
|
||||||
:root {
|
|
||||||
--r-bold-color: #{$orange};
|
|
||||||
--r-italic-color: #{$yellow};
|
|
||||||
--r-inline-code-color: #{$inlineCodeColor};
|
|
||||||
--r-list-bullet-color: #{$listBulletColor};
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal {
|
|
||||||
strong, b {
|
|
||||||
color: var(--r-bold-color);
|
|
||||||
}
|
|
||||||
em, i, blockquote {
|
|
||||||
color: var(--r-italic-color);
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
color: var(--r-inline-code-color);
|
|
||||||
}
|
|
||||||
// Dracula colored list bullets and numbers
|
|
||||||
ul, ol {
|
|
||||||
li::marker {
|
|
||||||
color: var(--r-list-bullet-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* League theme for reveal.js.
|
|
||||||
*
|
|
||||||
* This was the default theme pre-3.0.0.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/league-gothic/league-gothic.css);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$headingTextShadow: 0px 0px 6px rgba(0,0,0,0.2);
|
|
||||||
$heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15);
|
|
||||||
|
|
||||||
// Background generator
|
|
||||||
@mixin bodyBackground() {
|
|
||||||
@include radial-gradient( rgba(28,30,32,1), rgba(85,90,95,1) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color(#222);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* Solarized Dark theme for reveal.js.
|
|
||||||
* Author: Achim Staebler
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/league-gothic/league-gothic.css);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Solarized colors by Ethan Schoonover
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Solarized colors
|
|
||||||
$base03: #002b36;
|
|
||||||
$base02: #073642;
|
|
||||||
$base01: #586e75;
|
|
||||||
$base00: #657b83;
|
|
||||||
$base0: #839496;
|
|
||||||
$base1: #93a1a1;
|
|
||||||
$base2: #eee8d5;
|
|
||||||
$base3: #fdf6e3;
|
|
||||||
$yellow: #b58900;
|
|
||||||
$orange: #cb4b16;
|
|
||||||
$red: #dc322f;
|
|
||||||
$magenta: #d33682;
|
|
||||||
$violet: #6c71c4;
|
|
||||||
$blue: #268bd2;
|
|
||||||
$cyan: #2aa198;
|
|
||||||
$green: #859900;
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainColor: $base1;
|
|
||||||
$headingColor: $base2;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$backgroundColor: $base03;
|
|
||||||
$linkColor: $blue;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: $magenta;
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color(#222);
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Black theme for reveal.js.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Montserrat:700);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$backgroundColor: #111;
|
|
||||||
|
|
||||||
$mainFont: 'Open Sans', sans-serif;
|
|
||||||
$linkColor: #e7ad52;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$headingFont: 'Montserrat', Impact, sans-serif;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingLetterSpacing: -0.03em;
|
|
||||||
$headingTextTransform: none;
|
|
||||||
$selectionBackgroundColor: #e7ad52;
|
|
||||||
|
|
||||||
// Change text colors against light slide backgrounds
|
|
||||||
@include light-bg-text-color(#222);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* A simple theme for reveal.js presentations, similar
|
|
||||||
* to the default theme. The accent color is brown.
|
|
||||||
*
|
|
||||||
* This theme is Copyright (C) 2012-2013 Owen Versteeg, http://owenversteeg.com - it is MIT licensed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
|
|
||||||
$mainColor: #000;
|
|
||||||
$headingFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
|
|
||||||
$headingColor: #383D3D;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingTextTransform: none;
|
|
||||||
$backgroundColor: #F0F1EB;
|
|
||||||
$linkColor: #51483D;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: #26351C;
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
.reveal a {
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* A simple theme for reveal.js presentations, similar
|
|
||||||
* to the default theme. The accent color is darkblue.
|
|
||||||
*
|
|
||||||
* This theme is Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed.
|
|
||||||
* reveal.js is Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainFont: 'Lato', sans-serif;
|
|
||||||
$mainColor: #000;
|
|
||||||
$headingFont: 'News Cycle', Impact, sans-serif;
|
|
||||||
$headingColor: #000;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingTextTransform: none;
|
|
||||||
$backgroundColor: #fff;
|
|
||||||
$linkColor: #00008B;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: rgba(0, 0, 0, 0.99);
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sky theme for reveal.js.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Quicksand:400,700,400italic,700italic);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainFont: 'Open Sans', sans-serif;
|
|
||||||
$mainColor: #333;
|
|
||||||
$headingFont: 'Quicksand', sans-serif;
|
|
||||||
$headingColor: #333;
|
|
||||||
$headingLetterSpacing: -0.08em;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$backgroundColor: #f7fbfc;
|
|
||||||
$linkColor: #3b759e;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: #134674;
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Fix links so they are not cut off
|
|
||||||
.reveal a {
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background generator
|
|
||||||
@mixin bodyBackground() {
|
|
||||||
@include radial-gradient( #add9e4, #f7fbfc );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* Solarized Light theme for reveal.js.
|
|
||||||
* Author: Achim Staebler
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/league-gothic/league-gothic.css);
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Solarized colors by Ethan Schoonover
|
|
||||||
*/
|
|
||||||
html * {
|
|
||||||
color-profile: sRGB;
|
|
||||||
rendering-intent: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solarized colors
|
|
||||||
$base03: #002b36;
|
|
||||||
$base02: #073642;
|
|
||||||
$base01: #586e75;
|
|
||||||
$base00: #657b83;
|
|
||||||
$base0: #839496;
|
|
||||||
$base1: #93a1a1;
|
|
||||||
$base2: #eee8d5;
|
|
||||||
$base3: #fdf6e3;
|
|
||||||
$yellow: #b58900;
|
|
||||||
$orange: #cb4b16;
|
|
||||||
$red: #dc322f;
|
|
||||||
$magenta: #d33682;
|
|
||||||
$violet: #6c71c4;
|
|
||||||
$blue: #268bd2;
|
|
||||||
$cyan: #2aa198;
|
|
||||||
$green: #859900;
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$mainColor: $base00;
|
|
||||||
$headingColor: $base01;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$backgroundColor: $base3;
|
|
||||||
$linkColor: $blue;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
$selectionBackgroundColor: $magenta;
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Background generator
|
|
||||||
// @mixin bodyBackground() {
|
|
||||||
// @include radial-gradient( rgba($base3,1), rgba(lighten($base3, 20%),1) );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* White compact & high contrast reveal.js theme, with headers not in capitals.
|
|
||||||
*
|
|
||||||
* By Peter Kehl. Based on white.(s)css by Hakim El Hattab, http://hakim.se
|
|
||||||
*
|
|
||||||
* - Keep the source similar to black.css - for easy comparison.
|
|
||||||
* - $mainFontSize controls code blocks, too (although under some ratio).
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/source-sans-pro/source-sans-pro.css);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$backgroundColor: #fff;
|
|
||||||
|
|
||||||
$mainColor: #000;
|
|
||||||
$headingColor: #000;
|
|
||||||
|
|
||||||
$mainFontSize: 42px;
|
|
||||||
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingLetterSpacing: normal;
|
|
||||||
$headingTextTransform: uppercase;
|
|
||||||
$headingFontWeight: 600;
|
|
||||||
$linkColor: #2a76dd;
|
|
||||||
$linkColorHover: lighten( $linkColor, 15% );
|
|
||||||
$selectionBackgroundColor: lighten( $linkColor, 25% );
|
|
||||||
|
|
||||||
$heading1Size: 2.5em;
|
|
||||||
$heading2Size: 1.6em;
|
|
||||||
$heading3Size: 1.3em;
|
|
||||||
$heading4Size: 1.0em;
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* White theme for reveal.js. This is the opposite of the 'black' theme.
|
|
||||||
*
|
|
||||||
* By Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Default mixins and settings -----------------
|
|
||||||
@import "../template/mixins";
|
|
||||||
@import "../template/settings";
|
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
// Include theme-specific fonts
|
|
||||||
@import url(./fonts/source-sans-pro/source-sans-pro.css);
|
|
||||||
|
|
||||||
|
|
||||||
// Override theme settings (see ../template/settings.scss)
|
|
||||||
$backgroundColor: #fff;
|
|
||||||
|
|
||||||
$mainColor: #222;
|
|
||||||
$headingColor: #222;
|
|
||||||
|
|
||||||
$mainFontSize: 42px;
|
|
||||||
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingLetterSpacing: normal;
|
|
||||||
$headingTextTransform: uppercase;
|
|
||||||
$headingFontWeight: 600;
|
|
||||||
$linkColor: #2a76dd;
|
|
||||||
$linkColorHover: lighten( $linkColor, 15% );
|
|
||||||
$selectionBackgroundColor: lighten( $linkColor, 25% );
|
|
||||||
|
|
||||||
$heading1Size: 2.5em;
|
|
||||||
$heading2Size: 1.6em;
|
|
||||||
$heading3Size: 1.3em;
|
|
||||||
$heading4Size: 1.0em;
|
|
||||||
|
|
||||||
$overlayElementBgColor: 0, 0, 0;
|
|
||||||
$overlayElementFgColor: 240, 240, 240;
|
|
||||||
|
|
||||||
// Change text colors against dark slide backgrounds
|
|
||||||
@include dark-bg-text-color(#fff);
|
|
||||||
|
|
||||||
|
|
||||||
// Theme template ------------------------------
|
|
||||||
@import "../template/theme";
|
|
||||||
// ---------------------------------------------
|
|
@ -1,30 +0,0 @@
|
|||||||
// Exposes theme's variables for easy re-use in CSS for plugin authors
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--r-background-color: #{$backgroundColor};
|
|
||||||
--r-main-font: #{$mainFont};
|
|
||||||
--r-main-font-size: #{$mainFontSize};
|
|
||||||
--r-main-color: #{$mainColor};
|
|
||||||
--r-block-margin: #{$blockMargin};
|
|
||||||
--r-heading-margin: #{$headingMargin};
|
|
||||||
--r-heading-font: #{$headingFont};
|
|
||||||
--r-heading-color: #{$headingColor};
|
|
||||||
--r-heading-line-height: #{$headingLineHeight};
|
|
||||||
--r-heading-letter-spacing: #{$headingLetterSpacing};
|
|
||||||
--r-heading-text-transform: #{$headingTextTransform};
|
|
||||||
--r-heading-text-shadow: #{$headingTextShadow};
|
|
||||||
--r-heading-font-weight: #{$headingFontWeight};
|
|
||||||
--r-heading1-text-shadow: #{$heading1TextShadow};
|
|
||||||
--r-heading1-size: #{$heading1Size};
|
|
||||||
--r-heading2-size: #{$heading2Size};
|
|
||||||
--r-heading3-size: #{$heading3Size};
|
|
||||||
--r-heading4-size: #{$heading4Size};
|
|
||||||
--r-code-font: #{$codeFont};
|
|
||||||
--r-link-color: #{$linkColor};
|
|
||||||
--r-link-color-dark: #{darken($linkColor , 15% )};
|
|
||||||
--r-link-color-hover: #{$linkColorHover};
|
|
||||||
--r-selection-background-color: #{$selectionBackgroundColor};
|
|
||||||
--r-selection-color: #{$selectionColor};
|
|
||||||
--r-overlay-element-bg-color: #{$overlayElementBgColor};
|
|
||||||
--r-overlay-element-fg-color: #{$overlayElementFgColor};
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
@mixin vertical-gradient( $top, $bottom ) {
|
|
||||||
background: $top;
|
|
||||||
background: -moz-linear-gradient( top, $top 0%, $bottom 100% );
|
|
||||||
background: -webkit-gradient( linear, left top, left bottom, color-stop(0%,$top), color-stop(100%,$bottom) );
|
|
||||||
background: -webkit-linear-gradient( top, $top 0%, $bottom 100% );
|
|
||||||
background: -o-linear-gradient( top, $top 0%, $bottom 100% );
|
|
||||||
background: -ms-linear-gradient( top, $top 0%, $bottom 100% );
|
|
||||||
background: linear-gradient( top, $top 0%, $bottom 100% );
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin horizontal-gradient( $top, $bottom ) {
|
|
||||||
background: $top;
|
|
||||||
background: -moz-linear-gradient( left, $top 0%, $bottom 100% );
|
|
||||||
background: -webkit-gradient( linear, left top, right top, color-stop(0%,$top), color-stop(100%,$bottom) );
|
|
||||||
background: -webkit-linear-gradient( left, $top 0%, $bottom 100% );
|
|
||||||
background: -o-linear-gradient( left, $top 0%, $bottom 100% );
|
|
||||||
background: -ms-linear-gradient( left, $top 0%, $bottom 100% );
|
|
||||||
background: linear-gradient( left, $top 0%, $bottom 100% );
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin radial-gradient( $outer, $inner, $type: circle ) {
|
|
||||||
background: $outer;
|
|
||||||
background: -moz-radial-gradient( center, $type cover, $inner 0%, $outer 100% );
|
|
||||||
background: -webkit-gradient( radial, center center, 0px, center center, 100%, color-stop(0%,$inner), color-stop(100%,$outer) );
|
|
||||||
background: -webkit-radial-gradient( center, $type cover, $inner 0%, $outer 100% );
|
|
||||||
background: -o-radial-gradient( center, $type cover, $inner 0%, $outer 100% );
|
|
||||||
background: -ms-radial-gradient( center, $type cover, $inner 0%, $outer 100% );
|
|
||||||
background: radial-gradient( center, $type cover, $inner 0%, $outer 100% );
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin light-bg-text-color( $color ) {
|
|
||||||
section.has-light-background {
|
|
||||||
&, h1, h2, h3, h4, h5, h6 {
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin dark-bg-text-color( $color ) {
|
|
||||||
section.has-dark-background {
|
|
||||||
&, h1, h2, h3, h4, h5, h6 {
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
// Base settings for all themes that can optionally be
|
|
||||||
// overridden by the super-theme
|
|
||||||
|
|
||||||
// Background of the presentation
|
|
||||||
$backgroundColor: #2b2b2b;
|
|
||||||
|
|
||||||
// Primary/body text
|
|
||||||
$mainFont: 'Lato', sans-serif;
|
|
||||||
$mainFontSize: 40px;
|
|
||||||
$mainColor: #eee;
|
|
||||||
|
|
||||||
// Vertical spacing between blocks of text
|
|
||||||
$blockMargin: 20px;
|
|
||||||
|
|
||||||
// Headings
|
|
||||||
$headingMargin: 0 0 $blockMargin 0;
|
|
||||||
$headingFont: 'League Gothic', Impact, sans-serif;
|
|
||||||
$headingColor: #eee;
|
|
||||||
$headingLineHeight: 1.2;
|
|
||||||
$headingLetterSpacing: normal;
|
|
||||||
$headingTextTransform: uppercase;
|
|
||||||
$headingTextShadow: none;
|
|
||||||
$headingFontWeight: normal;
|
|
||||||
$heading1TextShadow: $headingTextShadow;
|
|
||||||
|
|
||||||
$heading1Size: 3.77em;
|
|
||||||
$heading2Size: 2.11em;
|
|
||||||
$heading3Size: 1.55em;
|
|
||||||
$heading4Size: 1.00em;
|
|
||||||
|
|
||||||
$codeFont: monospace;
|
|
||||||
|
|
||||||
// Links and actions
|
|
||||||
$linkColor: #13DAEC;
|
|
||||||
$linkColorHover: lighten( $linkColor, 20% );
|
|
||||||
|
|
||||||
// Text selection
|
|
||||||
$selectionBackgroundColor: #FF5E99;
|
|
||||||
$selectionColor: #fff;
|
|
||||||
|
|
||||||
// Colors used for UI elements that are overlaid on top of
|
|
||||||
// the presentation
|
|
||||||
$overlayElementBgColor: 240, 240, 240;
|
|
||||||
$overlayElementFgColor: 0, 0, 0;
|
|
||||||
|
|
||||||
// Generates the presentation background, can be overridden
|
|
||||||
// to return a background image or gradient
|
|
||||||
@mixin bodyBackground() {
|
|
||||||
background: $backgroundColor;
|
|
||||||
}
|
|
@ -1,331 +0,0 @@
|
|||||||
// Base theme template for reveal.js
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* GLOBAL STYLES
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
@import "./exposer";
|
|
||||||
|
|
||||||
.reveal-viewport {
|
|
||||||
@include bodyBackground();
|
|
||||||
background-color: var(--r-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal {
|
|
||||||
font-family: var(--r-main-font);
|
|
||||||
font-size: var(--r-main-font-size);
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--r-main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ::selection {
|
|
||||||
color: var(--r-selection-color);
|
|
||||||
background: var(--r-selection-background-color);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ::-moz-selection {
|
|
||||||
color: var(--r-selection-color);
|
|
||||||
background: var(--r-selection-background-color);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .slides section,
|
|
||||||
.reveal .slides section>section {
|
|
||||||
line-height: 1.3;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* HEADERS
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal h1,
|
|
||||||
.reveal h2,
|
|
||||||
.reveal h3,
|
|
||||||
.reveal h4,
|
|
||||||
.reveal h5,
|
|
||||||
.reveal h6 {
|
|
||||||
margin: var(--r-heading-margin);
|
|
||||||
color: var(--r-heading-color);
|
|
||||||
|
|
||||||
font-family: var(--r-heading-font);
|
|
||||||
font-weight: var(--r-heading-font-weight);
|
|
||||||
line-height: var(--r-heading-line-height);
|
|
||||||
letter-spacing: var(--r-heading-letter-spacing);
|
|
||||||
|
|
||||||
text-transform: var(--r-heading-text-transform);
|
|
||||||
text-shadow: var(--r-heading-text-shadow);
|
|
||||||
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal h1 {font-size: var(--r-heading1-size); }
|
|
||||||
.reveal h2 {font-size: var(--r-heading2-size); }
|
|
||||||
.reveal h3 {font-size: var(--r-heading3-size); }
|
|
||||||
.reveal h4 {font-size: var(--r-heading4-size); }
|
|
||||||
|
|
||||||
.reveal h1 {
|
|
||||||
text-shadow: var(--r-heading1-text-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* OTHER
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal p {
|
|
||||||
margin: var(--r-block-margin) 0;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove trailing margins after titles */
|
|
||||||
.reveal h1:last-child,
|
|
||||||
.reveal h2:last-child,
|
|
||||||
.reveal h3:last-child,
|
|
||||||
.reveal h4:last-child,
|
|
||||||
.reveal h5:last-child,
|
|
||||||
.reveal h6:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure certain elements are never larger than the slide itself */
|
|
||||||
.reveal img,
|
|
||||||
.reveal video,
|
|
||||||
.reveal iframe {
|
|
||||||
max-width: 95%;
|
|
||||||
max-height: 95%;
|
|
||||||
}
|
|
||||||
.reveal strong,
|
|
||||||
.reveal b {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal em {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ol,
|
|
||||||
.reveal dl,
|
|
||||||
.reveal ul {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
margin: 0 0 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ul ul {
|
|
||||||
list-style-type: square;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ul ul ul {
|
|
||||||
list-style-type: circle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal ul ul,
|
|
||||||
.reveal ul ol,
|
|
||||||
.reveal ol ol,
|
|
||||||
.reveal ol ul {
|
|
||||||
display: block;
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal dt {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal dd {
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal blockquote {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
width: 70%;
|
|
||||||
margin: var(--r-block-margin) auto;
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
font-style: italic;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
box-shadow: 0px 0px 2px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
.reveal blockquote p:first-child,
|
|
||||||
.reveal blockquote p:last-child {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal q {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal pre {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
width: 90%;
|
|
||||||
margin: var(--r-block-margin) auto;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.55em;
|
|
||||||
font-family: var(--r-code-font);
|
|
||||||
line-height: 1.2em;
|
|
||||||
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal code {
|
|
||||||
font-family: var(--r-code-font);
|
|
||||||
text-transform: none;
|
|
||||||
tab-size: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal pre code {
|
|
||||||
display: block;
|
|
||||||
padding: 5px;
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 400px;
|
|
||||||
word-wrap: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .code-wrapper {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .code-wrapper code {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table {
|
|
||||||
margin: auto;
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table th {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table th,
|
|
||||||
.reveal table td {
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table th[align="center"],
|
|
||||||
.reveal table td[align="center"] {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table th[align="right"],
|
|
||||||
.reveal table td[align="right"] {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal table tbody tr:last-child th,
|
|
||||||
.reveal table tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal sup {
|
|
||||||
vertical-align: super;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
.reveal sub {
|
|
||||||
vertical-align: sub;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal small {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.6em;
|
|
||||||
line-height: 1.2em;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal small * {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal img {
|
|
||||||
margin: var(--r-block-margin) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* LINKS
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal a {
|
|
||||||
color: var(--r-link-color);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color .15s ease;
|
|
||||||
}
|
|
||||||
.reveal a:hover {
|
|
||||||
color: var(--r-link-color-hover);
|
|
||||||
text-shadow: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal .roll span:after {
|
|
||||||
color: #fff;
|
|
||||||
// background: darken( var(--r-link-color), 15% );
|
|
||||||
background: var(--r-link-color-dark);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* Frame helper
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal .r-frame {
|
|
||||||
border: 4px solid var(--r-main-color);
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal a .r-frame {
|
|
||||||
transition: all .15s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal a:hover .r-frame {
|
|
||||||
border-color: var(--r-link-color);
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* NAVIGATION CONTROLS
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal .controls {
|
|
||||||
color: var(--r-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* PROGRESS BAR
|
|
||||||
*********************************************/
|
|
||||||
|
|
||||||
.reveal .progress {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
color: var(--r-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********************************************
|
|
||||||
* PRINT BACKGROUND
|
|
||||||
*********************************************/
|
|
||||||
@media print {
|
|
||||||
.backgrounds {
|
|
||||||
background-color: var(--r-background-color);
|
|
||||||
}
|
|
||||||
}
|
|
321
gulpfile.js
@ -1,321 +0,0 @@
|
|||||||
const pkg = require('./package.json')
|
|
||||||
const glob = require('glob')
|
|
||||||
const yargs = require('yargs')
|
|
||||||
const through = require('through2');
|
|
||||||
const qunit = require('node-qunit-puppeteer')
|
|
||||||
|
|
||||||
const {rollup} = require('rollup')
|
|
||||||
const terser = require('@rollup/plugin-terser')
|
|
||||||
const babel = require('@rollup/plugin-babel').default
|
|
||||||
const commonjs = require('@rollup/plugin-commonjs')
|
|
||||||
const resolve = require('@rollup/plugin-node-resolve').default
|
|
||||||
const sass = require('sass')
|
|
||||||
|
|
||||||
const gulp = require('gulp')
|
|
||||||
const tap = require('gulp-tap')
|
|
||||||
const zip = require('gulp-zip')
|
|
||||||
const header = require('gulp-header')
|
|
||||||
const eslint = require('gulp-eslint')
|
|
||||||
const minify = require('gulp-clean-css')
|
|
||||||
const connect = require('gulp-connect')
|
|
||||||
const autoprefixer = require('gulp-autoprefixer')
|
|
||||||
|
|
||||||
const root = yargs.argv.root || '.'
|
|
||||||
const port = yargs.argv.port || 8000
|
|
||||||
const host = yargs.argv.host || 'localhost'
|
|
||||||
|
|
||||||
const banner = `/*!
|
|
||||||
* reveal.js ${pkg.version}
|
|
||||||
* ${pkg.homepage}
|
|
||||||
* MIT licensed
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2024 Hakim El Hattab, https://hakim.se
|
|
||||||
*/\n`
|
|
||||||
|
|
||||||
// Prevents warnings from opening too many test pages
|
|
||||||
process.setMaxListeners(20);
|
|
||||||
|
|
||||||
const babelConfig = {
|
|
||||||
babelHelpers: 'bundled',
|
|
||||||
ignore: ['node_modules'],
|
|
||||||
compact: false,
|
|
||||||
extensions: ['.js', '.html'],
|
|
||||||
plugins: [
|
|
||||||
'transform-html-import-to-string'
|
|
||||||
],
|
|
||||||
presets: [[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
corejs: 3,
|
|
||||||
useBuiltIns: 'usage',
|
|
||||||
modules: false
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Our ES module bundle only targets newer browsers with
|
|
||||||
// module support. Browsers are targeted explicitly instead
|
|
||||||
// of using the "esmodule: true" target since that leads to
|
|
||||||
// polyfilling older browsers and a larger bundle.
|
|
||||||
const babelConfigESM = JSON.parse( JSON.stringify( babelConfig ) );
|
|
||||||
babelConfigESM.presets[0][1].targets = { browsers: [
|
|
||||||
'last 2 Chrome versions',
|
|
||||||
'last 2 Safari versions',
|
|
||||||
'last 2 iOS versions',
|
|
||||||
'last 2 Firefox versions',
|
|
||||||
'last 2 Edge versions',
|
|
||||||
] };
|
|
||||||
|
|
||||||
let cache = {};
|
|
||||||
|
|
||||||
// Creates a bundle with broad browser support, exposed
|
|
||||||
// as UMD
|
|
||||||
gulp.task('js-es5', () => {
|
|
||||||
return rollup({
|
|
||||||
cache: cache.umd,
|
|
||||||
input: 'js/index.js',
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
babel( babelConfig ),
|
|
||||||
terser()
|
|
||||||
]
|
|
||||||
}).then( bundle => {
|
|
||||||
cache.umd = bundle.cache;
|
|
||||||
return bundle.write({
|
|
||||||
name: 'Reveal',
|
|
||||||
file: './dist/reveal.js',
|
|
||||||
format: 'umd',
|
|
||||||
banner: banner,
|
|
||||||
sourcemap: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Creates an ES module bundle
|
|
||||||
gulp.task('js-es6', () => {
|
|
||||||
return rollup({
|
|
||||||
cache: cache.esm,
|
|
||||||
input: 'js/index.js',
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
babel( babelConfigESM ),
|
|
||||||
terser()
|
|
||||||
]
|
|
||||||
}).then( bundle => {
|
|
||||||
cache.esm = bundle.cache;
|
|
||||||
return bundle.write({
|
|
||||||
file: './dist/reveal.esm.js',
|
|
||||||
format: 'es',
|
|
||||||
banner: banner,
|
|
||||||
sourcemap: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
gulp.task('js', gulp.parallel('js-es5', 'js-es6'));
|
|
||||||
|
|
||||||
// Creates a UMD and ES module bundle for each of our
|
|
||||||
// built-in plugins
|
|
||||||
gulp.task('plugins', () => {
|
|
||||||
return Promise.all([
|
|
||||||
{ name: 'RevealHighlight', input: './plugin/highlight/plugin.js', output: './plugin/highlight/highlight' },
|
|
||||||
{ name: 'RevealMarkdown', input: './plugin/markdown/plugin.js', output: './plugin/markdown/markdown' },
|
|
||||||
{ name: 'RevealSearch', input: './plugin/search/plugin.js', output: './plugin/search/search' },
|
|
||||||
{ name: 'RevealNotes', input: './plugin/notes/plugin.js', output: './plugin/notes/notes' },
|
|
||||||
{ name: 'RevealZoom', input: './plugin/zoom/plugin.js', output: './plugin/zoom/zoom' },
|
|
||||||
{ name: 'RevealMath', input: './plugin/math/plugin.js', output: './plugin/math/math' },
|
|
||||||
].map( plugin => {
|
|
||||||
return rollup({
|
|
||||||
cache: cache[plugin.input],
|
|
||||||
input: plugin.input,
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
babel({
|
|
||||||
...babelConfig,
|
|
||||||
ignore: [/node_modules\/(?!(highlight\.js|marked)\/).*/],
|
|
||||||
}),
|
|
||||||
terser()
|
|
||||||
]
|
|
||||||
}).then( bundle => {
|
|
||||||
cache[plugin.input] = bundle.cache;
|
|
||||||
bundle.write({
|
|
||||||
file: plugin.output + '.esm.js',
|
|
||||||
name: plugin.name,
|
|
||||||
format: 'es'
|
|
||||||
})
|
|
||||||
|
|
||||||
bundle.write({
|
|
||||||
file: plugin.output + '.js',
|
|
||||||
name: plugin.name,
|
|
||||||
format: 'umd'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
} ));
|
|
||||||
})
|
|
||||||
|
|
||||||
// a custom pipeable step to transform Sass to CSS
|
|
||||||
function compileSass() {
|
|
||||||
return through.obj( ( vinylFile, encoding, callback ) => {
|
|
||||||
const transformedFile = vinylFile.clone();
|
|
||||||
|
|
||||||
sass.render({
|
|
||||||
data: transformedFile.contents.toString(),
|
|
||||||
file: transformedFile.path,
|
|
||||||
}, ( err, result ) => {
|
|
||||||
if( err ) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
transformedFile.extname = '.css';
|
|
||||||
transformedFile.contents = result.css;
|
|
||||||
callback( null, transformedFile );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task('css-themes', () => gulp.src(['./css/theme/source/*.{sass,scss}'])
|
|
||||||
.pipe(compileSass())
|
|
||||||
.pipe(gulp.dest('./dist/theme')))
|
|
||||||
|
|
||||||
gulp.task('css-core', () => gulp.src(['css/reveal.scss'])
|
|
||||||
.pipe(compileSass())
|
|
||||||
.pipe(autoprefixer())
|
|
||||||
.pipe(minify({compatibility: 'ie9'}))
|
|
||||||
.pipe(header(banner))
|
|
||||||
.pipe(gulp.dest('./dist')))
|
|
||||||
|
|
||||||
gulp.task('css', gulp.parallel('css-themes', 'css-core'))
|
|
||||||
|
|
||||||
gulp.task('qunit', () => {
|
|
||||||
|
|
||||||
let serverConfig = {
|
|
||||||
root,
|
|
||||||
port: 8009,
|
|
||||||
host: 'localhost',
|
|
||||||
name: 'test-server'
|
|
||||||
}
|
|
||||||
|
|
||||||
let server = connect.server( serverConfig )
|
|
||||||
|
|
||||||
let testFiles = glob.sync('test/*.html' )
|
|
||||||
|
|
||||||
let totalTests = 0;
|
|
||||||
let failingTests = 0;
|
|
||||||
|
|
||||||
let tests = Promise.all( testFiles.map( filename => {
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
qunit.runQunitPuppeteer({
|
|
||||||
targetUrl: `http://${serverConfig.host}:${serverConfig.port}/${filename}`,
|
|
||||||
timeout: 20000,
|
|
||||||
redirectConsole: false,
|
|
||||||
puppeteerArgs: ['--allow-file-access-from-files']
|
|
||||||
})
|
|
||||||
.then(result => {
|
|
||||||
if( result.stats.failed > 0 ) {
|
|
||||||
console.log(`${'!'} ${filename} [${result.stats.passed}/${result.stats.total}] in ${result.stats.runtime}ms`.red);
|
|
||||||
// qunit.printResultSummary(result, console);
|
|
||||||
qunit.printFailedTests(result, console);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(`${'✔'} ${filename} [${result.stats.passed}/${result.stats.total}] in ${result.stats.runtime}ms`.green);
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTests += result.stats.total;
|
|
||||||
failingTests += result.stats.failed;
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
reject();
|
|
||||||
});
|
|
||||||
} )
|
|
||||||
} ) );
|
|
||||||
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
|
|
||||||
tests.then( () => {
|
|
||||||
if( failingTests > 0 ) {
|
|
||||||
reject( new Error(`${failingTests}/${totalTests} tests failed`.red) );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(`${'✔'} Passed ${totalTests} tests`.green.bold);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
.catch( () => {
|
|
||||||
reject();
|
|
||||||
} )
|
|
||||||
.finally( () => {
|
|
||||||
server.close();
|
|
||||||
} );
|
|
||||||
|
|
||||||
} );
|
|
||||||
} )
|
|
||||||
|
|
||||||
gulp.task('eslint', () => gulp.src(['./js/**', 'gulpfile.js'])
|
|
||||||
.pipe(eslint())
|
|
||||||
.pipe(eslint.format()))
|
|
||||||
|
|
||||||
gulp.task('test', gulp.series( 'eslint', 'qunit' ))
|
|
||||||
|
|
||||||
gulp.task('default', gulp.series(gulp.parallel('js', 'css', 'plugins'), 'test'))
|
|
||||||
|
|
||||||
gulp.task('build', gulp.parallel('js', 'css', 'plugins'))
|
|
||||||
|
|
||||||
gulp.task('package', gulp.series(() =>
|
|
||||||
|
|
||||||
gulp.src(
|
|
||||||
[
|
|
||||||
'./index.html',
|
|
||||||
'./dist/**',
|
|
||||||
'./lib/**',
|
|
||||||
'./images/**',
|
|
||||||
'./plugin/**',
|
|
||||||
'./**/*.md'
|
|
||||||
],
|
|
||||||
{ base: './' }
|
|
||||||
)
|
|
||||||
.pipe(zip('reveal-js-presentation.zip')).pipe(gulp.dest('./'))
|
|
||||||
|
|
||||||
))
|
|
||||||
|
|
||||||
gulp.task('reload', () => gulp.src(['index.html'])
|
|
||||||
.pipe(connect.reload()));
|
|
||||||
|
|
||||||
gulp.task('serve', () => {
|
|
||||||
|
|
||||||
connect.server({
|
|
||||||
root: root,
|
|
||||||
port: port,
|
|
||||||
host: host,
|
|
||||||
livereload: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const slidesRoot = root.endsWith('/') ? root : root + '/'
|
|
||||||
gulp.watch([
|
|
||||||
slidesRoot + '**/*.html',
|
|
||||||
slidesRoot + '**/*.md',
|
|
||||||
`!${slidesRoot}**/node_modules/**`, // ignore node_modules
|
|
||||||
], gulp.series('reload'))
|
|
||||||
|
|
||||||
gulp.watch(['js/**'], gulp.series('js', 'reload', 'eslint'))
|
|
||||||
|
|
||||||
gulp.watch(['plugin/**/plugin.js', 'plugin/**/*.html'], gulp.series('plugins', 'reload'))
|
|
||||||
|
|
||||||
gulp.watch([
|
|
||||||
'css/theme/source/**/*.{sass,scss}',
|
|
||||||
'css/theme/template/*.{sass,scss}',
|
|
||||||
], gulp.series('css-themes', 'reload'))
|
|
||||||
|
|
||||||
gulp.watch([
|
|
||||||
'css/*.scss',
|
|
||||||
'css/print/*.{sass,scss,css}'
|
|
||||||
], gulp.series('css-core', 'reload'))
|
|
||||||
|
|
||||||
gulp.watch(['test/*.html'], gulp.series('test'))
|
|
||||||
|
|
||||||
})
|
|
43
index.html
@ -1,43 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
|
|
||||||
<title>reveal.js</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="dist/reset.css">
|
|
||||||
<link rel="stylesheet" href="dist/reveal.css">
|
|
||||||
<link rel="stylesheet" href="dist/theme/black.css">
|
|
||||||
|
|
||||||
<!-- Theme used for syntax highlighted code -->
|
|
||||||
<link rel="stylesheet" href="plugin/highlight/monokai.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="reveal">
|
|
||||||
<div class="slides">
|
|
||||||
<section>Slide 1</section>
|
|
||||||
<section>Slide 2</section>
|
|
||||||
<section> Uganda Rundreise
|
|
||||||
<video data-autoplay src="assets/videos/Uganda Rundreise.mp4"></video>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="dist/reveal.js"></script>
|
|
||||||
<script src="plugin/notes/notes.js"></script>
|
|
||||||
<script src="plugin/markdown/markdown.js"></script>
|
|
||||||
<script src="plugin/highlight/highlight.js"></script>
|
|
||||||
<script>
|
|
||||||
// More info about initialization & config:
|
|
||||||
// - https://revealjs.com/initialization/
|
|
||||||
// - https://revealjs.com/config/
|
|
||||||
Reveal.initialize({
|
|
||||||
hash: true,
|
|
||||||
|
|
||||||
// Learn about plugins: https://revealjs.com/plugins/
|
|
||||||
plugins: [ RevealMarkdown, RevealHighlight, RevealNotes ]
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* UI component that lets the use control auto-slide
|
|
||||||
* playback via play/pause.
|
|
||||||
*/
|
|
||||||
export default class Playback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} container The component will append
|
|
||||||
* itself to this
|
|
||||||
* @param {function} progressCheck A method which will be
|
|
||||||
* called frequently to get the current playback progress on
|
|
||||||
* a range of 0-1
|
|
||||||
*/
|
|
||||||
constructor( container, progressCheck ) {
|
|
||||||
|
|
||||||
// Cosmetics
|
|
||||||
this.diameter = 100;
|
|
||||||
this.diameter2 = this.diameter/2;
|
|
||||||
this.thickness = 6;
|
|
||||||
|
|
||||||
// Flags if we are currently playing
|
|
||||||
this.playing = false;
|
|
||||||
|
|
||||||
// Current progress on a 0-1 range
|
|
||||||
this.progress = 0;
|
|
||||||
|
|
||||||
// Used to loop the animation smoothly
|
|
||||||
this.progressOffset = 1;
|
|
||||||
|
|
||||||
this.container = container;
|
|
||||||
this.progressCheck = progressCheck;
|
|
||||||
|
|
||||||
this.canvas = document.createElement( 'canvas' );
|
|
||||||
this.canvas.className = 'playback';
|
|
||||||
this.canvas.width = this.diameter;
|
|
||||||
this.canvas.height = this.diameter;
|
|
||||||
this.canvas.style.width = this.diameter2 + 'px';
|
|
||||||
this.canvas.style.height = this.diameter2 + 'px';
|
|
||||||
this.context = this.canvas.getContext( '2d' );
|
|
||||||
|
|
||||||
this.container.appendChild( this.canvas );
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlaying( value ) {
|
|
||||||
|
|
||||||
const wasPlaying = this.playing;
|
|
||||||
|
|
||||||
this.playing = value;
|
|
||||||
|
|
||||||
// Start repainting if we weren't already
|
|
||||||
if( !wasPlaying && this.playing ) {
|
|
||||||
this.animate();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
animate() {
|
|
||||||
|
|
||||||
const progressBefore = this.progress;
|
|
||||||
|
|
||||||
this.progress = this.progressCheck();
|
|
||||||
|
|
||||||
// When we loop, offset the progress so that it eases
|
|
||||||
// smoothly rather than immediately resetting
|
|
||||||
if( progressBefore > 0.8 && this.progress < 0.2 ) {
|
|
||||||
this.progressOffset = this.progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
if( this.playing ) {
|
|
||||||
requestAnimationFrame( this.animate.bind( this ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the current progress and playback state.
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
|
|
||||||
let progress = this.playing ? this.progress : 0,
|
|
||||||
radius = ( this.diameter2 ) - this.thickness,
|
|
||||||
x = this.diameter2,
|
|
||||||
y = this.diameter2,
|
|
||||||
iconSize = 28;
|
|
||||||
|
|
||||||
// Ease towards 1
|
|
||||||
this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
|
|
||||||
|
|
||||||
const endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
|
|
||||||
const startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
|
|
||||||
|
|
||||||
this.context.save();
|
|
||||||
this.context.clearRect( 0, 0, this.diameter, this.diameter );
|
|
||||||
|
|
||||||
// Solid background color
|
|
||||||
this.context.beginPath();
|
|
||||||
this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
|
|
||||||
this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
|
|
||||||
this.context.fill();
|
|
||||||
|
|
||||||
// Draw progress track
|
|
||||||
this.context.beginPath();
|
|
||||||
this.context.arc( x, y, radius, 0, Math.PI * 2, false );
|
|
||||||
this.context.lineWidth = this.thickness;
|
|
||||||
this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
|
|
||||||
this.context.stroke();
|
|
||||||
|
|
||||||
if( this.playing ) {
|
|
||||||
// Draw progress on top of track
|
|
||||||
this.context.beginPath();
|
|
||||||
this.context.arc( x, y, radius, startAngle, endAngle, false );
|
|
||||||
this.context.lineWidth = this.thickness;
|
|
||||||
this.context.strokeStyle = '#fff';
|
|
||||||
this.context.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
|
|
||||||
|
|
||||||
// Draw play/pause icons
|
|
||||||
if( this.playing ) {
|
|
||||||
this.context.fillStyle = '#fff';
|
|
||||||
this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
|
|
||||||
this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.context.beginPath();
|
|
||||||
this.context.translate( 4, 0 );
|
|
||||||
this.context.moveTo( 0, 0 );
|
|
||||||
this.context.lineTo( iconSize - 4, iconSize / 2 );
|
|
||||||
this.context.lineTo( 0, iconSize );
|
|
||||||
this.context.fillStyle = '#fff';
|
|
||||||
this.context.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.restore();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
on( type, listener ) {
|
|
||||||
this.canvas.addEventListener( type, listener, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
off( type, listener ) {
|
|
||||||
this.canvas.removeEventListener( type, listener, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.playing = false;
|
|
||||||
|
|
||||||
if( this.canvas.parentNode ) {
|
|
||||||
this.container.removeChild( this.canvas );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
330
js/config.js
@ -1,330 +0,0 @@
|
|||||||
/**
|
|
||||||
* The default reveal.js config object.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
|
|
||||||
// The "normal" size of the presentation, aspect ratio will be preserved
|
|
||||||
// when the presentation is scaled to fit different resolutions
|
|
||||||
width: 960,
|
|
||||||
height: 700,
|
|
||||||
|
|
||||||
// Factor of the display size that should remain empty around the content
|
|
||||||
margin: 0.04,
|
|
||||||
|
|
||||||
// Bounds for smallest/largest possible scale to apply to content
|
|
||||||
minScale: 0.2,
|
|
||||||
maxScale: 2.0,
|
|
||||||
|
|
||||||
// Display presentation control arrows
|
|
||||||
controls: true,
|
|
||||||
|
|
||||||
// Help the user learn the controls by providing hints, for example by
|
|
||||||
// bouncing the down arrow when they first encounter a vertical slide
|
|
||||||
controlsTutorial: true,
|
|
||||||
|
|
||||||
// Determines where controls appear, "edges" or "bottom-right"
|
|
||||||
controlsLayout: 'bottom-right',
|
|
||||||
|
|
||||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
|
||||||
// or "visible"
|
|
||||||
controlsBackArrows: 'faded',
|
|
||||||
|
|
||||||
// Display a presentation progress bar
|
|
||||||
progress: true,
|
|
||||||
|
|
||||||
// Display the page number of the current slide
|
|
||||||
// - true: Show slide number
|
|
||||||
// - false: Hide slide number
|
|
||||||
//
|
|
||||||
// Can optionally be set as a string that specifies the number formatting:
|
|
||||||
// - "h.v": Horizontal . vertical slide number (default)
|
|
||||||
// - "h/v": Horizontal / vertical slide number
|
|
||||||
// - "c": Flattened slide number
|
|
||||||
// - "c/t": Flattened slide number / total slides
|
|
||||||
//
|
|
||||||
// Alternatively, you can provide a function that returns the slide
|
|
||||||
// number for the current slide. The function should take in a slide
|
|
||||||
// object and return an array with one string [slideNumber] or
|
|
||||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
|
||||||
slideNumber: false,
|
|
||||||
|
|
||||||
// Can be used to limit the contexts in which the slide number appears
|
|
||||||
// - "all": Always show the slide number
|
|
||||||
// - "print": Only when printing to PDF
|
|
||||||
// - "speaker": Only in the speaker view
|
|
||||||
showSlideNumber: 'all',
|
|
||||||
|
|
||||||
// Use 1 based indexing for # links to match slide number (default is zero
|
|
||||||
// based)
|
|
||||||
hashOneBasedIndex: false,
|
|
||||||
|
|
||||||
// Add the current slide number to the URL hash so that reloading the
|
|
||||||
// page/copying the URL will return you to the same slide
|
|
||||||
hash: false,
|
|
||||||
|
|
||||||
// Flags if we should monitor the hash and change slides accordingly
|
|
||||||
respondToHashChanges: true,
|
|
||||||
|
|
||||||
// Enable support for jump-to-slide navigation shortcuts
|
|
||||||
jumpToSlide: true,
|
|
||||||
|
|
||||||
// Push each slide change to the browser history. Implies `hash: true`
|
|
||||||
history: false,
|
|
||||||
|
|
||||||
// Enable keyboard shortcuts for navigation
|
|
||||||
keyboard: true,
|
|
||||||
|
|
||||||
// Optional function that blocks keyboard events when retuning false
|
|
||||||
//
|
|
||||||
// If you set this to 'focused', we will only capture keyboard events
|
|
||||||
// for embedded decks when they are in focus
|
|
||||||
keyboardCondition: null,
|
|
||||||
|
|
||||||
// Disables the default reveal.js slide layout (scaling and centering)
|
|
||||||
// so that you can use custom CSS layout
|
|
||||||
disableLayout: false,
|
|
||||||
|
|
||||||
// Enable the slide overview mode
|
|
||||||
overview: true,
|
|
||||||
|
|
||||||
// Vertical centering of slides
|
|
||||||
center: true,
|
|
||||||
|
|
||||||
// Enables touch navigation on devices with touch input
|
|
||||||
touch: true,
|
|
||||||
|
|
||||||
// Loop the presentation
|
|
||||||
loop: false,
|
|
||||||
|
|
||||||
// Change the presentation direction to be RTL
|
|
||||||
rtl: false,
|
|
||||||
|
|
||||||
// Changes the behavior of our navigation directions.
|
|
||||||
//
|
|
||||||
// "default"
|
|
||||||
// Left/right arrow keys step between horizontal slides, up/down
|
|
||||||
// arrow keys step between vertical slides. Space key steps through
|
|
||||||
// all slides (both horizontal and vertical).
|
|
||||||
//
|
|
||||||
// "linear"
|
|
||||||
// Removes the up/down arrows. Left/right arrows step through all
|
|
||||||
// slides (both horizontal and vertical).
|
|
||||||
//
|
|
||||||
// "grid"
|
|
||||||
// When this is enabled, stepping left/right from a vertical stack
|
|
||||||
// to an adjacent vertical stack will land you at the same vertical
|
|
||||||
// index.
|
|
||||||
//
|
|
||||||
// Consider a deck with six slides ordered in two vertical stacks:
|
|
||||||
// 1.1 2.1
|
|
||||||
// 1.2 2.2
|
|
||||||
// 1.3 2.3
|
|
||||||
//
|
|
||||||
// If you're on slide 1.3 and navigate right, you will normally move
|
|
||||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
|
||||||
// from 1.3 -> 2.3.
|
|
||||||
navigationMode: 'default',
|
|
||||||
|
|
||||||
// Randomizes the order of slides each time the presentation loads
|
|
||||||
shuffle: false,
|
|
||||||
|
|
||||||
// Turns fragments on and off globally
|
|
||||||
fragments: true,
|
|
||||||
|
|
||||||
// Flags whether to include the current fragment in the URL,
|
|
||||||
// so that reloading brings you to the same fragment position
|
|
||||||
fragmentInURL: true,
|
|
||||||
|
|
||||||
// Flags if the presentation is running in an embedded mode,
|
|
||||||
// i.e. contained within a limited portion of the screen
|
|
||||||
embedded: false,
|
|
||||||
|
|
||||||
// Flags if we should show a help overlay when the question-mark
|
|
||||||
// key is pressed
|
|
||||||
help: true,
|
|
||||||
|
|
||||||
// Flags if it should be possible to pause the presentation (blackout)
|
|
||||||
pause: true,
|
|
||||||
|
|
||||||
// Flags if speaker notes should be visible to all viewers
|
|
||||||
showNotes: false,
|
|
||||||
|
|
||||||
// Flags if slides with data-visibility="hidden" should be kep visible
|
|
||||||
showHiddenSlides: false,
|
|
||||||
|
|
||||||
// Global override for autoplaying embedded media (video/audio/iframe)
|
|
||||||
// - null: Media will only autoplay if data-autoplay is present
|
|
||||||
// - true: All media will autoplay, regardless of individual setting
|
|
||||||
// - false: No media will autoplay, regardless of individual setting
|
|
||||||
autoPlayMedia: null,
|
|
||||||
|
|
||||||
// Global override for preloading lazy-loaded iframes
|
|
||||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
|
||||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
|
||||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
|
||||||
// - false: All iframes with data-src will be loaded only when visible
|
|
||||||
preloadIframes: null,
|
|
||||||
|
|
||||||
// Can be used to globally disable auto-animation
|
|
||||||
autoAnimate: true,
|
|
||||||
|
|
||||||
// Optionally provide a custom element matcher that will be
|
|
||||||
// used to dictate which elements we can animate between.
|
|
||||||
autoAnimateMatcher: null,
|
|
||||||
|
|
||||||
// Default settings for our auto-animate transitions, can be
|
|
||||||
// overridden per-slide or per-element via data arguments
|
|
||||||
autoAnimateEasing: 'ease',
|
|
||||||
autoAnimateDuration: 1.0,
|
|
||||||
autoAnimateUnmatched: true,
|
|
||||||
|
|
||||||
// CSS properties that can be auto-animated. Position & scale
|
|
||||||
// is matched separately so there's no need to include styles
|
|
||||||
// like top/right/bottom/left, width/height or margin.
|
|
||||||
autoAnimateStyles: [
|
|
||||||
'opacity',
|
|
||||||
'color',
|
|
||||||
'background-color',
|
|
||||||
'padding',
|
|
||||||
'font-size',
|
|
||||||
'line-height',
|
|
||||||
'letter-spacing',
|
|
||||||
'border-width',
|
|
||||||
'border-color',
|
|
||||||
'border-radius',
|
|
||||||
'outline',
|
|
||||||
'outline-offset'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Controls automatic progression to the next slide
|
|
||||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
|
||||||
// is present on the current slide or fragment
|
|
||||||
// - 1+: All slides will progress automatically at the given interval
|
|
||||||
// - false: No auto-sliding, even if data-autoslide is present
|
|
||||||
autoSlide: 0,
|
|
||||||
|
|
||||||
// Stop auto-sliding after user input
|
|
||||||
autoSlideStoppable: true,
|
|
||||||
|
|
||||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
|
||||||
autoSlideMethod: null,
|
|
||||||
|
|
||||||
// Specify the average time in seconds that you think you will spend
|
|
||||||
// presenting each slide. This is used to show a pacing timer in the
|
|
||||||
// speaker view
|
|
||||||
defaultTiming: null,
|
|
||||||
|
|
||||||
// Enable slide navigation via mouse wheel
|
|
||||||
mouseWheel: false,
|
|
||||||
|
|
||||||
// Opens links in an iframe preview overlay
|
|
||||||
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
|
|
||||||
// individually
|
|
||||||
previewLinks: false,
|
|
||||||
|
|
||||||
// Exposes the reveal.js API through window.postMessage
|
|
||||||
postMessage: true,
|
|
||||||
|
|
||||||
// Dispatches all reveal.js events to the parent window through postMessage
|
|
||||||
postMessageEvents: false,
|
|
||||||
|
|
||||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
|
||||||
focusBodyOnPageVisibilityChange: true,
|
|
||||||
|
|
||||||
// Transition style
|
|
||||||
transition: 'slide', // none/fade/slide/convex/concave/zoom
|
|
||||||
|
|
||||||
// Transition speed
|
|
||||||
transitionSpeed: 'default', // default/fast/slow
|
|
||||||
|
|
||||||
// Transition style for full page slide backgrounds
|
|
||||||
backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
|
|
||||||
|
|
||||||
// Parallax background image
|
|
||||||
parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
|
|
||||||
|
|
||||||
// Parallax background size
|
|
||||||
parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
|
|
||||||
|
|
||||||
// Parallax background repeat
|
|
||||||
parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
|
|
||||||
|
|
||||||
// Parallax background position
|
|
||||||
parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
|
|
||||||
|
|
||||||
// Amount of pixels to move the parallax background per slide step
|
|
||||||
parallaxBackgroundHorizontal: null,
|
|
||||||
parallaxBackgroundVertical: null,
|
|
||||||
|
|
||||||
// Can be used to initialize reveal.js in one of the following views:
|
|
||||||
// - print: Render the presentation so that it can be printed to PDF
|
|
||||||
// - scroll: Show the presentation as a tall scrollable page with scroll
|
|
||||||
// triggered animations
|
|
||||||
view: null,
|
|
||||||
|
|
||||||
// Adjusts the height of each slide in the scroll view.
|
|
||||||
// - full: Each slide is as tall as the viewport
|
|
||||||
// - compact: Slides are as small as possible, allowing multiple slides
|
|
||||||
// to be visible in parallel on tall devices
|
|
||||||
scrollLayout: 'full',
|
|
||||||
|
|
||||||
// Control how scroll snapping works in the scroll view.
|
|
||||||
// - false: No snapping, scrolling is continuous
|
|
||||||
// - proximity: Snap when close to a slide
|
|
||||||
// - mandatory: Always snap to the closest slide
|
|
||||||
//
|
|
||||||
// Only applies to presentations in scroll view.
|
|
||||||
scrollSnap: 'mandatory',
|
|
||||||
|
|
||||||
// Enables and configure the scroll view progress bar.
|
|
||||||
// - 'auto': Show the scrollbar while scrolling, hide while idle
|
|
||||||
// - true: Always show the scrollbar
|
|
||||||
// - false: Never show the scrollbar
|
|
||||||
scrollProgress: 'auto',
|
|
||||||
|
|
||||||
// Automatically activate the scroll view when we the viewport falls
|
|
||||||
// below the given width.
|
|
||||||
scrollActivationWidth: 435,
|
|
||||||
|
|
||||||
// The maximum number of pages a single slide can expand onto when printing
|
|
||||||
// to PDF, unlimited by default
|
|
||||||
pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
|
|
||||||
|
|
||||||
// Prints each fragment on a separate slide
|
|
||||||
pdfSeparateFragments: true,
|
|
||||||
|
|
||||||
// Offset used to reduce the height of content within exported PDF pages.
|
|
||||||
// This exists to account for environment differences based on how you
|
|
||||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
|
||||||
// on precisely the total height of the document whereas in-browser
|
|
||||||
// printing has to end one pixel before.
|
|
||||||
pdfPageHeightOffset: -1,
|
|
||||||
|
|
||||||
// Number of slides away from the current that are visible
|
|
||||||
viewDistance: 3,
|
|
||||||
|
|
||||||
// Number of slides away from the current that are visible on mobile
|
|
||||||
// devices. It is advisable to set this to a lower number than
|
|
||||||
// viewDistance in order to save resources.
|
|
||||||
mobileViewDistance: 2,
|
|
||||||
|
|
||||||
// The display mode that will be used to show slides
|
|
||||||
display: 'block',
|
|
||||||
|
|
||||||
// Hide cursor if inactive
|
|
||||||
hideInactiveCursor: true,
|
|
||||||
|
|
||||||
// Time before the cursor is hidden (in ms)
|
|
||||||
hideCursorTime: 5000,
|
|
||||||
|
|
||||||
// Should we automatically sort and set indices for fragments
|
|
||||||
// at each sync? (See Reveal.sync)
|
|
||||||
sortFragmentsOnSync: true,
|
|
||||||
|
|
||||||
// Script dependencies to load
|
|
||||||
dependencies: [],
|
|
||||||
|
|
||||||
// Plugin objects to register and use for this presentation
|
|
||||||
plugins: []
|
|
||||||
|
|
||||||
}
|
|
@ -1,640 +0,0 @@
|
|||||||
import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'
|
|
||||||
import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'
|
|
||||||
|
|
||||||
// Counter used to generate unique IDs for auto-animated elements
|
|
||||||
let autoAnimateCounter = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically animates matching elements across
|
|
||||||
* slides with the [data-auto-animate] attribute.
|
|
||||||
*/
|
|
||||||
export default class AutoAnimate {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs an auto-animation between the given slides.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} fromSlide
|
|
||||||
* @param {HTMLElement} toSlide
|
|
||||||
*/
|
|
||||||
run( fromSlide, toSlide ) {
|
|
||||||
|
|
||||||
// Clean up after prior animations
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
let allSlides = this.Reveal.getSlides();
|
|
||||||
let toSlideIndex = allSlides.indexOf( toSlide );
|
|
||||||
let fromSlideIndex = allSlides.indexOf( fromSlide );
|
|
||||||
|
|
||||||
// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value
|
|
||||||
// (including null if absent on both) and that data-auto-animate-restart isn't set on the
|
|
||||||
// physically latter slide (independent of slide direction)
|
|
||||||
if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
|
|
||||||
&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' )
|
|
||||||
&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {
|
|
||||||
|
|
||||||
// Create a new auto-animate sheet
|
|
||||||
this.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet();
|
|
||||||
|
|
||||||
let animationOptions = this.getAutoAnimateOptions( toSlide );
|
|
||||||
|
|
||||||
// Set our starting state
|
|
||||||
fromSlide.dataset.autoAnimate = 'pending';
|
|
||||||
toSlide.dataset.autoAnimate = 'pending';
|
|
||||||
|
|
||||||
// Flag the navigation direction, needed for fragment buildup
|
|
||||||
animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';
|
|
||||||
|
|
||||||
// If the from-slide is hidden because it has moved outside
|
|
||||||
// the view distance, we need to temporarily show it while
|
|
||||||
// measuring
|
|
||||||
let fromSlideIsHidden = fromSlide.style.display === 'none';
|
|
||||||
if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;
|
|
||||||
|
|
||||||
// Inject our auto-animate styles for this transition
|
|
||||||
let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {
|
|
||||||
return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );
|
|
||||||
} );
|
|
||||||
|
|
||||||
if( fromSlideIsHidden ) fromSlide.style.display = 'none';
|
|
||||||
|
|
||||||
// Animate unmatched elements, if enabled
|
|
||||||
if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {
|
|
||||||
|
|
||||||
// Our default timings for unmatched elements
|
|
||||||
let defaultUnmatchedDuration = animationOptions.duration * 0.8,
|
|
||||||
defaultUnmatchedDelay = animationOptions.duration * 0.2;
|
|
||||||
|
|
||||||
this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => {
|
|
||||||
|
|
||||||
let unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions );
|
|
||||||
let id = 'unmatched';
|
|
||||||
|
|
||||||
// If there is a duration or delay set specifically for this
|
|
||||||
// element our unmatched elements should adhere to those
|
|
||||||
if( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) {
|
|
||||||
id = 'unmatched-' + autoAnimateCounter++;
|
|
||||||
css.push( `[data-auto-animate="running"] [data-auto-animate-target="${id}"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` );
|
|
||||||
}
|
|
||||||
|
|
||||||
unmatchedElement.dataset.autoAnimateTarget = id;
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
// Our default transition for unmatched elements
|
|
||||||
css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setting the whole chunk of CSS at once is the most
|
|
||||||
// efficient way to do this. Using sheet.insertRule
|
|
||||||
// is multiple factors slower.
|
|
||||||
this.autoAnimateStyleSheet.innerHTML = css.join( '' );
|
|
||||||
|
|
||||||
// Start the animation next cycle
|
|
||||||
requestAnimationFrame( () => {
|
|
||||||
if( this.autoAnimateStyleSheet ) {
|
|
||||||
// This forces our newly injected styles to be applied in Firefox
|
|
||||||
getComputedStyle( this.autoAnimateStyleSheet ).fontWeight;
|
|
||||||
|
|
||||||
toSlide.dataset.autoAnimate = 'running';
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
type: 'autoanimate',
|
|
||||||
data: {
|
|
||||||
fromSlide,
|
|
||||||
toSlide,
|
|
||||||
sheet: this.autoAnimateStyleSheet
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rolls back all changes that we've made to the DOM so
|
|
||||||
* that as part of animating.
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
|
|
||||||
// Reset slides
|
|
||||||
queryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=""])' ).forEach( element => {
|
|
||||||
element.dataset.autoAnimate = '';
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Reset elements
|
|
||||||
queryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => {
|
|
||||||
delete element.dataset.autoAnimateTarget;
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Remove the animation sheet
|
|
||||||
if( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) {
|
|
||||||
this.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet );
|
|
||||||
this.autoAnimateStyleSheet = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a FLIP animation where the `to` element starts out
|
|
||||||
* in the `from` element position and animates to its original
|
|
||||||
* state.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} from
|
|
||||||
* @param {HTMLElement} to
|
|
||||||
* @param {Object} elementOptions Options for this element pair
|
|
||||||
* @param {Object} animationOptions Options set at the slide level
|
|
||||||
* @param {String} id Unique ID that we can use to identify this
|
|
||||||
* auto-animate element in the DOM
|
|
||||||
*/
|
|
||||||
autoAnimateElements( from, to, elementOptions, animationOptions, id ) {
|
|
||||||
|
|
||||||
// 'from' elements are given a data-auto-animate-target with no value,
|
|
||||||
// 'to' elements are are given a data-auto-animate-target with an ID
|
|
||||||
from.dataset.autoAnimateTarget = '';
|
|
||||||
to.dataset.autoAnimateTarget = id;
|
|
||||||
|
|
||||||
// Each element may override any of the auto-animate options
|
|
||||||
// like transition easing, duration and delay via data-attributes
|
|
||||||
let options = this.getAutoAnimateOptions( to, animationOptions );
|
|
||||||
|
|
||||||
// If we're using a custom element matcher the element options
|
|
||||||
// may contain additional transition overrides
|
|
||||||
if( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay;
|
|
||||||
if( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration;
|
|
||||||
if( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing;
|
|
||||||
|
|
||||||
let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),
|
|
||||||
toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );
|
|
||||||
|
|
||||||
// Maintain fragment visibility for matching elements when
|
|
||||||
// we're navigating forwards, this way the viewer won't need
|
|
||||||
// to step through the same fragments twice
|
|
||||||
if( to.classList.contains( 'fragment' ) ) {
|
|
||||||
|
|
||||||
// Don't auto-animate the opacity of fragments to avoid
|
|
||||||
// conflicts with fragment animations
|
|
||||||
delete toProps.styles['opacity'];
|
|
||||||
|
|
||||||
if( from.classList.contains( 'fragment' ) ) {
|
|
||||||
|
|
||||||
let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
|
||||||
let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
|
||||||
|
|
||||||
// Only skip the fragment if the fragment animation style
|
|
||||||
// remains unchanged
|
|
||||||
if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {
|
|
||||||
to.classList.add( 'visible', 'disabled' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// If translation and/or scaling are enabled, css transform
|
|
||||||
// the 'to' element so that it matches the position and size
|
|
||||||
// of the 'from' element
|
|
||||||
if( elementOptions.translate !== false || elementOptions.scale !== false ) {
|
|
||||||
|
|
||||||
let presentationScale = this.Reveal.getScale();
|
|
||||||
|
|
||||||
let delta = {
|
|
||||||
x: ( fromProps.x - toProps.x ) / presentationScale,
|
|
||||||
y: ( fromProps.y - toProps.y ) / presentationScale,
|
|
||||||
scaleX: fromProps.width / toProps.width,
|
|
||||||
scaleY: fromProps.height / toProps.height
|
|
||||||
};
|
|
||||||
|
|
||||||
// Limit decimal points to avoid 0.0001px blur and stutter
|
|
||||||
delta.x = Math.round( delta.x * 1000 ) / 1000;
|
|
||||||
delta.y = Math.round( delta.y * 1000 ) / 1000;
|
|
||||||
delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
|
|
||||||
delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
|
|
||||||
|
|
||||||
let translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ),
|
|
||||||
scale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 );
|
|
||||||
|
|
||||||
// No need to transform if nothing's changed
|
|
||||||
if( translate || scale ) {
|
|
||||||
|
|
||||||
let transform = [];
|
|
||||||
|
|
||||||
if( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` );
|
|
||||||
if( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` );
|
|
||||||
|
|
||||||
fromProps.styles['transform'] = transform.join( ' ' );
|
|
||||||
fromProps.styles['transform-origin'] = 'top left';
|
|
||||||
|
|
||||||
toProps.styles['transform'] = 'none';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all unchanged 'to' styles
|
|
||||||
for( let propertyName in toProps.styles ) {
|
|
||||||
const toValue = toProps.styles[propertyName];
|
|
||||||
const fromValue = fromProps.styles[propertyName];
|
|
||||||
|
|
||||||
if( toValue === fromValue ) {
|
|
||||||
delete toProps.styles[propertyName];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// If these property values were set via a custom matcher providing
|
|
||||||
// an explicit 'from' and/or 'to' value, we always inject those values.
|
|
||||||
if( toValue.explicitValue === true ) {
|
|
||||||
toProps.styles[propertyName] = toValue.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( fromValue.explicitValue === true ) {
|
|
||||||
fromProps.styles[propertyName] = fromValue.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let css = '';
|
|
||||||
|
|
||||||
let toStyleProperties = Object.keys( toProps.styles );
|
|
||||||
|
|
||||||
// Only create animate this element IF at least one style
|
|
||||||
// property has changed
|
|
||||||
if( toStyleProperties.length > 0 ) {
|
|
||||||
|
|
||||||
// Instantly move to the 'from' state
|
|
||||||
fromProps.styles['transition'] = 'none';
|
|
||||||
|
|
||||||
// Animate towards the 'to' state
|
|
||||||
toProps.styles['transition'] = `all ${options.duration}s ${options.easing} ${options.delay}s`;
|
|
||||||
toProps.styles['transition-property'] = toStyleProperties.join( ', ' );
|
|
||||||
toProps.styles['will-change'] = toStyleProperties.join( ', ' );
|
|
||||||
|
|
||||||
// Build up our custom CSS. We need to override inline styles
|
|
||||||
// so we need to make our styles vErY IMPORTANT!1!!
|
|
||||||
let fromCSS = Object.keys( fromProps.styles ).map( propertyName => {
|
|
||||||
return propertyName + ': ' + fromProps.styles[propertyName] + ' !important;';
|
|
||||||
} ).join( '' );
|
|
||||||
|
|
||||||
let toCSS = Object.keys( toProps.styles ).map( propertyName => {
|
|
||||||
return propertyName + ': ' + toProps.styles[propertyName] + ' !important;';
|
|
||||||
} ).join( '' );
|
|
||||||
|
|
||||||
css = '[data-auto-animate-target="'+ id +'"] {'+ fromCSS +'}' +
|
|
||||||
'[data-auto-animate="running"] [data-auto-animate-target="'+ id +'"] {'+ toCSS +'}';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return css;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the auto-animate options for the given element.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element Element to pick up options
|
|
||||||
* from, either a slide or an animation target
|
|
||||||
* @param {Object} [inheritedOptions] Optional set of existing
|
|
||||||
* options
|
|
||||||
*/
|
|
||||||
getAutoAnimateOptions( element, inheritedOptions ) {
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
easing: this.Reveal.getConfig().autoAnimateEasing,
|
|
||||||
duration: this.Reveal.getConfig().autoAnimateDuration,
|
|
||||||
delay: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
options = extend( options, inheritedOptions );
|
|
||||||
|
|
||||||
// Inherit options from parent elements
|
|
||||||
if( element.parentNode ) {
|
|
||||||
let autoAnimatedParent = closest( element.parentNode, '[data-auto-animate-target]' );
|
|
||||||
if( autoAnimatedParent ) {
|
|
||||||
options = this.getAutoAnimateOptions( autoAnimatedParent, options );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( element.dataset.autoAnimateEasing ) {
|
|
||||||
options.easing = element.dataset.autoAnimateEasing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( element.dataset.autoAnimateDuration ) {
|
|
||||||
options.duration = parseFloat( element.dataset.autoAnimateDuration );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( element.dataset.autoAnimateDelay ) {
|
|
||||||
options.delay = parseFloat( element.dataset.autoAnimateDelay );
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object containing all of the properties
|
|
||||||
* that can be auto-animated for the given element and
|
|
||||||
* their current computed values.
|
|
||||||
*
|
|
||||||
* @param {String} direction 'from' or 'to'
|
|
||||||
*/
|
|
||||||
getAutoAnimatableProperties( direction, element, elementOptions ) {
|
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
|
|
||||||
let properties = { styles: [] };
|
|
||||||
|
|
||||||
// Position and size
|
|
||||||
if( elementOptions.translate !== false || elementOptions.scale !== false ) {
|
|
||||||
let bounds;
|
|
||||||
|
|
||||||
// Custom auto-animate may optionally return a custom tailored
|
|
||||||
// measurement function
|
|
||||||
if( typeof elementOptions.measure === 'function' ) {
|
|
||||||
bounds = elementOptions.measure( element );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if( config.center ) {
|
|
||||||
// More precise, but breaks when used in combination
|
|
||||||
// with zoom for scaling the deck ¯\_(ツ)_/¯
|
|
||||||
bounds = element.getBoundingClientRect();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let scale = this.Reveal.getScale();
|
|
||||||
bounds = {
|
|
||||||
x: element.offsetLeft * scale,
|
|
||||||
y: element.offsetTop * scale,
|
|
||||||
width: element.offsetWidth * scale,
|
|
||||||
height: element.offsetHeight * scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
properties.x = bounds.x;
|
|
||||||
properties.y = bounds.y;
|
|
||||||
properties.width = bounds.width;
|
|
||||||
properties.height = bounds.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStyles = getComputedStyle( element );
|
|
||||||
|
|
||||||
// CSS styles
|
|
||||||
( elementOptions.styles || config.autoAnimateStyles ).forEach( style => {
|
|
||||||
let value;
|
|
||||||
|
|
||||||
// `style` is either the property name directly, or an object
|
|
||||||
// definition of a style property
|
|
||||||
if( typeof style === 'string' ) style = { property: style };
|
|
||||||
|
|
||||||
if( typeof style.from !== 'undefined' && direction === 'from' ) {
|
|
||||||
value = { value: style.from, explicitValue: true };
|
|
||||||
}
|
|
||||||
else if( typeof style.to !== 'undefined' && direction === 'to' ) {
|
|
||||||
value = { value: style.to, explicitValue: true };
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use a unitless value for line-height so that it inherits properly
|
|
||||||
if( style.property === 'line-height' ) {
|
|
||||||
value = parseFloat( computedStyles['line-height'] ) / parseFloat( computedStyles['font-size'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( isNaN(value) ) {
|
|
||||||
value = computedStyles[style.property];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( value !== '' ) {
|
|
||||||
properties.styles[style.property] = value;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
return properties;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of all element pairs that we can animate
|
|
||||||
* between the given slides.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} fromSlide
|
|
||||||
* @param {HTMLElement} toSlide
|
|
||||||
*
|
|
||||||
* @return {Array} Each value is an array where [0] is
|
|
||||||
* the element we're animating from and [1] is the
|
|
||||||
* element we're animating to
|
|
||||||
*/
|
|
||||||
getAutoAnimatableElements( fromSlide, toSlide ) {
|
|
||||||
|
|
||||||
let matcher = typeof this.Reveal.getConfig().autoAnimateMatcher === 'function' ? this.Reveal.getConfig().autoAnimateMatcher : this.getAutoAnimatePairs;
|
|
||||||
|
|
||||||
let pairs = matcher.call( this, fromSlide, toSlide );
|
|
||||||
|
|
||||||
let reserved = [];
|
|
||||||
|
|
||||||
// Remove duplicate pairs
|
|
||||||
return pairs.filter( ( pair, index ) => {
|
|
||||||
if( reserved.indexOf( pair.to ) === -1 ) {
|
|
||||||
reserved.push( pair.to );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies matching elements between slides.
|
|
||||||
*
|
|
||||||
* You can specify a custom matcher function by using
|
|
||||||
* the `autoAnimateMatcher` config option.
|
|
||||||
*/
|
|
||||||
getAutoAnimatePairs( fromSlide, toSlide ) {
|
|
||||||
|
|
||||||
let pairs = [];
|
|
||||||
|
|
||||||
const codeNodes = 'pre';
|
|
||||||
const textNodes = 'h1, h2, h3, h4, h5, h6, p, li';
|
|
||||||
const mediaNodes = 'img, video, iframe';
|
|
||||||
|
|
||||||
// Explicit matches via data-id
|
|
||||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => {
|
|
||||||
return node.nodeName + ':::' + node.getAttribute( 'data-id' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Text
|
|
||||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => {
|
|
||||||
return node.nodeName + ':::' + node.innerText;
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Media
|
|
||||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, node => {
|
|
||||||
return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Code
|
|
||||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => {
|
|
||||||
return node.nodeName + ':::' + node.innerText;
|
|
||||||
} );
|
|
||||||
|
|
||||||
pairs.forEach( pair => {
|
|
||||||
// Disable scale transformations on text nodes, we transition
|
|
||||||
// each individual text property instead
|
|
||||||
if( matches( pair.from, textNodes ) ) {
|
|
||||||
pair.options = { scale: false };
|
|
||||||
}
|
|
||||||
// Animate individual lines of code
|
|
||||||
else if( matches( pair.from, codeNodes ) ) {
|
|
||||||
|
|
||||||
// Transition the code block's width and height instead of scaling
|
|
||||||
// to prevent its content from being squished
|
|
||||||
pair.options = { scale: false, styles: [ 'width', 'height' ] };
|
|
||||||
|
|
||||||
// Lines of code
|
|
||||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', node => {
|
|
||||||
return node.textContent;
|
|
||||||
}, {
|
|
||||||
scale: false,
|
|
||||||
styles: [],
|
|
||||||
measure: this.getLocalBoundingBox.bind( this )
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Line numbers
|
|
||||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-numbers[data-line-number]', node => {
|
|
||||||
return node.getAttribute( 'data-line-number' );
|
|
||||||
}, {
|
|
||||||
scale: false,
|
|
||||||
styles: [ 'width' ],
|
|
||||||
measure: this.getLocalBoundingBox.bind( this )
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
return pairs;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method which returns a bounding box based on
|
|
||||||
* the given elements offset coordinates.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @return {Object} x, y, width, height
|
|
||||||
*/
|
|
||||||
getLocalBoundingBox( element ) {
|
|
||||||
|
|
||||||
const presentationScale = this.Reveal.getScale();
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100,
|
|
||||||
y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100,
|
|
||||||
width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100,
|
|
||||||
height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds matching elements between two slides.
|
|
||||||
*
|
|
||||||
* @param {Array} pairs List of pairs to push matches to
|
|
||||||
* @param {HTMLElement} fromScope Scope within the from element exists
|
|
||||||
* @param {HTMLElement} toScope Scope within the to element exists
|
|
||||||
* @param {String} selector CSS selector of the element to match
|
|
||||||
* @param {Function} serializer A function that accepts an element and returns
|
|
||||||
* a stringified ID based on its contents
|
|
||||||
* @param {Object} animationOptions Optional config options for this pair
|
|
||||||
*/
|
|
||||||
findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) {
|
|
||||||
|
|
||||||
let fromMatches = {};
|
|
||||||
let toMatches = {};
|
|
||||||
|
|
||||||
[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
|
|
||||||
const key = serializer( element );
|
|
||||||
if( typeof key === 'string' && key.length ) {
|
|
||||||
fromMatches[key] = fromMatches[key] || [];
|
|
||||||
fromMatches[key].push( element );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
[].slice.call( toScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
|
|
||||||
const key = serializer( element );
|
|
||||||
toMatches[key] = toMatches[key] || [];
|
|
||||||
toMatches[key].push( element );
|
|
||||||
|
|
||||||
let fromElement;
|
|
||||||
|
|
||||||
// Retrieve the 'from' element
|
|
||||||
if( fromMatches[key] ) {
|
|
||||||
const primaryIndex = toMatches[key].length - 1;
|
|
||||||
const secondaryIndex = fromMatches[key].length - 1;
|
|
||||||
|
|
||||||
// If there are multiple identical from elements, retrieve
|
|
||||||
// the one at the same index as our to-element.
|
|
||||||
if( fromMatches[key][ primaryIndex ] ) {
|
|
||||||
fromElement = fromMatches[key][ primaryIndex ];
|
|
||||||
fromMatches[key][ primaryIndex ] = null;
|
|
||||||
}
|
|
||||||
// If there are no matching from-elements at the same index,
|
|
||||||
// use the last one.
|
|
||||||
else if( fromMatches[key][ secondaryIndex ] ) {
|
|
||||||
fromElement = fromMatches[key][ secondaryIndex ];
|
|
||||||
fromMatches[key][ secondaryIndex ] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've got a matching pair, push it to the list of pairs
|
|
||||||
if( fromElement ) {
|
|
||||||
pairs.push({
|
|
||||||
from: fromElement,
|
|
||||||
to: element,
|
|
||||||
options: animationOptions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a all elements within the given scope that should
|
|
||||||
* be considered unmatched in an auto-animate transition. If
|
|
||||||
* fading of unmatched elements is turned on, these elements
|
|
||||||
* will fade when going between auto-animate slides.
|
|
||||||
*
|
|
||||||
* Note that parents of auto-animate targets are NOT considered
|
|
||||||
* unmatched since fading them would break the auto-animation.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} rootElement
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
getUnmatchedAutoAnimateElements( rootElement ) {
|
|
||||||
|
|
||||||
return [].slice.call( rootElement.children ).reduce( ( result, element ) => {
|
|
||||||
|
|
||||||
const containsAnimatedElements = element.querySelector( '[data-auto-animate-target]' );
|
|
||||||
|
|
||||||
// The element is unmatched if
|
|
||||||
// - It is not an auto-animate target
|
|
||||||
// - It does not contain any auto-animate targets
|
|
||||||
if( !element.hasAttribute( 'data-auto-animate-target' ) && !containsAnimatedElements ) {
|
|
||||||
result.push( element );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( element.querySelector( '[data-auto-animate-target]' ) ) {
|
|
||||||
result = result.concat( this.getUnmatchedAutoAnimateElements( element ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,439 +0,0 @@
|
|||||||
import { queryAll } from '../utils/util.js'
|
|
||||||
import { colorToRgb, colorBrightness } from '../utils/color.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and updates slide backgrounds.
|
|
||||||
*/
|
|
||||||
export default class Backgrounds {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
|
||||||
this.element.className = 'backgrounds';
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the slide background elements and appends them
|
|
||||||
* to the background container. One element is created per
|
|
||||||
* slide no matter if the given slide has visible background.
|
|
||||||
*/
|
|
||||||
create() {
|
|
||||||
|
|
||||||
// Clear prior backgrounds
|
|
||||||
this.element.innerHTML = '';
|
|
||||||
this.element.classList.add( 'no-transition' );
|
|
||||||
|
|
||||||
// Iterate over all horizontal slides
|
|
||||||
this.Reveal.getHorizontalSlides().forEach( slideh => {
|
|
||||||
|
|
||||||
let backgroundStack = this.createBackground( slideh, this.element );
|
|
||||||
|
|
||||||
// Iterate over all vertical slides
|
|
||||||
queryAll( slideh, 'section' ).forEach( slidev => {
|
|
||||||
|
|
||||||
this.createBackground( slidev, backgroundStack );
|
|
||||||
|
|
||||||
backgroundStack.classList.add( 'stack' );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Add parallax background if specified
|
|
||||||
if( this.Reveal.getConfig().parallaxBackgroundImage ) {
|
|
||||||
|
|
||||||
this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")';
|
|
||||||
this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize;
|
|
||||||
this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat;
|
|
||||||
this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition;
|
|
||||||
|
|
||||||
// Make sure the below properties are set on the element - these properties are
|
|
||||||
// needed for proper transitions to be set on the element via CSS. To remove
|
|
||||||
// annoying background slide-in effect when the presentation starts, apply
|
|
||||||
// these properties after short time delay
|
|
||||||
setTimeout( () => {
|
|
||||||
this.Reveal.getRevealElement().classList.add( 'has-parallax-background' );
|
|
||||||
}, 1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
this.element.style.backgroundImage = '';
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a background for the given slide.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide
|
|
||||||
* @param {HTMLElement} container The element that the background
|
|
||||||
* should be appended to
|
|
||||||
* @return {HTMLElement} New background div
|
|
||||||
*/
|
|
||||||
createBackground( slide, container ) {
|
|
||||||
|
|
||||||
// Main slide background element
|
|
||||||
let element = document.createElement( 'div' );
|
|
||||||
element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
|
|
||||||
|
|
||||||
// Inner background element that wraps images/videos/iframes
|
|
||||||
let contentElement = document.createElement( 'div' );
|
|
||||||
contentElement.className = 'slide-background-content';
|
|
||||||
|
|
||||||
element.appendChild( contentElement );
|
|
||||||
container.appendChild( element );
|
|
||||||
|
|
||||||
slide.slideBackgroundElement = element;
|
|
||||||
slide.slideBackgroundContentElement = contentElement;
|
|
||||||
|
|
||||||
// Syncs the background to reflect all current background settings
|
|
||||||
this.sync( slide );
|
|
||||||
|
|
||||||
return element;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders all of the visual properties of a slide background
|
|
||||||
* based on the various background attributes.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide
|
|
||||||
*/
|
|
||||||
sync( slide ) {
|
|
||||||
|
|
||||||
const element = slide.slideBackgroundElement,
|
|
||||||
contentElement = slide.slideBackgroundContentElement;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
background: slide.getAttribute( 'data-background' ),
|
|
||||||
backgroundSize: slide.getAttribute( 'data-background-size' ),
|
|
||||||
backgroundImage: slide.getAttribute( 'data-background-image' ),
|
|
||||||
backgroundVideo: slide.getAttribute( 'data-background-video' ),
|
|
||||||
backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
|
|
||||||
backgroundColor: slide.getAttribute( 'data-background-color' ),
|
|
||||||
backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
|
|
||||||
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
|
|
||||||
backgroundPosition: slide.getAttribute( 'data-background-position' ),
|
|
||||||
backgroundTransition: slide.getAttribute( 'data-background-transition' ),
|
|
||||||
backgroundOpacity: slide.getAttribute( 'data-background-opacity' ),
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataPreload = slide.hasAttribute( 'data-preload' );
|
|
||||||
|
|
||||||
// Reset the prior background state in case this is not the
|
|
||||||
// initial sync
|
|
||||||
slide.classList.remove( 'has-dark-background' );
|
|
||||||
slide.classList.remove( 'has-light-background' );
|
|
||||||
|
|
||||||
element.removeAttribute( 'data-loaded' );
|
|
||||||
element.removeAttribute( 'data-background-hash' );
|
|
||||||
element.removeAttribute( 'data-background-size' );
|
|
||||||
element.removeAttribute( 'data-background-transition' );
|
|
||||||
element.style.backgroundColor = '';
|
|
||||||
|
|
||||||
contentElement.style.backgroundSize = '';
|
|
||||||
contentElement.style.backgroundRepeat = '';
|
|
||||||
contentElement.style.backgroundPosition = '';
|
|
||||||
contentElement.style.backgroundImage = '';
|
|
||||||
contentElement.style.opacity = '';
|
|
||||||
contentElement.innerHTML = '';
|
|
||||||
|
|
||||||
if( data.background ) {
|
|
||||||
// Auto-wrap image urls in url(...)
|
|
||||||
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
|
|
||||||
slide.setAttribute( 'data-background-image', data.background );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
element.style.background = data.background;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a hash for this combination of background settings.
|
|
||||||
// This is used to determine when two slide backgrounds are
|
|
||||||
// the same.
|
|
||||||
if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
|
|
||||||
element.setAttribute( 'data-background-hash', data.background +
|
|
||||||
data.backgroundSize +
|
|
||||||
data.backgroundImage +
|
|
||||||
data.backgroundVideo +
|
|
||||||
data.backgroundIframe +
|
|
||||||
data.backgroundColor +
|
|
||||||
data.backgroundGradient +
|
|
||||||
data.backgroundRepeat +
|
|
||||||
data.backgroundPosition +
|
|
||||||
data.backgroundTransition +
|
|
||||||
data.backgroundOpacity );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional and optional background properties
|
|
||||||
if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
|
|
||||||
if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
|
|
||||||
if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
|
|
||||||
if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
|
|
||||||
|
|
||||||
if( dataPreload ) element.setAttribute( 'data-preload', '' );
|
|
||||||
|
|
||||||
// Background image options are set on the content wrapper
|
|
||||||
if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
|
|
||||||
if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
|
|
||||||
if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
|
|
||||||
if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
|
|
||||||
|
|
||||||
const contrastClass = this.getContrastClass( slide );
|
|
||||||
|
|
||||||
if( typeof contrastClass === 'string' ) {
|
|
||||||
slide.classList.add( contrastClass );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a class name that can be applied to a slide to indicate
|
|
||||||
* if it has a light or dark background.
|
|
||||||
*
|
|
||||||
* @param {*} slide
|
|
||||||
*
|
|
||||||
* @returns {string|null}
|
|
||||||
*/
|
|
||||||
getContrastClass( slide ) {
|
|
||||||
|
|
||||||
const element = slide.slideBackgroundElement;
|
|
||||||
|
|
||||||
// If this slide has a background color, we add a class that
|
|
||||||
// signals if it is light or dark. If the slide has no background
|
|
||||||
// color, no class will be added
|
|
||||||
let contrastColor = slide.getAttribute( 'data-background-color' );
|
|
||||||
|
|
||||||
// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
|
|
||||||
if( !contrastColor || !colorToRgb( contrastColor ) ) {
|
|
||||||
let computedBackgroundStyle = window.getComputedStyle( element );
|
|
||||||
if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
|
|
||||||
contrastColor = computedBackgroundStyle.backgroundColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( contrastColor ) {
|
|
||||||
const rgb = colorToRgb( contrastColor );
|
|
||||||
|
|
||||||
// Ignore fully transparent backgrounds. Some browsers return
|
|
||||||
// rgba(0,0,0,0) when reading the computed background color of
|
|
||||||
// an element with no background
|
|
||||||
if( rgb && rgb.a !== 0 ) {
|
|
||||||
if( colorBrightness( contrastColor ) < 128 ) {
|
|
||||||
return 'has-dark-background';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 'has-light-background';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bubble the 'has-light-background'/'has-dark-background' classes.
|
|
||||||
*/
|
|
||||||
bubbleSlideContrastClassToElement( slide, target ) {
|
|
||||||
|
|
||||||
[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
|
|
||||||
if( slide.classList.contains( classToBubble ) ) {
|
|
||||||
target.classList.add( classToBubble );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
target.classList.remove( classToBubble );
|
|
||||||
}
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the background elements to reflect the current
|
|
||||||
* slide.
|
|
||||||
*
|
|
||||||
* @param {boolean} includeAll If true, the backgrounds of
|
|
||||||
* all vertical slides (not just the present) will be updated.
|
|
||||||
*/
|
|
||||||
update( includeAll = false ) {
|
|
||||||
|
|
||||||
let currentSlide = this.Reveal.getCurrentSlide();
|
|
||||||
let indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
let currentBackground = null;
|
|
||||||
|
|
||||||
// Reverse past/future classes when in RTL mode
|
|
||||||
let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past',
|
|
||||||
horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future';
|
|
||||||
|
|
||||||
// Update the classes of all backgrounds to match the
|
|
||||||
// states of their slides (past/present/future)
|
|
||||||
Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {
|
|
||||||
|
|
||||||
backgroundh.classList.remove( 'past', 'present', 'future' );
|
|
||||||
|
|
||||||
if( h < indices.h ) {
|
|
||||||
backgroundh.classList.add( horizontalPast );
|
|
||||||
}
|
|
||||||
else if ( h > indices.h ) {
|
|
||||||
backgroundh.classList.add( horizontalFuture );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
backgroundh.classList.add( 'present' );
|
|
||||||
|
|
||||||
// Store a reference to the current background element
|
|
||||||
currentBackground = backgroundh;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( includeAll || h === indices.h ) {
|
|
||||||
queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {
|
|
||||||
|
|
||||||
backgroundv.classList.remove( 'past', 'present', 'future' );
|
|
||||||
|
|
||||||
const indexv = typeof indices.v === 'number' ? indices.v : 0;
|
|
||||||
|
|
||||||
if( v < indexv ) {
|
|
||||||
backgroundv.classList.add( 'past' );
|
|
||||||
}
|
|
||||||
else if ( v > indexv ) {
|
|
||||||
backgroundv.classList.add( 'future' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
backgroundv.classList.add( 'present' );
|
|
||||||
|
|
||||||
// Only if this is the present horizontal and vertical slide
|
|
||||||
if( h === indices.h ) currentBackground = backgroundv;
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Stop content inside of previous backgrounds
|
|
||||||
if( this.previousBackground ) {
|
|
||||||
|
|
||||||
this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start content in the current background
|
|
||||||
if( currentBackground ) {
|
|
||||||
|
|
||||||
this.Reveal.slideContent.startEmbeddedContent( currentBackground );
|
|
||||||
|
|
||||||
let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
|
|
||||||
if( currentBackgroundContent ) {
|
|
||||||
|
|
||||||
let backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
|
|
||||||
|
|
||||||
// Restart GIFs (doesn't work in Firefox)
|
|
||||||
if( /\.gif/i.test( backgroundImageURL ) ) {
|
|
||||||
currentBackgroundContent.style.backgroundImage = '';
|
|
||||||
window.getComputedStyle( currentBackgroundContent ).opacity;
|
|
||||||
currentBackgroundContent.style.backgroundImage = backgroundImageURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't transition between identical backgrounds. This
|
|
||||||
// prevents unwanted flicker.
|
|
||||||
let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null;
|
|
||||||
let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
|
|
||||||
if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
|
|
||||||
this.element.classList.add( 'no-transition' );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousBackground = currentBackground;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a background brightness flag for this slide,
|
|
||||||
// bubble it to the .reveal container
|
|
||||||
if( currentSlide ) {
|
|
||||||
this.bubbleSlideContrastClassToElement( currentSlide, this.Reveal.getRevealElement() );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the first background to apply without transition
|
|
||||||
setTimeout( () => {
|
|
||||||
this.element.classList.remove( 'no-transition' );
|
|
||||||
}, 1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the position of the parallax background based
|
|
||||||
* on the current slide index.
|
|
||||||
*/
|
|
||||||
updateParallax() {
|
|
||||||
|
|
||||||
let indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().parallaxBackgroundImage ) {
|
|
||||||
|
|
||||||
let horizontalSlides = this.Reveal.getHorizontalSlides(),
|
|
||||||
verticalSlides = this.Reveal.getVerticalSlides();
|
|
||||||
|
|
||||||
let backgroundSize = this.element.style.backgroundSize.split( ' ' ),
|
|
||||||
backgroundWidth, backgroundHeight;
|
|
||||||
|
|
||||||
if( backgroundSize.length === 1 ) {
|
|
||||||
backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
backgroundWidth = parseInt( backgroundSize[0], 10 );
|
|
||||||
backgroundHeight = parseInt( backgroundSize[1], 10 );
|
|
||||||
}
|
|
||||||
|
|
||||||
let slideWidth = this.element.offsetWidth,
|
|
||||||
horizontalSlideCount = horizontalSlides.length,
|
|
||||||
horizontalOffsetMultiplier,
|
|
||||||
horizontalOffset;
|
|
||||||
|
|
||||||
if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {
|
|
||||||
horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalOffset = horizontalOffsetMultiplier * indices.h * -1;
|
|
||||||
|
|
||||||
let slideHeight = this.element.offsetHeight,
|
|
||||||
verticalSlideCount = verticalSlides.length,
|
|
||||||
verticalOffsetMultiplier,
|
|
||||||
verticalOffset;
|
|
||||||
|
|
||||||
if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {
|
|
||||||
verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0;
|
|
||||||
|
|
||||||
this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
266
js/controllers/controls.js
vendored
@ -1,266 +0,0 @@
|
|||||||
import { queryAll } from '../utils/util.js'
|
|
||||||
import { isAndroid } from '../utils/device.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages our presentation controls. This includes both
|
|
||||||
* the built-in control arrows as well as event monitoring
|
|
||||||
* of any elements within the presentation with either of the
|
|
||||||
* following helper classes:
|
|
||||||
* - .navigate-up
|
|
||||||
* - .navigate-right
|
|
||||||
* - .navigate-down
|
|
||||||
* - .navigate-left
|
|
||||||
* - .navigate-next
|
|
||||||
* - .navigate-prev
|
|
||||||
*/
|
|
||||||
export default class Controls {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
|
|
||||||
this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
|
|
||||||
this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
|
|
||||||
this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
|
|
||||||
this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
|
|
||||||
this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const rtl = this.Reveal.getConfig().rtl;
|
|
||||||
const revealElement = this.Reveal.getRevealElement();
|
|
||||||
|
|
||||||
this.element = document.createElement( 'aside' );
|
|
||||||
this.element.className = 'controls';
|
|
||||||
this.element.innerHTML =
|
|
||||||
`<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
|
|
||||||
<button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
|
|
||||||
<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
|
|
||||||
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
|
|
||||||
// There can be multiple instances of controls throughout the page
|
|
||||||
this.controlsLeft = queryAll( revealElement, '.navigate-left' );
|
|
||||||
this.controlsRight = queryAll( revealElement, '.navigate-right' );
|
|
||||||
this.controlsUp = queryAll( revealElement, '.navigate-up' );
|
|
||||||
this.controlsDown = queryAll( revealElement, '.navigate-down' );
|
|
||||||
this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
|
|
||||||
this.controlsNext = queryAll( revealElement, '.navigate-next' );
|
|
||||||
|
|
||||||
// The left, right and down arrows in the standard reveal.js controls
|
|
||||||
this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
|
|
||||||
this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
|
|
||||||
this.controlsDownArrow = this.element.querySelector( '.navigate-down' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
this.element.style.display = config.controls ? 'block' : 'none';
|
|
||||||
|
|
||||||
this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
|
|
||||||
this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
// Listen to both touch and click events, in case the device
|
|
||||||
// supports both
|
|
||||||
let pointerEvents = [ 'touchstart', 'click' ];
|
|
||||||
|
|
||||||
// Only support touch for Android, fixes double navigations in
|
|
||||||
// stock browser
|
|
||||||
if( isAndroid ) {
|
|
||||||
pointerEvents = [ 'touchstart' ];
|
|
||||||
}
|
|
||||||
|
|
||||||
pointerEvents.forEach( eventName => {
|
|
||||||
this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
|
|
||||||
this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
|
|
||||||
this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
|
|
||||||
this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
|
|
||||||
this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
|
||||||
this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
[ 'touchstart', 'click' ].forEach( eventName => {
|
|
||||||
this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
|
|
||||||
this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
|
|
||||||
this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
|
|
||||||
this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
|
|
||||||
this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
|
||||||
this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the state of all control/navigation arrows.
|
|
||||||
*/
|
|
||||||
update() {
|
|
||||||
|
|
||||||
let routes = this.Reveal.availableRoutes();
|
|
||||||
|
|
||||||
// Remove the 'enabled' class from all directions
|
|
||||||
[...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
|
|
||||||
node.classList.remove( 'enabled', 'fragmented' );
|
|
||||||
|
|
||||||
// Set 'disabled' attribute on all directions
|
|
||||||
node.setAttribute( 'disabled', 'disabled' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
|
|
||||||
if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
|
|
||||||
// Prev/next buttons
|
|
||||||
if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
|
|
||||||
// Highlight fragment directions
|
|
||||||
let currentSlide = this.Reveal.getCurrentSlide();
|
|
||||||
if( currentSlide ) {
|
|
||||||
|
|
||||||
let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
|
|
||||||
|
|
||||||
// Always apply fragment decorator to prev/next buttons
|
|
||||||
if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
|
|
||||||
// Apply fragment decorators to directional buttons based on
|
|
||||||
// what slide axis they are in
|
|
||||||
if( this.Reveal.isVerticalSlide( currentSlide ) ) {
|
|
||||||
if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().controlsTutorial ) {
|
|
||||||
|
|
||||||
let indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
// Highlight control arrows with an animation to ensure
|
|
||||||
// that the viewer knows how to navigate
|
|
||||||
if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
|
|
||||||
this.controlsDownArrow.classList.add( 'highlight' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.controlsDownArrow.classList.remove( 'highlight' );
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().rtl ) {
|
|
||||||
|
|
||||||
if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
|
|
||||||
this.controlsLeftArrow.classList.add( 'highlight' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.controlsLeftArrow.classList.remove( 'highlight' );
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
|
|
||||||
this.controlsRightArrow.classList.add( 'highlight' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.controlsRightArrow.classList.remove( 'highlight' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.unbind();
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handlers for navigation control buttons.
|
|
||||||
*/
|
|
||||||
onNavigateLeftClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().navigationMode === 'linear' ) {
|
|
||||||
this.Reveal.prev();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.left();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavigateRightClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().navigationMode === 'linear' ) {
|
|
||||||
this.Reveal.next();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.right();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavigateUpClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
this.Reveal.up();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavigateDownClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
this.Reveal.down();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavigatePrevClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
this.Reveal.prev();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavigateNextClicked( event ) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.Reveal.onUserInput();
|
|
||||||
|
|
||||||
this.Reveal.next();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
import { closest } from '../utils/util.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages focus when a presentation is embedded. This
|
|
||||||
* helps us only capture keyboard from the presentation
|
|
||||||
* a user is currently interacting with in a page where
|
|
||||||
* multiple presentations are embedded.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const STATE_FOCUS = 'focus';
|
|
||||||
const STATE_BLUR = 'blur';
|
|
||||||
|
|
||||||
export default class Focus {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.onRevealPointerDown = this.onRevealPointerDown.bind( this );
|
|
||||||
this.onDocumentPointerDown = this.onDocumentPointerDown.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
if( config.embedded ) {
|
|
||||||
this.blur();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.focus();
|
|
||||||
this.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().embedded ) {
|
|
||||||
this.Reveal.getRevealElement().addEventListener( 'pointerdown', this.onRevealPointerDown, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().removeEventListener( 'pointerdown', this.onRevealPointerDown, false );
|
|
||||||
document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
|
|
||||||
if( this.state !== STATE_FOCUS ) {
|
|
||||||
this.Reveal.getRevealElement().classList.add( 'focused' );
|
|
||||||
document.addEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = STATE_FOCUS;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
blur() {
|
|
||||||
|
|
||||||
if( this.state !== STATE_BLUR ) {
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'focused' );
|
|
||||||
document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = STATE_BLUR;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
isFocused() {
|
|
||||||
|
|
||||||
return this.state === STATE_FOCUS;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'focused' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onRevealPointerDown( event ) {
|
|
||||||
|
|
||||||
this.focus();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onDocumentPointerDown( event ) {
|
|
||||||
|
|
||||||
let revealElement = closest( event.target, '.reveal' );
|
|
||||||
if( !revealElement || revealElement !== this.Reveal.getRevealElement() ) {
|
|
||||||
this.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,375 +0,0 @@
|
|||||||
import { extend, queryAll } from '../utils/util.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles sorting and navigation of slide fragments.
|
|
||||||
* Fragments are elements within a slide that are
|
|
||||||
* revealed/animated incrementally.
|
|
||||||
*/
|
|
||||||
export default class Fragments {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
if( config.fragments === false ) {
|
|
||||||
this.disable();
|
|
||||||
}
|
|
||||||
else if( oldConfig.fragments === false ) {
|
|
||||||
this.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If fragments are disabled in the deck, they should all be
|
|
||||||
* visible rather than stepped through.
|
|
||||||
*/
|
|
||||||
disable() {
|
|
||||||
|
|
||||||
queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
|
|
||||||
element.classList.add( 'visible' );
|
|
||||||
element.classList.remove( 'current-fragment' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse of #disable(). Only called if fragments have
|
|
||||||
* previously been disabled.
|
|
||||||
*/
|
|
||||||
enable() {
|
|
||||||
|
|
||||||
queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
|
|
||||||
element.classList.remove( 'visible' );
|
|
||||||
element.classList.remove( 'current-fragment' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object describing the available fragment
|
|
||||||
* directions.
|
|
||||||
*
|
|
||||||
* @return {{prev: boolean, next: boolean}}
|
|
||||||
*/
|
|
||||||
availableRoutes() {
|
|
||||||
|
|
||||||
let currentSlide = this.Reveal.getCurrentSlide();
|
|
||||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
|
||||||
let fragments = currentSlide.querySelectorAll( '.fragment:not(.disabled)' );
|
|
||||||
let hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.disabled):not(.visible)' );
|
|
||||||
|
|
||||||
return {
|
|
||||||
prev: fragments.length - hiddenFragments.length > 0,
|
|
||||||
next: !!hiddenFragments.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return { prev: false, next: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a sorted fragments list, ordered by an increasing
|
|
||||||
* "data-fragment-index" attribute.
|
|
||||||
*
|
|
||||||
* Fragments will be revealed in the order that they are returned by
|
|
||||||
* this function, so you can use the index attributes to control the
|
|
||||||
* order of fragment appearance.
|
|
||||||
*
|
|
||||||
* To maintain a sensible default fragment order, fragments are presumed
|
|
||||||
* to be passed in document order. This function adds a "fragment-index"
|
|
||||||
* attribute to each node if such an attribute is not already present,
|
|
||||||
* and sets that attribute to an integer value which is the position of
|
|
||||||
* the fragment within the fragments list.
|
|
||||||
*
|
|
||||||
* @param {object[]|*} fragments
|
|
||||||
* @param {boolean} grouped If true the returned array will contain
|
|
||||||
* nested arrays for all fragments with the same index
|
|
||||||
* @return {object[]} sorted Sorted array of fragments
|
|
||||||
*/
|
|
||||||
sort( fragments, grouped = false ) {
|
|
||||||
|
|
||||||
fragments = Array.from( fragments );
|
|
||||||
|
|
||||||
let ordered = [],
|
|
||||||
unordered = [],
|
|
||||||
sorted = [];
|
|
||||||
|
|
||||||
// Group ordered and unordered elements
|
|
||||||
fragments.forEach( fragment => {
|
|
||||||
if( fragment.hasAttribute( 'data-fragment-index' ) ) {
|
|
||||||
let index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
|
|
||||||
|
|
||||||
if( !ordered[index] ) {
|
|
||||||
ordered[index] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
ordered[index].push( fragment );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
unordered.push( [ fragment ] );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Append fragments without explicit indices in their
|
|
||||||
// DOM order
|
|
||||||
ordered = ordered.concat( unordered );
|
|
||||||
|
|
||||||
// Manually count the index up per group to ensure there
|
|
||||||
// are no gaps
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
// Push all fragments in their sorted order to an array,
|
|
||||||
// this flattens the groups
|
|
||||||
ordered.forEach( group => {
|
|
||||||
group.forEach( fragment => {
|
|
||||||
sorted.push( fragment );
|
|
||||||
fragment.setAttribute( 'data-fragment-index', index );
|
|
||||||
} );
|
|
||||||
|
|
||||||
index ++;
|
|
||||||
} );
|
|
||||||
|
|
||||||
return grouped === true ? ordered : sorted;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts and formats all of fragments in the
|
|
||||||
* presentation.
|
|
||||||
*/
|
|
||||||
sortAll() {
|
|
||||||
|
|
||||||
this.Reveal.getHorizontalSlides().forEach( horizontalSlide => {
|
|
||||||
|
|
||||||
let verticalSlides = queryAll( horizontalSlide, 'section' );
|
|
||||||
verticalSlides.forEach( ( verticalSlide, y ) => {
|
|
||||||
|
|
||||||
this.sort( verticalSlide.querySelectorAll( '.fragment' ) );
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
if( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the fragments on the current slide so that they
|
|
||||||
* have the appropriate classes (.visible + .current-fragment).
|
|
||||||
*
|
|
||||||
* @param {number} [index] The index of the current fragment
|
|
||||||
* @param {array} [fragments] Array containing all fragments
|
|
||||||
* in the current slide
|
|
||||||
*
|
|
||||||
* @return {{shown: array, hidden: array}}
|
|
||||||
*/
|
|
||||||
update( index, fragments, slide = this.Reveal.getCurrentSlide() ) {
|
|
||||||
|
|
||||||
let changedFragments = {
|
|
||||||
shown: [],
|
|
||||||
hidden: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if( slide && this.Reveal.getConfig().fragments ) {
|
|
||||||
|
|
||||||
fragments = fragments || this.sort( slide.querySelectorAll( '.fragment' ) );
|
|
||||||
|
|
||||||
if( fragments.length ) {
|
|
||||||
|
|
||||||
let maxIndex = 0;
|
|
||||||
|
|
||||||
if( typeof index !== 'number' ) {
|
|
||||||
let currentFragment = this.sort( slide.querySelectorAll( '.fragment.visible' ) ).pop();
|
|
||||||
if( currentFragment ) {
|
|
||||||
index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.from( fragments ).forEach( ( el, i ) => {
|
|
||||||
|
|
||||||
if( el.hasAttribute( 'data-fragment-index' ) ) {
|
|
||||||
i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
|
|
||||||
}
|
|
||||||
|
|
||||||
maxIndex = Math.max( maxIndex, i );
|
|
||||||
|
|
||||||
// Visible fragments
|
|
||||||
if( i <= index ) {
|
|
||||||
let wasVisible = el.classList.contains( 'visible' )
|
|
||||||
el.classList.add( 'visible' );
|
|
||||||
el.classList.remove( 'current-fragment' );
|
|
||||||
|
|
||||||
if( i === index ) {
|
|
||||||
// Announce the fragments one by one to the Screen Reader
|
|
||||||
this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
|
|
||||||
|
|
||||||
el.classList.add( 'current-fragment' );
|
|
||||||
this.Reveal.slideContent.startEmbeddedContent( el );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !wasVisible ) {
|
|
||||||
changedFragments.shown.push( el )
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
target: el,
|
|
||||||
type: 'visible',
|
|
||||||
bubbles: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Hidden fragments
|
|
||||||
else {
|
|
||||||
let wasVisible = el.classList.contains( 'visible' )
|
|
||||||
el.classList.remove( 'visible' );
|
|
||||||
el.classList.remove( 'current-fragment' );
|
|
||||||
|
|
||||||
if( wasVisible ) {
|
|
||||||
this.Reveal.slideContent.stopEmbeddedContent( el );
|
|
||||||
changedFragments.hidden.push( el );
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
target: el,
|
|
||||||
type: 'hidden',
|
|
||||||
bubbles: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Write the current fragment index to the slide <section>.
|
|
||||||
// This can be used by end users to apply styles based on
|
|
||||||
// the current fragment index.
|
|
||||||
index = typeof index === 'number' ? index : -1;
|
|
||||||
index = Math.max( Math.min( index, maxIndex ), -1 );
|
|
||||||
slide.setAttribute( 'data-fragment', index );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return changedFragments;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the fragments on the given slide so that they have
|
|
||||||
* valid indices. Call this if fragments are changed in the DOM
|
|
||||||
* after reveal.js has already initialized.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide
|
|
||||||
* @return {Array} a list of the HTML fragments that were synced
|
|
||||||
*/
|
|
||||||
sync( slide = this.Reveal.getCurrentSlide() ) {
|
|
||||||
|
|
||||||
return this.sort( slide.querySelectorAll( '.fragment' ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the specified slide fragment.
|
|
||||||
*
|
|
||||||
* @param {?number} index The index of the fragment that
|
|
||||||
* should be shown, -1 means all are invisible
|
|
||||||
* @param {number} offset Integer offset to apply to the
|
|
||||||
* fragment index
|
|
||||||
*
|
|
||||||
* @return {boolean} true if a change was made in any
|
|
||||||
* fragments visibility as part of this call
|
|
||||||
*/
|
|
||||||
goto( index, offset = 0 ) {
|
|
||||||
|
|
||||||
let currentSlide = this.Reveal.getCurrentSlide();
|
|
||||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
|
||||||
|
|
||||||
let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
|
|
||||||
if( fragments.length ) {
|
|
||||||
|
|
||||||
// If no index is specified, find the current
|
|
||||||
if( typeof index !== 'number' ) {
|
|
||||||
let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
|
|
||||||
|
|
||||||
if( lastVisibleFragment ) {
|
|
||||||
index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
index = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the offset if there is one
|
|
||||||
index += offset;
|
|
||||||
|
|
||||||
let changedFragments = this.update( index, fragments );
|
|
||||||
|
|
||||||
if( changedFragments.hidden.length ) {
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
type: 'fragmenthidden',
|
|
||||||
data: {
|
|
||||||
fragment: changedFragments.hidden[0],
|
|
||||||
fragments: changedFragments.hidden
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if( changedFragments.shown.length ) {
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
type: 'fragmentshown',
|
|
||||||
data: {
|
|
||||||
fragment: changedFragments.shown[0],
|
|
||||||
fragments: changedFragments.shown
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Reveal.controls.update();
|
|
||||||
this.Reveal.progress.update();
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().fragmentInURL ) {
|
|
||||||
this.Reveal.location.writeURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!( changedFragments.shown.length || changedFragments.hidden.length );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the next slide fragment.
|
|
||||||
*
|
|
||||||
* @return {boolean} true if there was a next fragment,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
next() {
|
|
||||||
|
|
||||||
return this.goto( null, 1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the previous slide fragment.
|
|
||||||
*
|
|
||||||
* @return {boolean} true if there was a previous fragment,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
prev() {
|
|
||||||
|
|
||||||
return this.goto( null, -1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,197 +0,0 @@
|
|||||||
import {
|
|
||||||
SLIDE_NUMBER_FORMAT_CURRENT,
|
|
||||||
SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL
|
|
||||||
} from "../utils/constants";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes it possible to jump to a slide by entering its
|
|
||||||
* slide number or id.
|
|
||||||
*/
|
|
||||||
export default class JumpToSlide {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.onInput = this.onInput.bind( this );
|
|
||||||
this.onBlur = this.onBlur.bind( this );
|
|
||||||
this.onKeyDown = this.onKeyDown.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
|
||||||
this.element.className = 'jump-to-slide';
|
|
||||||
|
|
||||||
this.jumpInput = document.createElement( 'input' );
|
|
||||||
this.jumpInput.type = 'text';
|
|
||||||
this.jumpInput.className = 'jump-to-slide-input';
|
|
||||||
this.jumpInput.placeholder = 'Jump to slide';
|
|
||||||
this.jumpInput.addEventListener( 'input', this.onInput );
|
|
||||||
this.jumpInput.addEventListener( 'keydown', this.onKeyDown );
|
|
||||||
this.jumpInput.addEventListener( 'blur', this.onBlur );
|
|
||||||
|
|
||||||
this.element.appendChild( this.jumpInput );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
|
|
||||||
this.indicesOnShow = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
this.jumpInput.focus();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
|
|
||||||
if( this.isVisible() ) {
|
|
||||||
this.element.remove();
|
|
||||||
this.jumpInput.value = '';
|
|
||||||
|
|
||||||
clearTimeout( this.jumpTimeout );
|
|
||||||
delete this.jumpTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisible() {
|
|
||||||
|
|
||||||
return !!this.element.parentNode;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the current input and jumps to the given slide.
|
|
||||||
*/
|
|
||||||
jump() {
|
|
||||||
|
|
||||||
clearTimeout( this.jumpTimeout );
|
|
||||||
delete this.jumpTimeout;
|
|
||||||
|
|
||||||
let query = this.jumpInput.value.trim( '' );
|
|
||||||
let indices;
|
|
||||||
|
|
||||||
// When slide numbers are formatted to be a single linear mumber
|
|
||||||
// (instead of showing a separate horizontal/vertical index) we
|
|
||||||
// use the same format for slide jumps
|
|
||||||
if( /^\d+$/.test( query ) ) {
|
|
||||||
const slideNumberFormat = this.Reveal.getConfig().slideNumber;
|
|
||||||
if( slideNumberFormat === SLIDE_NUMBER_FORMAT_CURRENT || slideNumberFormat === SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL ) {
|
|
||||||
const slide = this.Reveal.getSlides()[ parseInt( query, 10 ) - 1 ];
|
|
||||||
if( slide ) {
|
|
||||||
indices = this.Reveal.getIndices( slide );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !indices ) {
|
|
||||||
// If the query uses "horizontal.vertical" format, convert to
|
|
||||||
// "horizontal/vertical" so that our URL parser can understand
|
|
||||||
if( /^\d+\.\d+$/.test( query ) ) {
|
|
||||||
query = query.replace( '.', '/' );
|
|
||||||
}
|
|
||||||
|
|
||||||
indices = this.Reveal.location.getIndicesFromHash( query, { oneBasedIndex: true } );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Still no valid index? Fall back on a text search
|
|
||||||
if( !indices && /\S+/i.test( query ) && query.length > 1 ) {
|
|
||||||
indices = this.search( query );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( indices && query !== '' ) {
|
|
||||||
this.Reveal.slide( indices.h, indices.v, indices.f );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpAfter( delay ) {
|
|
||||||
|
|
||||||
clearTimeout( this.jumpTimeout );
|
|
||||||
this.jumpTimeout = setTimeout( () => this.jump(), delay );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A lofi search that looks for the given query in all
|
|
||||||
* of our slides and returns the first match.
|
|
||||||
*/
|
|
||||||
search( query ) {
|
|
||||||
|
|
||||||
const regex = new RegExp( '\\b' + query.trim() + '\\b', 'i' );
|
|
||||||
|
|
||||||
const slide = this.Reveal.getSlides().find( ( slide ) => {
|
|
||||||
return regex.test( slide.innerText );
|
|
||||||
} );
|
|
||||||
|
|
||||||
if( slide ) {
|
|
||||||
return this.Reveal.getIndices( slide );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverts back to the slide we were on when jump to slide was
|
|
||||||
* invoked.
|
|
||||||
*/
|
|
||||||
cancel() {
|
|
||||||
|
|
||||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
|
||||||
this.hide();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
|
|
||||||
this.jump();
|
|
||||||
this.hide();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.jumpInput.removeEventListener( 'input', this.onInput );
|
|
||||||
this.jumpInput.removeEventListener( 'keydown', this.onKeyDown );
|
|
||||||
this.jumpInput.removeEventListener( 'blur', this.onBlur );
|
|
||||||
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown( event ) {
|
|
||||||
|
|
||||||
if( event.keyCode === 13 ) {
|
|
||||||
this.confirm();
|
|
||||||
}
|
|
||||||
else if( event.keyCode === 27 ) {
|
|
||||||
this.cancel();
|
|
||||||
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onInput( event ) {
|
|
||||||
|
|
||||||
this.jumpAfter( 200 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
|
|
||||||
setTimeout( () => this.hide(), 1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,386 +0,0 @@
|
|||||||
import { enterFullscreen } from '../utils/util.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles all reveal.js keyboard interactions.
|
|
||||||
*/
|
|
||||||
export default class Keyboard {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
// A key:value map of keyboard keys and descriptions of
|
|
||||||
// the actions they trigger
|
|
||||||
this.shortcuts = {};
|
|
||||||
|
|
||||||
// Holds custom key code mappings
|
|
||||||
this.bindings = {};
|
|
||||||
|
|
||||||
this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
if( config.navigationMode === 'linear' ) {
|
|
||||||
this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
|
|
||||||
this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.shortcuts['N , SPACE'] = 'Next slide';
|
|
||||||
this.shortcuts['P , Shift SPACE'] = 'Previous slide';
|
|
||||||
this.shortcuts['← , H'] = 'Navigate left';
|
|
||||||
this.shortcuts['→ , L'] = 'Navigate right';
|
|
||||||
this.shortcuts['↑ , K'] = 'Navigate up';
|
|
||||||
this.shortcuts['↓ , J'] = 'Navigate down';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shortcuts['Alt + ←/↑/→/↓'] = 'Navigate without fragments';
|
|
||||||
this.shortcuts['Shift + ←/↑/→/↓'] = 'Jump to first/last slide';
|
|
||||||
this.shortcuts['B , .'] = 'Pause';
|
|
||||||
this.shortcuts['F'] = 'Fullscreen';
|
|
||||||
this.shortcuts['G'] = 'Jump to slide';
|
|
||||||
this.shortcuts['ESC, O'] = 'Slide overview';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts listening for keyboard events.
|
|
||||||
*/
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops listening for keyboard events.
|
|
||||||
*/
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a custom key binding with optional description to
|
|
||||||
* be added to the help screen.
|
|
||||||
*/
|
|
||||||
addKeyBinding( binding, callback ) {
|
|
||||||
|
|
||||||
if( typeof binding === 'object' && binding.keyCode ) {
|
|
||||||
this.bindings[binding.keyCode] = {
|
|
||||||
callback: callback,
|
|
||||||
key: binding.key,
|
|
||||||
description: binding.description
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.bindings[binding] = {
|
|
||||||
callback: callback,
|
|
||||||
key: null,
|
|
||||||
description: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the specified custom key binding.
|
|
||||||
*/
|
|
||||||
removeKeyBinding( keyCode ) {
|
|
||||||
|
|
||||||
delete this.bindings[keyCode];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Programmatically triggers a keyboard event
|
|
||||||
*
|
|
||||||
* @param {int} keyCode
|
|
||||||
*/
|
|
||||||
triggerKey( keyCode ) {
|
|
||||||
|
|
||||||
this.onDocumentKeyDown( { keyCode } );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a new shortcut to include in the help overlay
|
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @param {String} value
|
|
||||||
*/
|
|
||||||
registerKeyboardShortcut( key, value ) {
|
|
||||||
|
|
||||||
this.shortcuts[key] = value;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getShortcuts() {
|
|
||||||
|
|
||||||
return this.shortcuts;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getBindings() {
|
|
||||||
|
|
||||||
return this.bindings;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the document level 'keydown' event.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onDocumentKeyDown( event ) {
|
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
|
|
||||||
// If there's a condition specified and it returns false,
|
|
||||||
// ignore this event
|
|
||||||
if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If keyboardCondition is set, only capture keyboard events
|
|
||||||
// for embedded decks when they are focused
|
|
||||||
if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shorthand
|
|
||||||
let keyCode = event.keyCode;
|
|
||||||
|
|
||||||
// Remember if auto-sliding was paused so we can toggle it
|
|
||||||
let autoSlideWasPaused = !this.Reveal.isAutoSliding();
|
|
||||||
|
|
||||||
this.Reveal.onUserInput( event );
|
|
||||||
|
|
||||||
// Is there a focused element that could be using the keyboard?
|
|
||||||
let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
|
|
||||||
let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
|
|
||||||
let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
|
|
||||||
|
|
||||||
// Whitelist certain modifiers for slide navigation shortcuts
|
|
||||||
let keyCodeUsesModifier = [32, 37, 38, 39, 40, 78, 80, 191].indexOf( event.keyCode ) !== -1;
|
|
||||||
|
|
||||||
// Prevent all other events when a modifier is pressed
|
|
||||||
let unusedModifier = !( keyCodeUsesModifier && event.shiftKey || event.altKey ) &&
|
|
||||||
( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
|
|
||||||
|
|
||||||
// Disregard the event if there's a focused element or a
|
|
||||||
// keyboard modifier key is present
|
|
||||||
if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
|
|
||||||
|
|
||||||
// While paused only allow resume keyboard events; 'b', 'v', '.'
|
|
||||||
let resumeKeyCodes = [66,86,190,191];
|
|
||||||
let key;
|
|
||||||
|
|
||||||
// Custom key bindings for togglePause should be able to resume
|
|
||||||
if( typeof config.keyboard === 'object' ) {
|
|
||||||
for( key in config.keyboard ) {
|
|
||||||
if( config.keyboard[key] === 'togglePause' ) {
|
|
||||||
resumeKeyCodes.push( parseInt( key, 10 ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use linear navigation if we're configured to OR if
|
|
||||||
// the presentation is one-dimensional
|
|
||||||
let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
|
|
||||||
|
|
||||||
let triggered = false;
|
|
||||||
|
|
||||||
// 1. User defined key bindings
|
|
||||||
if( typeof config.keyboard === 'object' ) {
|
|
||||||
|
|
||||||
for( key in config.keyboard ) {
|
|
||||||
|
|
||||||
// Check if this binding matches the pressed key
|
|
||||||
if( parseInt( key, 10 ) === keyCode ) {
|
|
||||||
|
|
||||||
let value = config.keyboard[ key ];
|
|
||||||
|
|
||||||
// Callback function
|
|
||||||
if( typeof value === 'function' ) {
|
|
||||||
value.apply( null, [ event ] );
|
|
||||||
}
|
|
||||||
// String shortcuts to reveal.js API
|
|
||||||
else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
|
|
||||||
this.Reveal[ value ].call();
|
|
||||||
}
|
|
||||||
|
|
||||||
triggered = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Registered custom key bindings
|
|
||||||
if( triggered === false ) {
|
|
||||||
|
|
||||||
for( key in this.bindings ) {
|
|
||||||
|
|
||||||
// Check if this binding matches the pressed key
|
|
||||||
if( parseInt( key, 10 ) === keyCode ) {
|
|
||||||
|
|
||||||
let action = this.bindings[ key ].callback;
|
|
||||||
|
|
||||||
// Callback function
|
|
||||||
if( typeof action === 'function' ) {
|
|
||||||
action.apply( null, [ event ] );
|
|
||||||
}
|
|
||||||
// String shortcuts to reveal.js API
|
|
||||||
else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
|
|
||||||
this.Reveal[ action ].call();
|
|
||||||
}
|
|
||||||
|
|
||||||
triggered = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. System defined key bindings
|
|
||||||
if( triggered === false ) {
|
|
||||||
|
|
||||||
// Assume true and try to prove false
|
|
||||||
triggered = true;
|
|
||||||
|
|
||||||
// P, PAGE UP
|
|
||||||
if( keyCode === 80 || keyCode === 33 ) {
|
|
||||||
this.Reveal.prev({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
// N, PAGE DOWN
|
|
||||||
else if( keyCode === 78 || keyCode === 34 ) {
|
|
||||||
this.Reveal.next({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
// H, LEFT
|
|
||||||
else if( keyCode === 72 || keyCode === 37 ) {
|
|
||||||
if( event.shiftKey ) {
|
|
||||||
this.Reveal.slide( 0 );
|
|
||||||
}
|
|
||||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
|
||||||
this.Reveal.prev({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.left({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// L, RIGHT
|
|
||||||
else if( keyCode === 76 || keyCode === 39 ) {
|
|
||||||
if( event.shiftKey ) {
|
|
||||||
this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
|
|
||||||
}
|
|
||||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
|
||||||
this.Reveal.next({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.right({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// K, UP
|
|
||||||
else if( keyCode === 75 || keyCode === 38 ) {
|
|
||||||
if( event.shiftKey ) {
|
|
||||||
this.Reveal.slide( undefined, 0 );
|
|
||||||
}
|
|
||||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
|
||||||
this.Reveal.prev({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.up({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// J, DOWN
|
|
||||||
else if( keyCode === 74 || keyCode === 40 ) {
|
|
||||||
if( event.shiftKey ) {
|
|
||||||
this.Reveal.slide( undefined, Number.MAX_VALUE );
|
|
||||||
}
|
|
||||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
|
||||||
this.Reveal.next({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.down({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// HOME
|
|
||||||
else if( keyCode === 36 ) {
|
|
||||||
this.Reveal.slide( 0 );
|
|
||||||
}
|
|
||||||
// END
|
|
||||||
else if( keyCode === 35 ) {
|
|
||||||
this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
|
|
||||||
}
|
|
||||||
// SPACE
|
|
||||||
else if( keyCode === 32 ) {
|
|
||||||
if( this.Reveal.overview.isActive() ) {
|
|
||||||
this.Reveal.overview.deactivate();
|
|
||||||
}
|
|
||||||
if( event.shiftKey ) {
|
|
||||||
this.Reveal.prev({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.next({skipFragments: event.altKey});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
|
|
||||||
else if( [58, 59, 66, 86, 190].includes( keyCode ) || ( keyCode === 191 && !event.shiftKey ) ) {
|
|
||||||
this.Reveal.togglePause();
|
|
||||||
}
|
|
||||||
// F
|
|
||||||
else if( keyCode === 70 ) {
|
|
||||||
enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
|
|
||||||
}
|
|
||||||
// A
|
|
||||||
else if( keyCode === 65 ) {
|
|
||||||
if( config.autoSlideStoppable ) {
|
|
||||||
this.Reveal.toggleAutoSlide( autoSlideWasPaused );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// G
|
|
||||||
else if( keyCode === 71 ) {
|
|
||||||
if( config.jumpToSlide ) {
|
|
||||||
this.Reveal.toggleJumpToSlide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ?
|
|
||||||
else if( keyCode === 191 && event.shiftKey ) {
|
|
||||||
this.Reveal.toggleHelp();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
triggered = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the input resulted in a triggered action we should prevent
|
|
||||||
// the browsers default behavior
|
|
||||||
if( triggered ) {
|
|
||||||
event.preventDefault && event.preventDefault();
|
|
||||||
}
|
|
||||||
// ESC or O key
|
|
||||||
else if( keyCode === 27 || keyCode === 79 ) {
|
|
||||||
if( this.Reveal.closeOverlay() === false ) {
|
|
||||||
this.Reveal.overview.toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault && event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If auto-sliding is enabled we need to cue up
|
|
||||||
// another timeout
|
|
||||||
this.Reveal.cueAutoSlide();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,247 +0,0 @@
|
|||||||
/**
|
|
||||||
* Reads and writes the URL based on reveal.js' current state.
|
|
||||||
*/
|
|
||||||
export default class Location {
|
|
||||||
|
|
||||||
// The minimum number of milliseconds that must pass between
|
|
||||||
// calls to history.replaceState
|
|
||||||
MAX_REPLACE_STATE_FREQUENCY = 1000
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
// Delays updates to the URL due to a Chrome thumbnailer bug
|
|
||||||
this.writeURLTimeout = 0;
|
|
||||||
|
|
||||||
this.replaceStateTimestamp = 0;
|
|
||||||
|
|
||||||
this.onWindowHashChange = this.onWindowHashChange.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
window.addEventListener( 'hashchange', this.onWindowHashChange, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
window.removeEventListener( 'hashchange', this.onWindowHashChange, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the slide indices for the given hash link.
|
|
||||||
*
|
|
||||||
* @param {string} [hash] the hash string that we want to
|
|
||||||
* find the indices for
|
|
||||||
*
|
|
||||||
* @returns slide indices or null
|
|
||||||
*/
|
|
||||||
getIndicesFromHash( hash=window.location.hash, options={} ) {
|
|
||||||
|
|
||||||
// Attempt to parse the hash as either an index or name
|
|
||||||
let name = hash.replace( /^#\/?/, '' );
|
|
||||||
let bits = name.split( '/' );
|
|
||||||
|
|
||||||
// If the first bit is not fully numeric and there is a name we
|
|
||||||
// can assume that this is a named link
|
|
||||||
if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
|
|
||||||
let slide;
|
|
||||||
|
|
||||||
let f;
|
|
||||||
|
|
||||||
// Parse named links with fragments (#/named-link/2)
|
|
||||||
if( /\/[-\d]+$/g.test( name ) ) {
|
|
||||||
f = parseInt( name.split( '/' ).pop(), 10 );
|
|
||||||
f = isNaN(f) ? undefined : f;
|
|
||||||
name = name.split( '/' ).shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the named link is a valid HTML ID attribute
|
|
||||||
try {
|
|
||||||
slide = document
|
|
||||||
.getElementById( decodeURIComponent( name ) )
|
|
||||||
.closest('.slides section');
|
|
||||||
}
|
|
||||||
catch ( error ) { }
|
|
||||||
|
|
||||||
if( slide ) {
|
|
||||||
return { ...this.Reveal.getIndices( slide ), f };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const config = this.Reveal.getConfig();
|
|
||||||
let hashIndexBase = config.hashOneBasedIndex || options.oneBasedIndex ? 1 : 0;
|
|
||||||
|
|
||||||
// Read the index components of the hash
|
|
||||||
let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
|
|
||||||
v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
|
|
||||||
f;
|
|
||||||
|
|
||||||
if( config.fragmentInURL ) {
|
|
||||||
f = parseInt( bits[2], 10 );
|
|
||||||
if( isNaN( f ) ) {
|
|
||||||
f = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { h, v, f };
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hash couldn't be parsed or no matching named link was found
|
|
||||||
return null
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the current URL (hash) and navigates accordingly.
|
|
||||||
*/
|
|
||||||
readURL() {
|
|
||||||
|
|
||||||
const currentIndices = this.Reveal.getIndices();
|
|
||||||
const newIndices = this.getIndicesFromHash();
|
|
||||||
|
|
||||||
if( newIndices ) {
|
|
||||||
if( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {
|
|
||||||
this.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If no new indices are available, we're trying to navigate to
|
|
||||||
// a slide hash that does not exist
|
|
||||||
else {
|
|
||||||
this.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the page URL (hash) to reflect the current
|
|
||||||
* state.
|
|
||||||
*
|
|
||||||
* @param {number} delay The time in ms to wait before
|
|
||||||
* writing the hash
|
|
||||||
*/
|
|
||||||
writeURL( delay ) {
|
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
let currentSlide = this.Reveal.getCurrentSlide();
|
|
||||||
|
|
||||||
// Make sure there's never more than one timeout running
|
|
||||||
clearTimeout( this.writeURLTimeout );
|
|
||||||
|
|
||||||
// If a delay is specified, timeout this call
|
|
||||||
if( typeof delay === 'number' ) {
|
|
||||||
this.writeURLTimeout = setTimeout( this.writeURL, delay );
|
|
||||||
}
|
|
||||||
else if( currentSlide ) {
|
|
||||||
|
|
||||||
let hash = this.getHash();
|
|
||||||
|
|
||||||
// If we're configured to push to history OR the history
|
|
||||||
// API is not available.
|
|
||||||
if( config.history ) {
|
|
||||||
window.location.hash = hash;
|
|
||||||
}
|
|
||||||
// If we're configured to reflect the current slide in the
|
|
||||||
// URL without pushing to history.
|
|
||||||
else if( config.hash ) {
|
|
||||||
// If the hash is empty, don't add it to the URL
|
|
||||||
if( hash === '/' ) {
|
|
||||||
this.debouncedReplaceState( window.location.pathname + window.location.search );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.debouncedReplaceState( '#' + hash );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// UPDATE: The below nuking of all hash changes breaks
|
|
||||||
// anchors on pages where reveal.js is running. Removed
|
|
||||||
// in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
|
|
||||||
//
|
|
||||||
// If history and hash are both disabled, a hash may still
|
|
||||||
// be added to the URL by clicking on a href with a hash
|
|
||||||
// target. Counter this by always removing the hash.
|
|
||||||
// else {
|
|
||||||
// window.history.replaceState( null, null, window.location.pathname + window.location.search );
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceState( url ) {
|
|
||||||
|
|
||||||
window.history.replaceState( null, null, url );
|
|
||||||
this.replaceStateTimestamp = Date.now();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
debouncedReplaceState( url ) {
|
|
||||||
|
|
||||||
clearTimeout( this.replaceStateTimeout );
|
|
||||||
|
|
||||||
if( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {
|
|
||||||
this.replaceState( url );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a hash URL that will resolve to the given slide location.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} [slide=currentSlide] The slide to link to
|
|
||||||
*/
|
|
||||||
getHash( slide ) {
|
|
||||||
|
|
||||||
let url = '/';
|
|
||||||
|
|
||||||
// Attempt to create a named link based on the slide's ID
|
|
||||||
let s = slide || this.Reveal.getCurrentSlide();
|
|
||||||
let id = s ? s.getAttribute( 'id' ) : null;
|
|
||||||
if( id ) {
|
|
||||||
id = encodeURIComponent( id );
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = this.Reveal.getIndices( slide );
|
|
||||||
if( !this.Reveal.getConfig().fragmentInURL ) {
|
|
||||||
index.f = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current slide has an ID, use that as a named link,
|
|
||||||
// but we don't support named links with a fragment index
|
|
||||||
if( typeof id === 'string' && id.length ) {
|
|
||||||
url = '/' + id;
|
|
||||||
|
|
||||||
// If there is also a fragment, append that at the end
|
|
||||||
// of the named link, like: #/named-link/2
|
|
||||||
if( index.f >= 0 ) url += '/' + index.f;
|
|
||||||
}
|
|
||||||
// Otherwise use the /h/v index
|
|
||||||
else {
|
|
||||||
let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
|
|
||||||
if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
|
|
||||||
if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
|
|
||||||
if( index.f >= 0 ) url += '/' + index.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the window level 'hashchange' event.
|
|
||||||
*
|
|
||||||
* @param {object} [event]
|
|
||||||
*/
|
|
||||||
onWindowHashChange( event ) {
|
|
||||||
|
|
||||||
this.readURL();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* Handles the showing of speaker notes
|
|
||||||
*/
|
|
||||||
export default class Notes {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
|
||||||
this.element.className = 'speaker-notes';
|
|
||||||
this.element.setAttribute( 'data-prevent-swipe', '' );
|
|
||||||
this.element.setAttribute( 'tabindex', '0' );
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
if( config.showNotes ) {
|
|
||||||
this.element.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick up notes from the current slide and display them
|
|
||||||
* to the viewer.
|
|
||||||
*
|
|
||||||
* @see {@link config.showNotes}
|
|
||||||
*/
|
|
||||||
update() {
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().showNotes &&
|
|
||||||
this.element && this.Reveal.getCurrentSlide() &&
|
|
||||||
!this.Reveal.isScrollView() &&
|
|
||||||
!this.Reveal.isPrintView()
|
|
||||||
) {
|
|
||||||
this.element.innerHTML = this.getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visibility of the speaker notes sidebar that
|
|
||||||
* is used to share annotated slides. The notes sidebar is
|
|
||||||
* only visible if showNotes is true and there are notes on
|
|
||||||
* one or more slides in the deck.
|
|
||||||
*/
|
|
||||||
updateVisibility() {
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().showNotes &&
|
|
||||||
this.hasNotes() &&
|
|
||||||
!this.Reveal.isScrollView() &&
|
|
||||||
!this.Reveal.isPrintView()
|
|
||||||
) {
|
|
||||||
this.Reveal.getRevealElement().classList.add( 'show-notes' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'show-notes' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there are speaker notes for ANY slide in the
|
|
||||||
* presentation.
|
|
||||||
*/
|
|
||||||
hasNotes() {
|
|
||||||
|
|
||||||
return this.Reveal.getSlidesElement().querySelectorAll( '[data-notes], aside.notes' ).length > 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this presentation is running inside of the
|
|
||||||
* speaker notes window.
|
|
||||||
*
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
isSpeakerNotesWindow() {
|
|
||||||
|
|
||||||
return !!window.location.search.match( /receiver/gi );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the speaker notes from a slide. Notes can be
|
|
||||||
* defined in two ways:
|
|
||||||
* 1. As a data-notes attribute on the slide <section>
|
|
||||||
* 2. With <aside class="notes"> elements inside the slide
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} [slide=currentSlide]
|
|
||||||
* @return {(string|null)}
|
|
||||||
*/
|
|
||||||
getSlideNotes( slide = this.Reveal.getCurrentSlide() ) {
|
|
||||||
|
|
||||||
// Notes can be specified via the data-notes attribute...
|
|
||||||
if( slide.hasAttribute( 'data-notes' ) ) {
|
|
||||||
return slide.getAttribute( 'data-notes' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... or using <aside class="notes"> elements
|
|
||||||
let notesElements = slide.querySelectorAll( 'aside.notes' );
|
|
||||||
if( notesElements ) {
|
|
||||||
return Array.from(notesElements).map( notesElement => notesElement.innerHTML ).join( '\n' );
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,255 +0,0 @@
|
|||||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
|
|
||||||
import { extend, queryAll, transformElement } from '../utils/util.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles all logic related to the overview mode
|
|
||||||
* (birds-eye view of all slides).
|
|
||||||
*/
|
|
||||||
export default class Overview {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
|
|
||||||
this.onSlideClicked = this.onSlideClicked.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the overview of slides (quick nav) by scaling
|
|
||||||
* down and arranging all slide elements.
|
|
||||||
*/
|
|
||||||
activate() {
|
|
||||||
|
|
||||||
// Only proceed if enabled in config
|
|
||||||
if( this.Reveal.getConfig().overview && !this.Reveal.isScrollView() && !this.isActive() ) {
|
|
||||||
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().classList.add( 'overview' );
|
|
||||||
|
|
||||||
// Don't auto-slide while in overview mode
|
|
||||||
this.Reveal.cancelAutoSlide();
|
|
||||||
|
|
||||||
// Move the backgrounds element into the slide container to
|
|
||||||
// that the same scaling is applied
|
|
||||||
this.Reveal.getSlidesElement().appendChild( this.Reveal.getBackgroundsElement() );
|
|
||||||
|
|
||||||
// Clicking on an overview slide navigates to it
|
|
||||||
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
|
|
||||||
if( !slide.classList.contains( 'stack' ) ) {
|
|
||||||
slide.addEventListener( 'click', this.onSlideClicked, true );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Calculate slide sizes
|
|
||||||
const margin = 70;
|
|
||||||
const slideSize = this.Reveal.getComputedSlideSize();
|
|
||||||
this.overviewSlideWidth = slideSize.width + margin;
|
|
||||||
this.overviewSlideHeight = slideSize.height + margin;
|
|
||||||
|
|
||||||
// Reverse in RTL mode
|
|
||||||
if( this.Reveal.getConfig().rtl ) {
|
|
||||||
this.overviewSlideWidth = -this.overviewSlideWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Reveal.updateSlidesVisibility();
|
|
||||||
|
|
||||||
this.layout();
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
this.Reveal.layout();
|
|
||||||
|
|
||||||
const indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
// Notify observers of the overview showing
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
type: 'overviewshown',
|
|
||||||
data: {
|
|
||||||
'indexh': indices.h,
|
|
||||||
'indexv': indices.v,
|
|
||||||
'currentSlide': this.Reveal.getCurrentSlide()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses CSS transforms to position all slides in a grid for
|
|
||||||
* display inside of the overview mode.
|
|
||||||
*/
|
|
||||||
layout() {
|
|
||||||
|
|
||||||
// Layout slides
|
|
||||||
this.Reveal.getHorizontalSlides().forEach( ( hslide, h ) => {
|
|
||||||
hslide.setAttribute( 'data-index-h', h );
|
|
||||||
transformElement( hslide, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
|
|
||||||
|
|
||||||
if( hslide.classList.contains( 'stack' ) ) {
|
|
||||||
|
|
||||||
queryAll( hslide, 'section' ).forEach( ( vslide, v ) => {
|
|
||||||
vslide.setAttribute( 'data-index-h', h );
|
|
||||||
vslide.setAttribute( 'data-index-v', v );
|
|
||||||
|
|
||||||
transformElement( vslide, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Layout slide backgrounds
|
|
||||||
Array.from( this.Reveal.getBackgroundsElement().childNodes ).forEach( ( hbackground, h ) => {
|
|
||||||
transformElement( hbackground, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
|
|
||||||
|
|
||||||
queryAll( hbackground, '.slide-background' ).forEach( ( vbackground, v ) => {
|
|
||||||
transformElement( vbackground, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the overview viewport to the current slides.
|
|
||||||
* Called each time the current slide changes.
|
|
||||||
*/
|
|
||||||
update() {
|
|
||||||
|
|
||||||
const vmin = Math.min( window.innerWidth, window.innerHeight );
|
|
||||||
const scale = Math.max( vmin / 5, 150 ) / vmin;
|
|
||||||
const indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
this.Reveal.transformSlides( {
|
|
||||||
overview: [
|
|
||||||
'scale('+ scale +')',
|
|
||||||
'translateX('+ ( -indices.h * this.overviewSlideWidth ) +'px)',
|
|
||||||
'translateY('+ ( -indices.v * this.overviewSlideHeight ) +'px)'
|
|
||||||
].join( ' ' )
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exits the slide overview and enters the currently
|
|
||||||
* active slide.
|
|
||||||
*/
|
|
||||||
deactivate() {
|
|
||||||
|
|
||||||
// Only proceed if enabled in config
|
|
||||||
if( this.Reveal.getConfig().overview ) {
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'overview' );
|
|
||||||
|
|
||||||
// Temporarily add a class so that transitions can do different things
|
|
||||||
// depending on whether they are exiting/entering overview, or just
|
|
||||||
// moving from slide to slide
|
|
||||||
this.Reveal.getRevealElement().classList.add( 'overview-deactivating' );
|
|
||||||
|
|
||||||
setTimeout( () => {
|
|
||||||
this.Reveal.getRevealElement().classList.remove( 'overview-deactivating' );
|
|
||||||
}, 1 );
|
|
||||||
|
|
||||||
// Move the background element back out
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.Reveal.getBackgroundsElement() );
|
|
||||||
|
|
||||||
// Clean up changes made to slides
|
|
||||||
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
|
|
||||||
transformElement( slide, '' );
|
|
||||||
|
|
||||||
slide.removeEventListener( 'click', this.onSlideClicked, true );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Clean up changes made to backgrounds
|
|
||||||
queryAll( this.Reveal.getBackgroundsElement(), '.slide-background' ).forEach( background => {
|
|
||||||
transformElement( background, '' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.Reveal.transformSlides( { overview: '' } );
|
|
||||||
|
|
||||||
const indices = this.Reveal.getIndices();
|
|
||||||
|
|
||||||
this.Reveal.slide( indices.h, indices.v );
|
|
||||||
this.Reveal.layout();
|
|
||||||
this.Reveal.cueAutoSlide();
|
|
||||||
|
|
||||||
// Notify observers of the overview hiding
|
|
||||||
this.Reveal.dispatchEvent({
|
|
||||||
type: 'overviewhidden',
|
|
||||||
data: {
|
|
||||||
'indexh': indices.h,
|
|
||||||
'indexv': indices.v,
|
|
||||||
'currentSlide': this.Reveal.getCurrentSlide()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the slide overview mode on and off.
|
|
||||||
*
|
|
||||||
* @param {Boolean} [override] Flag which overrides the
|
|
||||||
* toggle logic and forcibly sets the desired state. True means
|
|
||||||
* overview is open, false means it's closed.
|
|
||||||
*/
|
|
||||||
toggle( override ) {
|
|
||||||
|
|
||||||
if( typeof override === 'boolean' ) {
|
|
||||||
override ? this.activate() : this.deactivate();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.isActive() ? this.deactivate() : this.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the overview is currently active.
|
|
||||||
*
|
|
||||||
* @return {Boolean} true if the overview is active,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
isActive() {
|
|
||||||
|
|
||||||
return this.active;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoked when a slide is and we're in the overview.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onSlideClicked( event ) {
|
|
||||||
|
|
||||||
if( this.isActive() ) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let element = event.target;
|
|
||||||
|
|
||||||
while( element && !element.nodeName.match( /section/gi ) ) {
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( element && !element.classList.contains( 'disabled' ) ) {
|
|
||||||
|
|
||||||
this.deactivate();
|
|
||||||
|
|
||||||
if( element.nodeName.match( /section/gi ) ) {
|
|
||||||
let h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
|
|
||||||
v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
|
|
||||||
|
|
||||||
this.Reveal.slide( h, v );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,254 +0,0 @@
|
|||||||
import { loadScript } from '../utils/loader.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages loading and registering of reveal.js plugins.
|
|
||||||
*/
|
|
||||||
export default class Plugins {
|
|
||||||
|
|
||||||
constructor( reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = reveal;
|
|
||||||
|
|
||||||
// Flags our current state (idle -> loading -> loaded)
|
|
||||||
this.state = 'idle';
|
|
||||||
|
|
||||||
// An id:instance map of currently registered plugins
|
|
||||||
this.registeredPlugins = {};
|
|
||||||
|
|
||||||
this.asyncDependencies = [];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads reveal.js dependencies, registers and
|
|
||||||
* initializes plugins.
|
|
||||||
*
|
|
||||||
* Plugins are direct references to a reveal.js plugin
|
|
||||||
* object that we register and initialize after any
|
|
||||||
* synchronous dependencies have loaded.
|
|
||||||
*
|
|
||||||
* Dependencies are defined via the 'dependencies' config
|
|
||||||
* option and will be loaded prior to starting reveal.js.
|
|
||||||
* Some dependencies may have an 'async' flag, if so they
|
|
||||||
* will load after reveal.js has been started up.
|
|
||||||
*/
|
|
||||||
load( plugins, dependencies ) {
|
|
||||||
|
|
||||||
this.state = 'loading';
|
|
||||||
|
|
||||||
plugins.forEach( this.registerPlugin.bind( this ) );
|
|
||||||
|
|
||||||
return new Promise( resolve => {
|
|
||||||
|
|
||||||
let scripts = [],
|
|
||||||
scriptsToLoad = 0;
|
|
||||||
|
|
||||||
dependencies.forEach( s => {
|
|
||||||
// Load if there's no condition or the condition is truthy
|
|
||||||
if( !s.condition || s.condition() ) {
|
|
||||||
if( s.async ) {
|
|
||||||
this.asyncDependencies.push( s );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
scripts.push( s );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
if( scripts.length ) {
|
|
||||||
scriptsToLoad = scripts.length;
|
|
||||||
|
|
||||||
const scriptLoadedCallback = (s) => {
|
|
||||||
if( s && typeof s.callback === 'function' ) s.callback();
|
|
||||||
|
|
||||||
if( --scriptsToLoad === 0 ) {
|
|
||||||
this.initPlugins().then( resolve );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load synchronous scripts
|
|
||||||
scripts.forEach( s => {
|
|
||||||
if( typeof s.id === 'string' ) {
|
|
||||||
this.registerPlugin( s );
|
|
||||||
scriptLoadedCallback( s );
|
|
||||||
}
|
|
||||||
else if( typeof s.src === 'string' ) {
|
|
||||||
loadScript( s.src, () => scriptLoadedCallback(s) );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn( 'Unrecognized plugin format', s );
|
|
||||||
scriptLoadedCallback();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.initPlugins().then( resolve );
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes our plugins and waits for them to be ready
|
|
||||||
* before proceeding.
|
|
||||||
*/
|
|
||||||
initPlugins() {
|
|
||||||
|
|
||||||
return new Promise( resolve => {
|
|
||||||
|
|
||||||
let pluginValues = Object.values( this.registeredPlugins );
|
|
||||||
let pluginsToInitialize = pluginValues.length;
|
|
||||||
|
|
||||||
// If there are no plugins, skip this step
|
|
||||||
if( pluginsToInitialize === 0 ) {
|
|
||||||
this.loadAsync().then( resolve );
|
|
||||||
}
|
|
||||||
// ... otherwise initialize plugins
|
|
||||||
else {
|
|
||||||
|
|
||||||
let initNextPlugin;
|
|
||||||
|
|
||||||
let afterPlugInitialized = () => {
|
|
||||||
if( --pluginsToInitialize === 0 ) {
|
|
||||||
this.loadAsync().then( resolve );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
initNextPlugin();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
// Initialize plugins serially
|
|
||||||
initNextPlugin = () => {
|
|
||||||
|
|
||||||
let plugin = pluginValues[i++];
|
|
||||||
|
|
||||||
// If the plugin has an 'init' method, invoke it
|
|
||||||
if( typeof plugin.init === 'function' ) {
|
|
||||||
let promise = plugin.init( this.Reveal );
|
|
||||||
|
|
||||||
// If the plugin returned a Promise, wait for it
|
|
||||||
if( promise && typeof promise.then === 'function' ) {
|
|
||||||
promise.then( afterPlugInitialized );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
afterPlugInitialized();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
afterPlugInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
initNextPlugin();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all async reveal.js dependencies.
|
|
||||||
*/
|
|
||||||
loadAsync() {
|
|
||||||
|
|
||||||
this.state = 'loaded';
|
|
||||||
|
|
||||||
if( this.asyncDependencies.length ) {
|
|
||||||
this.asyncDependencies.forEach( s => {
|
|
||||||
loadScript( s.src, s.callback );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a new plugin with this reveal.js instance.
|
|
||||||
*
|
|
||||||
* reveal.js waits for all registered plugins to initialize
|
|
||||||
* before considering itself ready, as long as the plugin
|
|
||||||
* is registered before calling `Reveal.initialize()`.
|
|
||||||
*/
|
|
||||||
registerPlugin( plugin ) {
|
|
||||||
|
|
||||||
// Backwards compatibility to make reveal.js ~3.9.0
|
|
||||||
// plugins work with reveal.js 4.0.0
|
|
||||||
if( arguments.length === 2 && typeof arguments[0] === 'string' ) {
|
|
||||||
plugin = arguments[1];
|
|
||||||
plugin.id = arguments[0];
|
|
||||||
}
|
|
||||||
// Plugin can optionally be a function which we call
|
|
||||||
// to create an instance of the plugin
|
|
||||||
else if( typeof plugin === 'function' ) {
|
|
||||||
plugin = plugin();
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = plugin.id;
|
|
||||||
|
|
||||||
if( typeof id !== 'string' ) {
|
|
||||||
console.warn( 'Unrecognized plugin format; can\'t find plugin.id', plugin );
|
|
||||||
}
|
|
||||||
else if( this.registeredPlugins[id] === undefined ) {
|
|
||||||
this.registeredPlugins[id] = plugin;
|
|
||||||
|
|
||||||
// If a plugin is registered after reveal.js is loaded,
|
|
||||||
// initialize it right away
|
|
||||||
if( this.state === 'loaded' && typeof plugin.init === 'function' ) {
|
|
||||||
plugin.init( this.Reveal );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a specific plugin has been registered.
|
|
||||||
*
|
|
||||||
* @param {String} id Unique plugin identifier
|
|
||||||
*/
|
|
||||||
hasPlugin( id ) {
|
|
||||||
|
|
||||||
return !!this.registeredPlugins[id];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the specific plugin instance, if a plugin
|
|
||||||
* with the given ID has been registered.
|
|
||||||
*
|
|
||||||
* @param {String} id Unique plugin identifier
|
|
||||||
*/
|
|
||||||
getPlugin( id ) {
|
|
||||||
|
|
||||||
return this.registeredPlugins[id];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegisteredPlugins() {
|
|
||||||
|
|
||||||
return this.registeredPlugins;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
Object.values( this.registeredPlugins ).forEach( plugin => {
|
|
||||||
if( typeof plugin.destroy === 'function' ) {
|
|
||||||
plugin.destroy();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.registeredPlugins = {};
|
|
||||||
this.asyncDependencies = [];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* Handles hiding of the pointer/cursor when inactive.
|
|
||||||
*/
|
|
||||||
export default class Pointer {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
// Throttles mouse wheel navigation
|
|
||||||
this.lastMouseWheelStep = 0;
|
|
||||||
|
|
||||||
// Is the mouse pointer currently hidden from view
|
|
||||||
this.cursorHidden = false;
|
|
||||||
|
|
||||||
// Timeout used to determine when the cursor is inactive
|
|
||||||
this.cursorInactiveTimeout = 0;
|
|
||||||
|
|
||||||
this.onDocumentCursorActive = this.onDocumentCursorActive.bind( this );
|
|
||||||
this.onDocumentMouseScroll = this.onDocumentMouseScroll.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
if( config.mouseWheel ) {
|
|
||||||
document.addEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.removeEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-hide the mouse pointer when its inactive
|
|
||||||
if( config.hideInactiveCursor ) {
|
|
||||||
document.addEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
|
||||||
document.addEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.showCursor();
|
|
||||||
|
|
||||||
document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
|
||||||
document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the mouse pointer after it has been hidden with
|
|
||||||
* #hideCursor.
|
|
||||||
*/
|
|
||||||
showCursor() {
|
|
||||||
|
|
||||||
if( this.cursorHidden ) {
|
|
||||||
this.cursorHidden = false;
|
|
||||||
this.Reveal.getRevealElement().style.cursor = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the mouse pointer when it's on top of the .reveal
|
|
||||||
* container.
|
|
||||||
*/
|
|
||||||
hideCursor() {
|
|
||||||
|
|
||||||
if( this.cursorHidden === false ) {
|
|
||||||
this.cursorHidden = true;
|
|
||||||
this.Reveal.getRevealElement().style.cursor = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.showCursor();
|
|
||||||
|
|
||||||
document.removeEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
|
||||||
document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
|
||||||
document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called whenever there is mouse input at the document level
|
|
||||||
* to determine if the cursor is active or not.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onDocumentCursorActive( event ) {
|
|
||||||
|
|
||||||
this.showCursor();
|
|
||||||
|
|
||||||
clearTimeout( this.cursorInactiveTimeout );
|
|
||||||
|
|
||||||
this.cursorInactiveTimeout = setTimeout( this.hideCursor.bind( this ), this.Reveal.getConfig().hideCursorTime );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles mouse wheel scrolling, throttled to avoid skipping
|
|
||||||
* multiple slides.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onDocumentMouseScroll( event ) {
|
|
||||||
|
|
||||||
if( Date.now() - this.lastMouseWheelStep > 1000 ) {
|
|
||||||
|
|
||||||
this.lastMouseWheelStep = Date.now();
|
|
||||||
|
|
||||||
let delta = event.detail || -event.wheelDelta;
|
|
||||||
if( delta > 0 ) {
|
|
||||||
this.Reveal.next();
|
|
||||||
}
|
|
||||||
else if( delta < 0 ) {
|
|
||||||
this.Reveal.prev();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,239 +0,0 @@
|
|||||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
|
|
||||||
import { queryAll, createStyleSheet } from '../utils/util.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setups up our presentation for printing/exporting to PDF.
|
|
||||||
*/
|
|
||||||
export default class PrintView {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the presentation for printing to a static
|
|
||||||
* PDF.
|
|
||||||
*/
|
|
||||||
async activate() {
|
|
||||||
|
|
||||||
const config = this.Reveal.getConfig();
|
|
||||||
const slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR )
|
|
||||||
|
|
||||||
// Compute slide numbers now, before we start duplicating slides
|
|
||||||
const injectPageNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );
|
|
||||||
|
|
||||||
const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
|
|
||||||
|
|
||||||
// Dimensions of the PDF pages
|
|
||||||
const pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
|
|
||||||
pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
|
|
||||||
|
|
||||||
// Dimensions of slides within the pages
|
|
||||||
const slideWidth = slideSize.width,
|
|
||||||
slideHeight = slideSize.height;
|
|
||||||
|
|
||||||
await new Promise( requestAnimationFrame );
|
|
||||||
|
|
||||||
// Let the browser know what page size we want to print
|
|
||||||
createStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
|
|
||||||
|
|
||||||
// Limit the size of certain elements to the dimensions of the slide
|
|
||||||
createStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
|
|
||||||
|
|
||||||
document.documentElement.classList.add( 'reveal-print', 'print-pdf' );
|
|
||||||
document.body.style.width = pageWidth + 'px';
|
|
||||||
document.body.style.height = pageHeight + 'px';
|
|
||||||
|
|
||||||
const viewportElement = this.Reveal.getViewportElement();
|
|
||||||
let presentationBackground;
|
|
||||||
if( viewportElement ) {
|
|
||||||
const viewportStyles = window.getComputedStyle( viewportElement );
|
|
||||||
if( viewportStyles && viewportStyles.background ) {
|
|
||||||
presentationBackground = viewportStyles.background;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure stretch elements fit on slide
|
|
||||||
await new Promise( requestAnimationFrame );
|
|
||||||
this.Reveal.layoutSlideContents( slideWidth, slideHeight );
|
|
||||||
|
|
||||||
// Batch scrollHeight access to prevent layout thrashing
|
|
||||||
await new Promise( requestAnimationFrame );
|
|
||||||
|
|
||||||
const slideScrollHeights = slides.map( slide => slide.scrollHeight );
|
|
||||||
|
|
||||||
const pages = [];
|
|
||||||
const pageContainer = slides[0].parentNode;
|
|
||||||
let slideNumber = 1;
|
|
||||||
|
|
||||||
// Slide and slide background layout
|
|
||||||
slides.forEach( function( slide, index ) {
|
|
||||||
|
|
||||||
// Vertical stacks are not centred since their section
|
|
||||||
// children will be
|
|
||||||
if( slide.classList.contains( 'stack' ) === false ) {
|
|
||||||
// Center the slide inside of the page, giving the slide some margin
|
|
||||||
let left = ( pageWidth - slideWidth ) / 2;
|
|
||||||
let top = ( pageHeight - slideHeight ) / 2;
|
|
||||||
|
|
||||||
const contentHeight = slideScrollHeights[ index ];
|
|
||||||
let numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
|
|
||||||
|
|
||||||
// Adhere to configured pages per slide limit
|
|
||||||
numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
|
|
||||||
|
|
||||||
// Center slides vertically
|
|
||||||
if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
|
|
||||||
top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the slide in a page element and hide its overflow
|
|
||||||
// so that no page ever flows onto another
|
|
||||||
const page = document.createElement( 'div' );
|
|
||||||
pages.push( page );
|
|
||||||
|
|
||||||
page.className = 'pdf-page';
|
|
||||||
page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
|
|
||||||
|
|
||||||
// Copy the presentation-wide background to each individual
|
|
||||||
// page when printing
|
|
||||||
if( presentationBackground ) {
|
|
||||||
page.style.background = presentationBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
page.appendChild( slide );
|
|
||||||
|
|
||||||
// Position the slide inside of the page
|
|
||||||
slide.style.left = left + 'px';
|
|
||||||
slide.style.top = top + 'px';
|
|
||||||
slide.style.width = slideWidth + 'px';
|
|
||||||
|
|
||||||
this.Reveal.slideContent.layout( slide );
|
|
||||||
|
|
||||||
if( slide.slideBackgroundElement ) {
|
|
||||||
page.insertBefore( slide.slideBackgroundElement, slide );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject notes if `showNotes` is enabled
|
|
||||||
if( config.showNotes ) {
|
|
||||||
|
|
||||||
// Are there notes for this slide?
|
|
||||||
const notes = this.Reveal.getSlideNotes( slide );
|
|
||||||
if( notes ) {
|
|
||||||
|
|
||||||
const notesSpacing = 8;
|
|
||||||
const notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
|
|
||||||
const notesElement = document.createElement( 'div' );
|
|
||||||
notesElement.classList.add( 'speaker-notes' );
|
|
||||||
notesElement.classList.add( 'speaker-notes-pdf' );
|
|
||||||
notesElement.setAttribute( 'data-layout', notesLayout );
|
|
||||||
notesElement.innerHTML = notes;
|
|
||||||
|
|
||||||
if( notesLayout === 'separate-page' ) {
|
|
||||||
pages.push( notesElement );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
notesElement.style.left = notesSpacing + 'px';
|
|
||||||
notesElement.style.bottom = notesSpacing + 'px';
|
|
||||||
notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
|
|
||||||
page.appendChild( notesElement );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject page numbers if `slideNumbers` are enabled
|
|
||||||
if( injectPageNumbers ) {
|
|
||||||
const numberElement = document.createElement( 'div' );
|
|
||||||
numberElement.classList.add( 'slide-number' );
|
|
||||||
numberElement.classList.add( 'slide-number-pdf' );
|
|
||||||
numberElement.innerHTML = slideNumber++;
|
|
||||||
page.appendChild( numberElement );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy page and show fragments one after another
|
|
||||||
if( config.pdfSeparateFragments ) {
|
|
||||||
|
|
||||||
// Each fragment 'group' is an array containing one or more
|
|
||||||
// fragments. Multiple fragments that appear at the same time
|
|
||||||
// are part of the same group.
|
|
||||||
const fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true );
|
|
||||||
|
|
||||||
let previousFragmentStep;
|
|
||||||
|
|
||||||
fragmentGroups.forEach( function( fragments, index ) {
|
|
||||||
|
|
||||||
// Remove 'current-fragment' from the previous group
|
|
||||||
if( previousFragmentStep ) {
|
|
||||||
previousFragmentStep.forEach( function( fragment ) {
|
|
||||||
fragment.classList.remove( 'current-fragment' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the fragments for the current index
|
|
||||||
fragments.forEach( function( fragment ) {
|
|
||||||
fragment.classList.add( 'visible', 'current-fragment' );
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
// Create a separate page for the current fragment state
|
|
||||||
const clonedPage = page.cloneNode( true );
|
|
||||||
|
|
||||||
// Inject unique page numbers for fragments
|
|
||||||
if( injectPageNumbers ) {
|
|
||||||
const numberElement = clonedPage.querySelector( '.slide-number-pdf' );
|
|
||||||
const fragmentNumber = index + 1;
|
|
||||||
numberElement.innerHTML += '.' + fragmentNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
pages.push( clonedPage );
|
|
||||||
|
|
||||||
previousFragmentStep = fragments;
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
// Reset the first/original page so that all fragments are hidden
|
|
||||||
fragmentGroups.forEach( function( fragments ) {
|
|
||||||
fragments.forEach( function( fragment ) {
|
|
||||||
fragment.classList.remove( 'visible', 'current-fragment' );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
// Show all fragments
|
|
||||||
else {
|
|
||||||
queryAll( page, '.fragment:not(.fade-out)' ).forEach( function( fragment ) {
|
|
||||||
fragment.classList.add( 'visible' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
await new Promise( requestAnimationFrame );
|
|
||||||
|
|
||||||
pages.forEach( page => pageContainer.appendChild( page ) );
|
|
||||||
|
|
||||||
// Re-run JS-based content layout after the slide is added to page DOM
|
|
||||||
this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
|
|
||||||
|
|
||||||
// Notify subscribers that the PDF layout is good to go
|
|
||||||
this.Reveal.dispatchEvent({ type: 'pdf-ready' });
|
|
||||||
|
|
||||||
viewportElement.classList.remove( 'loading-scroll-mode' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the print mode is/should be activated.
|
|
||||||
*/
|
|
||||||
isActive() {
|
|
||||||
|
|
||||||
return this.Reveal.getConfig().view === 'print';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
/**
|
|
||||||
* Creates a visual progress bar for the presentation.
|
|
||||||
*/
|
|
||||||
export default class Progress {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.onProgressClicked = this.onProgressClicked.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
|
||||||
this.element.className = 'progress';
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
|
|
||||||
this.bar = document.createElement( 'span' );
|
|
||||||
this.element.appendChild( this.bar );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
this.element.style.display = config.progress ? 'block' : 'none';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().progress && this.element ) {
|
|
||||||
this.element.addEventListener( 'click', this.onProgressClicked, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
if ( this.Reveal.getConfig().progress && this.element ) {
|
|
||||||
this.element.removeEventListener( 'click', this.onProgressClicked, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the progress bar to reflect the current slide.
|
|
||||||
*/
|
|
||||||
update() {
|
|
||||||
|
|
||||||
// Update progress if enabled
|
|
||||||
if( this.Reveal.getConfig().progress && this.bar ) {
|
|
||||||
|
|
||||||
let scale = this.Reveal.getProgress();
|
|
||||||
|
|
||||||
// Don't fill the progress bar if there's only one slide
|
|
||||||
if( this.Reveal.getTotalSlides() < 2 ) {
|
|
||||||
scale = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bar.style.transform = 'scaleX('+ scale +')';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxWidth() {
|
|
||||||
|
|
||||||
return this.Reveal.getRevealElement().offsetWidth;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clicking on the progress bar results in a navigation to the
|
|
||||||
* closest approximate horizontal slide using this equation:
|
|
||||||
*
|
|
||||||
* ( clickX / presentationWidth ) * numberOfSlides
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onProgressClicked( event ) {
|
|
||||||
|
|
||||||
this.Reveal.onUserInput( event );
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let slides = this.Reveal.getSlides();
|
|
||||||
let slidesTotal = slides.length;
|
|
||||||
let slideIndex = Math.floor( ( event.clientX / this.getMaxWidth() ) * slidesTotal );
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().rtl ) {
|
|
||||||
slideIndex = slidesTotal - slideIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetIndices = this.Reveal.getIndices(slides[slideIndex]);
|
|
||||||
this.Reveal.slide( targetIndices.h, targetIndices.v );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,906 +0,0 @@
|
|||||||
import { HORIZONTAL_SLIDES_SELECTOR } from '../utils/constants.js'
|
|
||||||
import { queryAll } from '../utils/util.js'
|
|
||||||
|
|
||||||
const HIDE_SCROLLBAR_TIMEOUT = 500;
|
|
||||||
const MAX_PROGRESS_SPACING = 4;
|
|
||||||
const MIN_PROGRESS_SEGMENT_HEIGHT = 6;
|
|
||||||
const MIN_PLAYHEAD_HEIGHT = 8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scroll view lets you read a reveal.js presentation
|
|
||||||
* as a linear scrollable page.
|
|
||||||
*/
|
|
||||||
export default class ScrollView {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
this.activatedCallbacks = [];
|
|
||||||
|
|
||||||
this.onScroll = this.onScroll.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activates the scroll view. This rearranges the presentation DOM
|
|
||||||
* by—among other things—wrapping each slide in a page element.
|
|
||||||
*/
|
|
||||||
activate() {
|
|
||||||
|
|
||||||
if( this.active ) return;
|
|
||||||
|
|
||||||
const stateBeforeActivation = this.Reveal.getState();
|
|
||||||
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
// Store the full presentation HTML so that we can restore it
|
|
||||||
// when/if the scroll view is deactivated
|
|
||||||
this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
|
|
||||||
|
|
||||||
const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
|
|
||||||
|
|
||||||
this.viewportElement.classList.add( 'loading-scroll-mode', 'reveal-scroll' );
|
|
||||||
|
|
||||||
let presentationBackground;
|
|
||||||
|
|
||||||
const viewportStyles = window.getComputedStyle( this.viewportElement );
|
|
||||||
if( viewportStyles && viewportStyles.background ) {
|
|
||||||
presentationBackground = viewportStyles.background;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageElements = [];
|
|
||||||
const pageContainer = horizontalSlides[0].parentNode;
|
|
||||||
|
|
||||||
let previousSlide;
|
|
||||||
|
|
||||||
// Creates a new page element and appends the given slide/bg
|
|
||||||
// to it.
|
|
||||||
const createPageElement = ( slide, h, v ) => {
|
|
||||||
|
|
||||||
let contentContainer;
|
|
||||||
|
|
||||||
// If this slide is part of an auto-animation sequence, we
|
|
||||||
// group it under the same page element as the previous slide
|
|
||||||
if( previousSlide && this.Reveal.shouldAutoAnimateBetween( previousSlide, slide ) ) {
|
|
||||||
contentContainer = document.createElement( 'div' );
|
|
||||||
contentContainer.className = 'scroll-page-content scroll-auto-animate-page';
|
|
||||||
contentContainer.style.display = 'none';
|
|
||||||
previousSlide.closest( '.scroll-page-content' ).parentNode.appendChild( contentContainer );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Wrap the slide in a page element and hide its overflow
|
|
||||||
// so that no page ever flows onto another
|
|
||||||
const page = document.createElement( 'div' );
|
|
||||||
page.className = 'scroll-page';
|
|
||||||
pageElements.push( page );
|
|
||||||
|
|
||||||
// Copy the presentation-wide background to each page
|
|
||||||
if( presentationBackground ) {
|
|
||||||
page.style.background = presentationBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stickyContainer = document.createElement( 'div' );
|
|
||||||
stickyContainer.className = 'scroll-page-sticky';
|
|
||||||
page.appendChild( stickyContainer );
|
|
||||||
|
|
||||||
contentContainer = document.createElement( 'div' );
|
|
||||||
contentContainer.className = 'scroll-page-content';
|
|
||||||
stickyContainer.appendChild( contentContainer );
|
|
||||||
}
|
|
||||||
|
|
||||||
contentContainer.appendChild( slide );
|
|
||||||
|
|
||||||
slide.classList.remove( 'past', 'future' );
|
|
||||||
slide.setAttribute( 'data-index-h', h );
|
|
||||||
slide.setAttribute( 'data-index-v', v );
|
|
||||||
|
|
||||||
if( slide.slideBackgroundElement ) {
|
|
||||||
slide.slideBackgroundElement.remove( 'past', 'future' );
|
|
||||||
contentContainer.insertBefore( slide.slideBackgroundElement, slide );
|
|
||||||
}
|
|
||||||
|
|
||||||
previousSlide = slide;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide and slide background layout
|
|
||||||
horizontalSlides.forEach( ( horizontalSlide, h ) => {
|
|
||||||
|
|
||||||
if( this.Reveal.isVerticalStack( horizontalSlide ) ) {
|
|
||||||
horizontalSlide.querySelectorAll( 'section' ).forEach( ( verticalSlide, v ) => {
|
|
||||||
createPageElement( verticalSlide, h, v );
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
createPageElement( horizontalSlide, h, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
}, this );
|
|
||||||
|
|
||||||
this.createProgressBar();
|
|
||||||
|
|
||||||
// Remove leftover stacks
|
|
||||||
queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
|
|
||||||
|
|
||||||
// Add our newly created pages to the DOM
|
|
||||||
pageElements.forEach( page => pageContainer.appendChild( page ) );
|
|
||||||
|
|
||||||
// Re-run JS-based content layout after the slide is added to page DOM
|
|
||||||
this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
|
|
||||||
|
|
||||||
this.Reveal.layout();
|
|
||||||
this.Reveal.setState( stateBeforeActivation );
|
|
||||||
|
|
||||||
this.activatedCallbacks.forEach( callback => callback() );
|
|
||||||
this.activatedCallbacks = [];
|
|
||||||
|
|
||||||
this.restoreScrollPosition();
|
|
||||||
|
|
||||||
this.viewportElement.classList.remove( 'loading-scroll-mode' );
|
|
||||||
this.viewportElement.addEventListener( 'scroll', this.onScroll, { passive: true } );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deactivates the scroll view and restores the standard slide-based
|
|
||||||
* presentation.
|
|
||||||
*/
|
|
||||||
deactivate() {
|
|
||||||
|
|
||||||
if( !this.active ) return;
|
|
||||||
|
|
||||||
const stateBeforeDeactivation = this.Reveal.getState();
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
|
|
||||||
this.viewportElement.removeEventListener( 'scroll', this.onScroll );
|
|
||||||
this.viewportElement.classList.remove( 'reveal-scroll' );
|
|
||||||
|
|
||||||
this.removeProgressBar();
|
|
||||||
|
|
||||||
this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
|
|
||||||
this.Reveal.sync();
|
|
||||||
this.Reveal.setState( stateBeforeDeactivation );
|
|
||||||
|
|
||||||
this.slideHTMLBeforeActivation = null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle( override ) {
|
|
||||||
|
|
||||||
if( typeof override === 'boolean' ) {
|
|
||||||
override ? this.activate() : this.deactivate();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.isActive() ? this.deactivate() : this.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the scroll view is currently active.
|
|
||||||
*/
|
|
||||||
isActive() {
|
|
||||||
|
|
||||||
return this.active;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the progress bar component.
|
|
||||||
*/
|
|
||||||
createProgressBar() {
|
|
||||||
|
|
||||||
this.progressBar = document.createElement( 'div' );
|
|
||||||
this.progressBar.className = 'scrollbar';
|
|
||||||
|
|
||||||
this.progressBarInner = document.createElement( 'div' );
|
|
||||||
this.progressBarInner.className = 'scrollbar-inner';
|
|
||||||
this.progressBar.appendChild( this.progressBarInner );
|
|
||||||
|
|
||||||
this.progressBarPlayhead = document.createElement( 'div' );
|
|
||||||
this.progressBarPlayhead.className = 'scrollbar-playhead';
|
|
||||||
this.progressBarInner.appendChild( this.progressBarPlayhead );
|
|
||||||
|
|
||||||
this.viewportElement.insertBefore( this.progressBar, this.viewportElement.firstChild );
|
|
||||||
|
|
||||||
const handleDocumentMouseMove = ( event ) => {
|
|
||||||
|
|
||||||
let progress = ( event.clientY - this.progressBarInner.getBoundingClientRect().top ) / this.progressBarHeight;
|
|
||||||
progress = Math.max( Math.min( progress, 1 ), 0 );
|
|
||||||
|
|
||||||
this.viewportElement.scrollTop = progress * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDocumentMouseUp = ( event ) => {
|
|
||||||
|
|
||||||
this.draggingProgressBar = false;
|
|
||||||
this.showProgressBar();
|
|
||||||
|
|
||||||
document.removeEventListener( 'mousemove', handleDocumentMouseMove );
|
|
||||||
document.removeEventListener( 'mouseup', handleDocumentMouseUp );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseDown = ( event ) => {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
this.draggingProgressBar = true;
|
|
||||||
|
|
||||||
document.addEventListener( 'mousemove', handleDocumentMouseMove );
|
|
||||||
document.addEventListener( 'mouseup', handleDocumentMouseUp );
|
|
||||||
|
|
||||||
handleDocumentMouseMove( event );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.progressBarInner.addEventListener( 'mousedown', handleMouseDown );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
removeProgressBar() {
|
|
||||||
|
|
||||||
if( this.progressBar ) {
|
|
||||||
this.progressBar.remove();
|
|
||||||
this.progressBar = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
layout() {
|
|
||||||
|
|
||||||
if( this.isActive() ) {
|
|
||||||
this.syncPages();
|
|
||||||
this.syncScrollPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates our pages to match the latest configuration and
|
|
||||||
* presentation size.
|
|
||||||
*/
|
|
||||||
syncPages() {
|
|
||||||
|
|
||||||
const config = this.Reveal.getConfig();
|
|
||||||
|
|
||||||
const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
|
|
||||||
const scale = this.Reveal.getScale();
|
|
||||||
const useCompactLayout = config.scrollLayout === 'compact';
|
|
||||||
|
|
||||||
const viewportHeight = this.viewportElement.offsetHeight;
|
|
||||||
const compactHeight = slideSize.height * scale;
|
|
||||||
const pageHeight = useCompactLayout ? compactHeight : viewportHeight;
|
|
||||||
|
|
||||||
// The height that needs to be scrolled between scroll triggers
|
|
||||||
this.scrollTriggerHeight = useCompactLayout ? compactHeight : viewportHeight;
|
|
||||||
|
|
||||||
this.viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
|
|
||||||
this.viewportElement.style.scrollSnapType = typeof config.scrollSnap === 'string' ? `y ${config.scrollSnap}` : '';
|
|
||||||
|
|
||||||
// This will hold all scroll triggers used to show/hide slides
|
|
||||||
this.slideTriggers = [];
|
|
||||||
|
|
||||||
const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.scroll-page' ) );
|
|
||||||
|
|
||||||
this.pages = pageElements.map( pageElement => {
|
|
||||||
const page = this.createPage({
|
|
||||||
pageElement,
|
|
||||||
slideElement: pageElement.querySelector( 'section' ),
|
|
||||||
stickyElement: pageElement.querySelector( '.scroll-page-sticky' ),
|
|
||||||
contentElement: pageElement.querySelector( '.scroll-page-content' ),
|
|
||||||
backgroundElement: pageElement.querySelector( '.slide-background' ),
|
|
||||||
autoAnimateElements: pageElement.querySelectorAll( '.scroll-auto-animate-page' ),
|
|
||||||
autoAnimatePages: []
|
|
||||||
});
|
|
||||||
|
|
||||||
page.pageElement.style.setProperty( '--slide-height', config.center === true ? 'auto' : slideSize.height + 'px' );
|
|
||||||
|
|
||||||
this.slideTriggers.push({
|
|
||||||
page: page,
|
|
||||||
activate: () => this.activatePage( page ),
|
|
||||||
deactivate: () => this.deactivatePage( page )
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create scroll triggers that show/hide fragments
|
|
||||||
this.createFragmentTriggersForPage( page );
|
|
||||||
|
|
||||||
// Create scroll triggers for triggering auto-animate steps
|
|
||||||
if( page.autoAnimateElements.length > 0 ) {
|
|
||||||
this.createAutoAnimateTriggersForPage( page );
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalScrollTriggerCount = Math.max( page.scrollTriggers.length - 1, 0 );
|
|
||||||
|
|
||||||
// Each auto-animate step may include its own scroll triggers
|
|
||||||
// for fragments, ensure we count those as well
|
|
||||||
totalScrollTriggerCount += page.autoAnimatePages.reduce( ( total, page ) => {
|
|
||||||
return total + Math.max( page.scrollTriggers.length - 1, 0 );
|
|
||||||
}, page.autoAnimatePages.length );
|
|
||||||
|
|
||||||
// Clean up from previous renders
|
|
||||||
page.pageElement.querySelectorAll( '.scroll-snap-point' ).forEach( el => el.remove() );
|
|
||||||
|
|
||||||
// Create snap points for all scroll triggers
|
|
||||||
// - Can't be absolute in FF
|
|
||||||
// - Can't be 0-height in Safari
|
|
||||||
// - Can't use snap-align on parent in Safari because then
|
|
||||||
// inner triggers won't work
|
|
||||||
for( let i = 0; i < totalScrollTriggerCount + 1; i++ ) {
|
|
||||||
const triggerStick = document.createElement( 'div' );
|
|
||||||
triggerStick.className = 'scroll-snap-point';
|
|
||||||
triggerStick.style.height = this.scrollTriggerHeight + 'px';
|
|
||||||
triggerStick.style.scrollSnapAlign = useCompactLayout ? 'center' : 'start';
|
|
||||||
page.pageElement.appendChild( triggerStick );
|
|
||||||
|
|
||||||
if( i === 0 ) {
|
|
||||||
triggerStick.style.marginTop = -this.scrollTriggerHeight + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the compact layout, only slides with scroll triggers cover the
|
|
||||||
// full viewport height. This helps avoid empty gaps before or after
|
|
||||||
// a sticky slide.
|
|
||||||
if( useCompactLayout && page.scrollTriggers.length > 0 ) {
|
|
||||||
page.pageHeight = viewportHeight;
|
|
||||||
page.pageElement.style.setProperty( '--page-height', viewportHeight + 'px' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
page.pageHeight = pageHeight;
|
|
||||||
page.pageElement.style.removeProperty( '--page-height' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add scroll padding based on how many scroll triggers we have
|
|
||||||
page.scrollPadding = this.scrollTriggerHeight * totalScrollTriggerCount;
|
|
||||||
|
|
||||||
// The total height including scrollable space
|
|
||||||
page.totalHeight = page.pageHeight + page.scrollPadding;
|
|
||||||
|
|
||||||
// This is used to pad the height of our page in CSS
|
|
||||||
page.pageElement.style.setProperty( '--page-scroll-padding', page.scrollPadding + 'px' );
|
|
||||||
|
|
||||||
// If this is a sticky page, stick it to the vertical center
|
|
||||||
if( totalScrollTriggerCount > 0 ) {
|
|
||||||
page.stickyElement.style.position = 'sticky';
|
|
||||||
page.stickyElement.style.top = Math.max( ( viewportHeight - page.pageHeight ) / 2, 0 ) + 'px';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
page.stickyElement.style.position = 'relative';
|
|
||||||
page.pageElement.style.scrollSnapAlign = page.pageHeight < viewportHeight ? 'center' : 'start';
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.setTriggerRanges();
|
|
||||||
|
|
||||||
/*
|
|
||||||
console.log(this.slideTriggers.map( t => {
|
|
||||||
return {
|
|
||||||
range: `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`,
|
|
||||||
triggers: t.page.scrollTriggers.map( t => {
|
|
||||||
return `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`
|
|
||||||
}).join( ', ' ),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.viewportElement.setAttribute( 'data-scrollbar', config.scrollProgress );
|
|
||||||
|
|
||||||
if( config.scrollProgress && this.totalScrollTriggerCount > 1 ) {
|
|
||||||
// Create the progress bar if it doesn't already exist
|
|
||||||
if( !this.progressBar ) this.createProgressBar();
|
|
||||||
|
|
||||||
this.syncProgressBar();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.removeProgressBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates and sets the scroll range for all of our scroll
|
|
||||||
* triggers.
|
|
||||||
*/
|
|
||||||
setTriggerRanges() {
|
|
||||||
|
|
||||||
// Calculate the total number of scroll triggers
|
|
||||||
this.totalScrollTriggerCount = this.slideTriggers.reduce( ( total, trigger ) => {
|
|
||||||
return total + Math.max( trigger.page.scrollTriggers.length, 1 );
|
|
||||||
}, 0 );
|
|
||||||
|
|
||||||
let rangeStart = 0;
|
|
||||||
|
|
||||||
// Calculate the scroll range of each scroll trigger on a scale
|
|
||||||
// of 0-1
|
|
||||||
this.slideTriggers.forEach( ( trigger, i ) => {
|
|
||||||
trigger.range = [
|
|
||||||
rangeStart,
|
|
||||||
rangeStart + Math.max( trigger.page.scrollTriggers.length, 1 ) / this.totalScrollTriggerCount
|
|
||||||
];
|
|
||||||
|
|
||||||
const scrollTriggerSegmentSize = ( trigger.range[1] - trigger.range[0] ) / trigger.page.scrollTriggers.length;
|
|
||||||
// Set the range for each inner scroll trigger
|
|
||||||
trigger.page.scrollTriggers.forEach( ( scrollTrigger, i ) => {
|
|
||||||
scrollTrigger.range = [
|
|
||||||
rangeStart + i * scrollTriggerSegmentSize,
|
|
||||||
rangeStart + ( i + 1 ) * scrollTriggerSegmentSize
|
|
||||||
];
|
|
||||||
} );
|
|
||||||
|
|
||||||
rangeStart = trigger.range[1];
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates one scroll trigger for each fragments in the given page.
|
|
||||||
*
|
|
||||||
* @param {*} page
|
|
||||||
*/
|
|
||||||
createFragmentTriggersForPage( page, slideElement ) {
|
|
||||||
|
|
||||||
slideElement = slideElement || page.slideElement;
|
|
||||||
|
|
||||||
// Each fragment 'group' is an array containing one or more
|
|
||||||
// fragments. Multiple fragments that appear at the same time
|
|
||||||
// are part of the same group.
|
|
||||||
const fragmentGroups = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment' ), true );
|
|
||||||
|
|
||||||
// Create scroll triggers that show/hide fragments
|
|
||||||
if( fragmentGroups.length ) {
|
|
||||||
page.fragments = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment:not(.disabled)' ) );
|
|
||||||
page.scrollTriggers.push(
|
|
||||||
// Trigger for the initial state with no fragments visible
|
|
||||||
{
|
|
||||||
activate: () => {
|
|
||||||
this.Reveal.fragments.update( -1, page.fragments, slideElement );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Triggers for each fragment group
|
|
||||||
fragmentGroups.forEach( ( fragments, i ) => {
|
|
||||||
page.scrollTriggers.push({
|
|
||||||
activate: () => {
|
|
||||||
this.Reveal.fragments.update( i, page.fragments, slideElement );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return page.scrollTriggers.length;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates scroll triggers for the auto-animate steps in the
|
|
||||||
* given page.
|
|
||||||
*
|
|
||||||
* @param {*} page
|
|
||||||
*/
|
|
||||||
createAutoAnimateTriggersForPage( page ) {
|
|
||||||
|
|
||||||
if( page.autoAnimateElements.length > 0 ) {
|
|
||||||
|
|
||||||
// Triggers for each subsequent auto-animate slide
|
|
||||||
this.slideTriggers.push( ...Array.from( page.autoAnimateElements ).map( ( autoAnimateElement, i ) => {
|
|
||||||
let autoAnimatePage = this.createPage({
|
|
||||||
slideElement: autoAnimateElement.querySelector( 'section' ),
|
|
||||||
contentElement: autoAnimateElement,
|
|
||||||
backgroundElement: autoAnimateElement.querySelector( '.slide-background' )
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create fragment scroll triggers for the auto-animate slide
|
|
||||||
this.createFragmentTriggersForPage( autoAnimatePage, autoAnimatePage.slideElement );
|
|
||||||
|
|
||||||
page.autoAnimatePages.push( autoAnimatePage );
|
|
||||||
|
|
||||||
// Return our slide trigger
|
|
||||||
return {
|
|
||||||
page: autoAnimatePage,
|
|
||||||
activate: () => this.activatePage( autoAnimatePage ),
|
|
||||||
deactivate: () => this.deactivatePage( autoAnimatePage )
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method for creating a page definition and adding
|
|
||||||
* required fields. A "page" is a slide or auto-animate step.
|
|
||||||
*/
|
|
||||||
createPage( page ) {
|
|
||||||
|
|
||||||
page.scrollTriggers = [];
|
|
||||||
page.indexh = parseInt( page.slideElement.getAttribute( 'data-index-h' ), 10 );
|
|
||||||
page.indexv = parseInt( page.slideElement.getAttribute( 'data-index-v' ), 10 );
|
|
||||||
|
|
||||||
return page;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rerenders progress bar segments so that they match the current
|
|
||||||
* reveal.js config and size.
|
|
||||||
*/
|
|
||||||
syncProgressBar() {
|
|
||||||
|
|
||||||
this.progressBarInner.querySelectorAll( '.scrollbar-slide' ).forEach( slide => slide.remove() );
|
|
||||||
|
|
||||||
const scrollHeight = this.viewportElement.scrollHeight;
|
|
||||||
const viewportHeight = this.viewportElement.offsetHeight;
|
|
||||||
const viewportHeightFactor = viewportHeight / scrollHeight;
|
|
||||||
|
|
||||||
this.progressBarHeight = this.progressBarInner.offsetHeight;
|
|
||||||
this.playheadHeight = Math.max( viewportHeightFactor * this.progressBarHeight, MIN_PLAYHEAD_HEIGHT );
|
|
||||||
this.progressBarScrollableHeight = this.progressBarHeight - this.playheadHeight;
|
|
||||||
|
|
||||||
const progressSegmentHeight = viewportHeight / scrollHeight * this.progressBarHeight;
|
|
||||||
const spacing = Math.min( progressSegmentHeight / 8, MAX_PROGRESS_SPACING );
|
|
||||||
|
|
||||||
this.progressBarPlayhead.style.height = this.playheadHeight - spacing + 'px';
|
|
||||||
|
|
||||||
// Don't show individual segments if they're too small
|
|
||||||
if( progressSegmentHeight > MIN_PROGRESS_SEGMENT_HEIGHT ) {
|
|
||||||
|
|
||||||
this.slideTriggers.forEach( slideTrigger => {
|
|
||||||
|
|
||||||
const { page } = slideTrigger;
|
|
||||||
|
|
||||||
// Visual representation of a slide
|
|
||||||
page.progressBarSlide = document.createElement( 'div' );
|
|
||||||
page.progressBarSlide.className = 'scrollbar-slide';
|
|
||||||
page.progressBarSlide.style.top = slideTrigger.range[0] * this.progressBarHeight + 'px';
|
|
||||||
page.progressBarSlide.style.height = ( slideTrigger.range[1] - slideTrigger.range[0] ) * this.progressBarHeight - spacing + 'px';
|
|
||||||
page.progressBarSlide.classList.toggle( 'has-triggers', page.scrollTriggers.length > 0 );
|
|
||||||
this.progressBarInner.appendChild( page.progressBarSlide );
|
|
||||||
|
|
||||||
// Visual representations of each scroll trigger
|
|
||||||
page.scrollTriggerElements = page.scrollTriggers.map( ( trigger, i ) => {
|
|
||||||
|
|
||||||
const triggerElement = document.createElement( 'div' );
|
|
||||||
triggerElement.className = 'scrollbar-trigger';
|
|
||||||
triggerElement.style.top = ( trigger.range[0] - slideTrigger.range[0] ) * this.progressBarHeight + 'px';
|
|
||||||
triggerElement.style.height = ( trigger.range[1] - trigger.range[0] ) * this.progressBarHeight - spacing + 'px';
|
|
||||||
page.progressBarSlide.appendChild( triggerElement );
|
|
||||||
|
|
||||||
if( i === 0 ) triggerElement.style.display = 'none';
|
|
||||||
|
|
||||||
return triggerElement;
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
this.pages.forEach( page => page.progressBarSlide = null );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the current scroll position and updates our active
|
|
||||||
* trigger states accordingly.
|
|
||||||
*/
|
|
||||||
syncScrollPosition() {
|
|
||||||
|
|
||||||
const viewportHeight = this.viewportElement.offsetHeight;
|
|
||||||
const viewportHeightFactor = viewportHeight / this.viewportElement.scrollHeight;
|
|
||||||
|
|
||||||
const scrollTop = this.viewportElement.scrollTop;
|
|
||||||
const scrollHeight = this.viewportElement.scrollHeight - viewportHeight
|
|
||||||
const scrollProgress = Math.max( Math.min( scrollTop / scrollHeight, 1 ), 0 );
|
|
||||||
const scrollProgressMid = Math.max( Math.min( ( scrollTop + viewportHeight / 2 ) / this.viewportElement.scrollHeight, 1 ), 0 );
|
|
||||||
|
|
||||||
let activePage;
|
|
||||||
|
|
||||||
this.slideTriggers.forEach( ( trigger ) => {
|
|
||||||
const { page } = trigger;
|
|
||||||
|
|
||||||
const shouldPreload = scrollProgress >= trigger.range[0] - viewportHeightFactor*2 &&
|
|
||||||
scrollProgress <= trigger.range[1] + viewportHeightFactor*2;
|
|
||||||
|
|
||||||
// Load slides that are within the preload range
|
|
||||||
if( shouldPreload && !page.loaded ) {
|
|
||||||
page.loaded = true;
|
|
||||||
this.Reveal.slideContent.load( page.slideElement );
|
|
||||||
}
|
|
||||||
else if( page.loaded ) {
|
|
||||||
page.loaded = false;
|
|
||||||
this.Reveal.slideContent.unload( page.slideElement );
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're within this trigger range, activate it
|
|
||||||
if( scrollProgress >= trigger.range[0] && scrollProgress <= trigger.range[1] ) {
|
|
||||||
this.activateTrigger( trigger );
|
|
||||||
activePage = trigger.page;
|
|
||||||
}
|
|
||||||
// .. otherwise deactivate
|
|
||||||
else if( trigger.active ) {
|
|
||||||
this.deactivateTrigger( trigger );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Each page can have its own scroll triggers, check if any of those
|
|
||||||
// need to be activated/deactivated
|
|
||||||
if( activePage ) {
|
|
||||||
activePage.scrollTriggers.forEach( ( trigger ) => {
|
|
||||||
if( scrollProgressMid >= trigger.range[0] && scrollProgressMid <= trigger.range[1] ) {
|
|
||||||
this.activateTrigger( trigger );
|
|
||||||
}
|
|
||||||
else if( trigger.active ) {
|
|
||||||
this.deactivateTrigger( trigger );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update our visual progress indication
|
|
||||||
this.setProgressBarValue( scrollTop / ( this.viewportElement.scrollHeight - viewportHeight ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the progress bar playhead to the specified position.
|
|
||||||
*
|
|
||||||
* @param {number} progress 0-1
|
|
||||||
*/
|
|
||||||
setProgressBarValue( progress ) {
|
|
||||||
|
|
||||||
if( this.progressBar ) {
|
|
||||||
|
|
||||||
this.progressBarPlayhead.style.transform = `translateY(${progress * this.progressBarScrollableHeight}px)`;
|
|
||||||
|
|
||||||
this.getAllPages()
|
|
||||||
.filter( page => page.progressBarSlide )
|
|
||||||
.forEach( ( page ) => {
|
|
||||||
page.progressBarSlide.classList.toggle( 'active', page.active === true );
|
|
||||||
|
|
||||||
page.scrollTriggers.forEach( ( trigger, i ) => {
|
|
||||||
page.scrollTriggerElements[i].classList.toggle( 'active', page.active === true && trigger.active === true );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.showProgressBar();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the progress bar and, if configured, automatically hide
|
|
||||||
* it after a delay.
|
|
||||||
*/
|
|
||||||
showProgressBar() {
|
|
||||||
|
|
||||||
this.progressBar.classList.add( 'visible' );
|
|
||||||
|
|
||||||
clearTimeout( this.hideProgressBarTimeout );
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().scrollProgress === 'auto' && !this.draggingProgressBar ) {
|
|
||||||
|
|
||||||
this.hideProgressBarTimeout = setTimeout( () => {
|
|
||||||
if( this.progressBar ) {
|
|
||||||
this.progressBar.classList.remove( 'visible' );
|
|
||||||
}
|
|
||||||
}, HIDE_SCROLLBAR_TIMEOUT );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll to the previous page.
|
|
||||||
*/
|
|
||||||
prev() {
|
|
||||||
|
|
||||||
this.viewportElement.scrollTop -= this.scrollTriggerHeight;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll to the next page.
|
|
||||||
*/
|
|
||||||
next() {
|
|
||||||
|
|
||||||
this.viewportElement.scrollTop += this.scrollTriggerHeight;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scrolls the given slide element into view.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slideElement
|
|
||||||
*/
|
|
||||||
scrollToSlide( slideElement ) {
|
|
||||||
|
|
||||||
// If the scroll view isn't active yet, queue this action
|
|
||||||
if( !this.active ) {
|
|
||||||
this.activatedCallbacks.push( () => this.scrollToSlide( slideElement ) );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Find the trigger for this slide
|
|
||||||
const trigger = this.getScrollTriggerBySlide( slideElement );
|
|
||||||
|
|
||||||
if( trigger ) {
|
|
||||||
// Use the trigger's range to calculate the scroll position
|
|
||||||
this.viewportElement.scrollTop = trigger.range[0] * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persists the current scroll position to session storage
|
|
||||||
* so that it can be restored.
|
|
||||||
*/
|
|
||||||
storeScrollPosition() {
|
|
||||||
|
|
||||||
clearTimeout( this.storeScrollPositionTimeout );
|
|
||||||
|
|
||||||
this.storeScrollPositionTimeout = setTimeout( () => {
|
|
||||||
sessionStorage.setItem( 'reveal-scroll-top', this.viewportElement.scrollTop );
|
|
||||||
sessionStorage.setItem( 'reveal-scroll-origin', location.origin + location.pathname );
|
|
||||||
|
|
||||||
this.storeScrollPositionTimeout = null;
|
|
||||||
}, 50 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the scroll position when a deck is reloader.
|
|
||||||
*/
|
|
||||||
restoreScrollPosition() {
|
|
||||||
|
|
||||||
const scrollPosition = sessionStorage.getItem( 'reveal-scroll-top' );
|
|
||||||
const scrollOrigin = sessionStorage.getItem( 'reveal-scroll-origin' );
|
|
||||||
|
|
||||||
if( scrollPosition && scrollOrigin === location.origin + location.pathname ) {
|
|
||||||
this.viewportElement.scrollTop = parseInt( scrollPosition, 10 );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activates the given page and starts its embedded content
|
|
||||||
* if there is any.
|
|
||||||
*
|
|
||||||
* @param {object} page
|
|
||||||
*/
|
|
||||||
activatePage( page ) {
|
|
||||||
|
|
||||||
if( !page.active ) {
|
|
||||||
|
|
||||||
page.active = true;
|
|
||||||
|
|
||||||
const { slideElement, backgroundElement, contentElement, indexh, indexv } = page;
|
|
||||||
|
|
||||||
contentElement.style.display = 'block';
|
|
||||||
|
|
||||||
slideElement.classList.add( 'present' );
|
|
||||||
|
|
||||||
if( backgroundElement ) {
|
|
||||||
backgroundElement.classList.add( 'present' );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Reveal.setCurrentScrollPage( slideElement, indexh, indexv );
|
|
||||||
this.Reveal.backgrounds.bubbleSlideContrastClassToElement( slideElement, this.viewportElement );
|
|
||||||
|
|
||||||
// If this page is part of an auto-animation there will be one
|
|
||||||
// content element per auto-animated page. We need to show the
|
|
||||||
// current page and hide all others.
|
|
||||||
Array.from( contentElement.parentNode.querySelectorAll( '.scroll-page-content' ) ).forEach( sibling => {
|
|
||||||
if( sibling !== contentElement ) {
|
|
||||||
sibling.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deactivates the page after it has been visible.
|
|
||||||
*
|
|
||||||
* @param {object} page
|
|
||||||
*/
|
|
||||||
deactivatePage( page ) {
|
|
||||||
|
|
||||||
if( page.active ) {
|
|
||||||
|
|
||||||
page.active = false;
|
|
||||||
if( page.slideElement ) page.slideElement.classList.remove( 'present' );
|
|
||||||
if( page.backgroundElement ) page.backgroundElement.classList.remove( 'present' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
activateTrigger( trigger ) {
|
|
||||||
|
|
||||||
if( !trigger.active ) {
|
|
||||||
trigger.active = true;
|
|
||||||
trigger.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivateTrigger( trigger ) {
|
|
||||||
|
|
||||||
if( trigger.active ) {
|
|
||||||
trigger.active = false;
|
|
||||||
|
|
||||||
if( trigger.deactivate ) {
|
|
||||||
trigger.deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a slide by its original h/v index (i.e. the indices the
|
|
||||||
* slide had before being linearized).
|
|
||||||
*
|
|
||||||
* @param {number} h
|
|
||||||
* @param {number} v
|
|
||||||
* @returns {HTMLElement}
|
|
||||||
*/
|
|
||||||
getSlideByIndices( h, v ) {
|
|
||||||
|
|
||||||
const page = this.getAllPages().find( page => {
|
|
||||||
return page.indexh === h && page.indexv === v;
|
|
||||||
} );
|
|
||||||
|
|
||||||
return page ? page.slideElement : null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a list of all scroll triggers for the given slide
|
|
||||||
* DOM element.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
getScrollTriggerBySlide( slide ) {
|
|
||||||
|
|
||||||
return this.slideTriggers.find( trigger => trigger.page.slideElement === slide );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of all pages in the scroll view. This includes
|
|
||||||
* both top-level slides and auto-animate steps.
|
|
||||||
*
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
getAllPages() {
|
|
||||||
|
|
||||||
return this.pages.flatMap( page => [page, ...(page.autoAnimatePages || [])] );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onScroll() {
|
|
||||||
|
|
||||||
this.syncScrollPosition();
|
|
||||||
this.storeScrollPosition();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
get viewportElement() {
|
|
||||||
|
|
||||||
return this.Reveal.getViewportElement();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,486 +0,0 @@
|
|||||||
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util.js'
|
|
||||||
import { isMobile } from '../utils/device.js'
|
|
||||||
|
|
||||||
import fitty from 'fitty';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles loading, unloading and playback of slide
|
|
||||||
* content such as images, videos and iframes.
|
|
||||||
*/
|
|
||||||
export default class SlideContent {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should the given element be preloaded?
|
|
||||||
* Decides based on local element attributes and global config.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
shouldPreload( element ) {
|
|
||||||
|
|
||||||
if( this.Reveal.isScrollView() ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer an explicit global preload setting
|
|
||||||
let preload = this.Reveal.getConfig().preloadIframes;
|
|
||||||
|
|
||||||
// If no global setting is available, fall back on the element's
|
|
||||||
// own preload setting
|
|
||||||
if( typeof preload !== 'boolean' ) {
|
|
||||||
preload = element.hasAttribute( 'data-preload' );
|
|
||||||
}
|
|
||||||
|
|
||||||
return preload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the given slide is within the configured view
|
|
||||||
* distance. Shows the slide element and loads any content
|
|
||||||
* that is set to load lazily (data-src).
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide Slide to show
|
|
||||||
*/
|
|
||||||
load( slide, options = {} ) {
|
|
||||||
|
|
||||||
// Show the slide element
|
|
||||||
slide.style.display = this.Reveal.getConfig().display;
|
|
||||||
|
|
||||||
// Media elements with data-src attributes
|
|
||||||
queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {
|
|
||||||
if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
|
|
||||||
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
|
|
||||||
element.setAttribute( 'data-lazy-loaded', '' );
|
|
||||||
element.removeAttribute( 'data-src' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Media elements with <source> children
|
|
||||||
queryAll( slide, 'video, audio' ).forEach( media => {
|
|
||||||
let sources = 0;
|
|
||||||
|
|
||||||
queryAll( media, 'source[data-src]' ).forEach( source => {
|
|
||||||
source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
|
|
||||||
source.removeAttribute( 'data-src' );
|
|
||||||
source.setAttribute( 'data-lazy-loaded', '' );
|
|
||||||
sources += 1;
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Enable inline video playback in mobile Safari
|
|
||||||
if( isMobile && media.tagName === 'VIDEO' ) {
|
|
||||||
media.setAttribute( 'playsinline', '' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we rewrote sources for this video/audio element, we need
|
|
||||||
// to manually tell it to load from its new origin
|
|
||||||
if( sources > 0 ) {
|
|
||||||
media.load();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
|
||||||
// Show the corresponding background element
|
|
||||||
let background = slide.slideBackgroundElement;
|
|
||||||
if( background ) {
|
|
||||||
background.style.display = 'block';
|
|
||||||
|
|
||||||
let backgroundContent = slide.slideBackgroundContentElement;
|
|
||||||
let backgroundIframe = slide.getAttribute( 'data-background-iframe' );
|
|
||||||
|
|
||||||
// If the background contains media, load it
|
|
||||||
if( background.hasAttribute( 'data-loaded' ) === false ) {
|
|
||||||
background.setAttribute( 'data-loaded', 'true' );
|
|
||||||
|
|
||||||
let backgroundImage = slide.getAttribute( 'data-background-image' ),
|
|
||||||
backgroundVideo = slide.getAttribute( 'data-background-video' ),
|
|
||||||
backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
|
|
||||||
backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );
|
|
||||||
|
|
||||||
// Images
|
|
||||||
if( backgroundImage ) {
|
|
||||||
// base64
|
|
||||||
if( /^data:/.test( backgroundImage.trim() ) ) {
|
|
||||||
backgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`;
|
|
||||||
}
|
|
||||||
// URL(s)
|
|
||||||
else {
|
|
||||||
backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {
|
|
||||||
// Decode URL(s) that are already encoded first
|
|
||||||
let decoded = decodeURI(background.trim());
|
|
||||||
return `url(${encodeRFC3986URI(decoded)})`;
|
|
||||||
}).join( ',' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Videos
|
|
||||||
else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
|
|
||||||
let video = document.createElement( 'video' );
|
|
||||||
|
|
||||||
if( backgroundVideoLoop ) {
|
|
||||||
video.setAttribute( 'loop', '' );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( backgroundVideoMuted ) {
|
|
||||||
video.muted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable inline playback in mobile Safari
|
|
||||||
//
|
|
||||||
// Mute is required for video to play when using
|
|
||||||
// swipe gestures to navigate since they don't
|
|
||||||
// count as direct user actions :'(
|
|
||||||
if( isMobile ) {
|
|
||||||
video.muted = true;
|
|
||||||
video.setAttribute( 'playsinline', '' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support comma separated lists of video sources
|
|
||||||
backgroundVideo.split( ',' ).forEach( source => {
|
|
||||||
const sourceElement = document.createElement( 'source' );
|
|
||||||
sourceElement.setAttribute( 'src', source );
|
|
||||||
|
|
||||||
let type = getMimeTypeFromFile( source );
|
|
||||||
if( type ) {
|
|
||||||
sourceElement.setAttribute( 'type', type );
|
|
||||||
}
|
|
||||||
|
|
||||||
video.appendChild( sourceElement );
|
|
||||||
} );
|
|
||||||
|
|
||||||
backgroundContent.appendChild( video );
|
|
||||||
}
|
|
||||||
// Iframes
|
|
||||||
else if( backgroundIframe && options.excludeIframes !== true ) {
|
|
||||||
let iframe = document.createElement( 'iframe' );
|
|
||||||
iframe.setAttribute( 'allowfullscreen', '' );
|
|
||||||
iframe.setAttribute( 'mozallowfullscreen', '' );
|
|
||||||
iframe.setAttribute( 'webkitallowfullscreen', '' );
|
|
||||||
iframe.setAttribute( 'allow', 'autoplay' );
|
|
||||||
|
|
||||||
iframe.setAttribute( 'data-src', backgroundIframe );
|
|
||||||
|
|
||||||
iframe.style.width = '100%';
|
|
||||||
iframe.style.height = '100%';
|
|
||||||
iframe.style.maxHeight = '100%';
|
|
||||||
iframe.style.maxWidth = '100%';
|
|
||||||
|
|
||||||
backgroundContent.appendChild( iframe );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start loading preloadable iframes
|
|
||||||
let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );
|
|
||||||
if( backgroundIframeElement ) {
|
|
||||||
|
|
||||||
// Check if this iframe is eligible to be preloaded
|
|
||||||
if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
|
|
||||||
if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
|
|
||||||
backgroundIframeElement.setAttribute( 'src', backgroundIframe );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layout( slide );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies JS-dependent layout helpers for the scope.
|
|
||||||
*/
|
|
||||||
layout( scopeElement ) {
|
|
||||||
|
|
||||||
// Autosize text with the r-fit-text class based on the
|
|
||||||
// size of its container. This needs to happen after the
|
|
||||||
// slide is visible in order to measure the text.
|
|
||||||
Array.from( scopeElement.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
|
|
||||||
fitty( element, {
|
|
||||||
minSize: 24,
|
|
||||||
maxSize: this.Reveal.getConfig().height * 0.8,
|
|
||||||
observeMutations: false,
|
|
||||||
observeWindow: false
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unloads and hides the given slide. This is called when the
|
|
||||||
* slide is moved outside of the configured view distance.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} slide
|
|
||||||
*/
|
|
||||||
unload( slide ) {
|
|
||||||
|
|
||||||
// Hide the slide element
|
|
||||||
slide.style.display = 'none';
|
|
||||||
|
|
||||||
// Hide the corresponding background element
|
|
||||||
let background = this.Reveal.getSlideBackground( slide );
|
|
||||||
if( background ) {
|
|
||||||
background.style.display = 'none';
|
|
||||||
|
|
||||||
// Unload any background iframes
|
|
||||||
queryAll( background, 'iframe[src]' ).forEach( element => {
|
|
||||||
element.removeAttribute( 'src' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset lazy-loaded media elements with src attributes
|
|
||||||
queryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => {
|
|
||||||
element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
|
|
||||||
element.removeAttribute( 'src' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Reset lazy-loaded media elements with <source> children
|
|
||||||
queryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => {
|
|
||||||
source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
|
|
||||||
source.removeAttribute( 'src' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforces origin-specific format rules for embedded media.
|
|
||||||
*/
|
|
||||||
formatEmbeddedContent() {
|
|
||||||
|
|
||||||
let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {
|
|
||||||
queryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ).forEach( el => {
|
|
||||||
let src = el.getAttribute( sourceAttribute );
|
|
||||||
if( src && src.indexOf( param ) === -1 ) {
|
|
||||||
el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// YouTube frames must include "?enablejsapi=1"
|
|
||||||
_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
|
|
||||||
_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
|
|
||||||
|
|
||||||
// Vimeo frames must include "?api=1"
|
|
||||||
_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
|
|
||||||
_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start playback of any embedded content inside of
|
|
||||||
* the given element.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
startEmbeddedContent( element ) {
|
|
||||||
|
|
||||||
if( element && !this.Reveal.isSpeakerNotes() ) {
|
|
||||||
|
|
||||||
// Restart GIFs
|
|
||||||
queryAll( element, 'img[src$=".gif"]' ).forEach( el => {
|
|
||||||
// Setting the same unchanged source like this was confirmed
|
|
||||||
// to work in Chrome, FF & Safari
|
|
||||||
el.setAttribute( 'src', el.getAttribute( 'src' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// HTML5 media elements
|
|
||||||
queryAll( element, 'video, audio' ).forEach( el => {
|
|
||||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer an explicit global autoplay setting
|
|
||||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
|
||||||
|
|
||||||
// If no global setting is available, fall back on the element's
|
|
||||||
// own autoplay setting
|
|
||||||
if( typeof autoplay !== 'boolean' ) {
|
|
||||||
autoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( autoplay && typeof el.play === 'function' ) {
|
|
||||||
|
|
||||||
// If the media is ready, start playback
|
|
||||||
if( el.readyState > 1 ) {
|
|
||||||
this.startEmbeddedMedia( { target: el } );
|
|
||||||
}
|
|
||||||
// Mobile devices never fire a loaded event so instead
|
|
||||||
// of waiting, we initiate playback
|
|
||||||
else if( isMobile ) {
|
|
||||||
let promise = el.play();
|
|
||||||
|
|
||||||
// If autoplay does not work, ensure that the controls are visible so
|
|
||||||
// that the viewer can start the media on their own
|
|
||||||
if( promise && typeof promise.catch === 'function' && el.controls === false ) {
|
|
||||||
promise.catch( () => {
|
|
||||||
el.controls = true;
|
|
||||||
|
|
||||||
// Once the video does start playing, hide the controls again
|
|
||||||
el.addEventListener( 'play', () => {
|
|
||||||
el.controls = false;
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the media isn't loaded, wait before playing
|
|
||||||
else {
|
|
||||||
el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes
|
|
||||||
el.addEventListener( 'loadeddata', this.startEmbeddedMedia );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Normal iframes
|
|
||||||
queryAll( element, 'iframe[src]' ).forEach( el => {
|
|
||||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startEmbeddedIframe( { target: el } );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Lazy loading iframes
|
|
||||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
|
||||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
|
|
||||||
el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
|
|
||||||
el.addEventListener( 'load', this.startEmbeddedIframe );
|
|
||||||
el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playing an embedded video/audio element after
|
|
||||||
* it has finished loading.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
startEmbeddedMedia( event ) {
|
|
||||||
|
|
||||||
let isAttachedToDOM = !!closest( event.target, 'html' ),
|
|
||||||
isVisible = !!closest( event.target, '.present' );
|
|
||||||
|
|
||||||
if( isAttachedToDOM && isVisible ) {
|
|
||||||
event.target.currentTime = 0;
|
|
||||||
event.target.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Starts" the content of an embedded iframe using the
|
|
||||||
* postMessage API.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
startEmbeddedIframe( event ) {
|
|
||||||
|
|
||||||
let iframe = event.target;
|
|
||||||
|
|
||||||
if( iframe && iframe.contentWindow ) {
|
|
||||||
|
|
||||||
let isAttachedToDOM = !!closest( event.target, 'html' ),
|
|
||||||
isVisible = !!closest( event.target, '.present' );
|
|
||||||
|
|
||||||
if( isAttachedToDOM && isVisible ) {
|
|
||||||
|
|
||||||
// Prefer an explicit global autoplay setting
|
|
||||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
|
||||||
|
|
||||||
// If no global setting is available, fall back on the element's
|
|
||||||
// own autoplay setting
|
|
||||||
if( typeof autoplay !== 'boolean' ) {
|
|
||||||
autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// YouTube postMessage API
|
|
||||||
if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
|
|
||||||
iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
|
|
||||||
}
|
|
||||||
// Vimeo postMessage API
|
|
||||||
else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
|
|
||||||
iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
|
|
||||||
}
|
|
||||||
// Generic postMessage API
|
|
||||||
else {
|
|
||||||
iframe.contentWindow.postMessage( 'slide:start', '*' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop playback of any embedded content inside of
|
|
||||||
* the targeted slide.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
stopEmbeddedContent( element, options = {} ) {
|
|
||||||
|
|
||||||
options = extend( {
|
|
||||||
// Defaults
|
|
||||||
unloadIframes: true
|
|
||||||
}, options );
|
|
||||||
|
|
||||||
if( element && element.parentNode ) {
|
|
||||||
// HTML5 media elements
|
|
||||||
queryAll( element, 'video, audio' ).forEach( el => {
|
|
||||||
if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
|
|
||||||
el.setAttribute('data-paused-by-reveal', '');
|
|
||||||
el.pause();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Generic postMessage API for non-lazy loaded iframes
|
|
||||||
queryAll( element, 'iframe' ).forEach( el => {
|
|
||||||
if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
|
|
||||||
el.removeEventListener( 'load', this.startEmbeddedIframe );
|
|
||||||
});
|
|
||||||
|
|
||||||
// YouTube postMessage API
|
|
||||||
queryAll( element, 'iframe[src*="youtube.com/embed/"]' ).forEach( el => {
|
|
||||||
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
|
|
||||||
el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Vimeo postMessage API
|
|
||||||
queryAll( element, 'iframe[src*="player.vimeo.com/"]' ).forEach( el => {
|
|
||||||
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
|
|
||||||
el.contentWindow.postMessage( '{"method":"pause"}', '*' );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if( options.unloadIframes === true ) {
|
|
||||||
// Unload lazy-loaded iframes
|
|
||||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
|
||||||
// Only removing the src doesn't actually unload the frame
|
|
||||||
// in all browsers (Firefox) so we set it to blank first
|
|
||||||
el.setAttribute( 'src', 'about:blank' );
|
|
||||||
el.removeAttribute( 'src' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
import {
|
|
||||||
SLIDE_NUMBER_FORMAT_CURRENT,
|
|
||||||
SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL,
|
|
||||||
SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL,
|
|
||||||
SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL
|
|
||||||
} from "../utils/constants";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the display of reveal.js' optional slide number.
|
|
||||||
*/
|
|
||||||
export default class SlideNumber {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
|
||||||
this.element.className = 'slide-number';
|
|
||||||
this.Reveal.getRevealElement().appendChild( this.element );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reveal.js config is updated.
|
|
||||||
*/
|
|
||||||
configure( config, oldConfig ) {
|
|
||||||
|
|
||||||
let slideNumberDisplay = 'none';
|
|
||||||
if( config.slideNumber && !this.Reveal.isPrintView() ) {
|
|
||||||
if( config.showSlideNumber === 'all' ) {
|
|
||||||
slideNumberDisplay = 'block';
|
|
||||||
}
|
|
||||||
else if( config.showSlideNumber === 'speaker' && this.Reveal.isSpeakerNotes() ) {
|
|
||||||
slideNumberDisplay = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.element.style.display = slideNumberDisplay;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the slide number to match the current slide.
|
|
||||||
*/
|
|
||||||
update() {
|
|
||||||
|
|
||||||
// Update slide number if enabled
|
|
||||||
if( this.Reveal.getConfig().slideNumber && this.element ) {
|
|
||||||
this.element.innerHTML = this.getSlideNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTML string corresponding to the current slide
|
|
||||||
* number, including formatting.
|
|
||||||
*/
|
|
||||||
getSlideNumber( slide = this.Reveal.getCurrentSlide() ) {
|
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
let value;
|
|
||||||
let format = SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL;
|
|
||||||
|
|
||||||
if ( typeof config.slideNumber === 'function' ) {
|
|
||||||
value = config.slideNumber( slide );
|
|
||||||
} else {
|
|
||||||
// Check if a custom number format is available
|
|
||||||
if( typeof config.slideNumber === 'string' ) {
|
|
||||||
format = config.slideNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are ONLY vertical slides in this deck, always use
|
|
||||||
// a flattened slide number
|
|
||||||
if( !/c/.test( format ) && this.Reveal.getHorizontalSlides().length === 1 ) {
|
|
||||||
format = SLIDE_NUMBER_FORMAT_CURRENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset the current slide number by 1 to make it 1-indexed
|
|
||||||
let horizontalOffset = slide && slide.dataset.visibility === 'uncounted' ? 0 : 1;
|
|
||||||
|
|
||||||
value = [];
|
|
||||||
switch( format ) {
|
|
||||||
case SLIDE_NUMBER_FORMAT_CURRENT:
|
|
||||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset );
|
|
||||||
break;
|
|
||||||
case SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL:
|
|
||||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset, '/', this.Reveal.getTotalSlides() );
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
let indices = this.Reveal.getIndices( slide );
|
|
||||||
value.push( indices.h + horizontalOffset );
|
|
||||||
let sep = format === SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL ? '/' : '.';
|
|
||||||
if( this.Reveal.isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = '#' + this.Reveal.location.getHash( slide );
|
|
||||||
return this.formatNumber( value[0], value[1], value[2], url );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies HTML formatting to a slide number before it's
|
|
||||||
* written to the DOM.
|
|
||||||
*
|
|
||||||
* @param {number} a Current slide
|
|
||||||
* @param {string} delimiter Character to separate slide numbers
|
|
||||||
* @param {(number|*)} b Total slides
|
|
||||||
* @param {HTMLElement} [url='#'+locationHash()] The url to link to
|
|
||||||
* @return {string} HTML string fragment
|
|
||||||
*/
|
|
||||||
formatNumber( a, delimiter, b, url = '#' + this.Reveal.location.getHash() ) {
|
|
||||||
|
|
||||||
if( typeof b === 'number' && !isNaN( b ) ) {
|
|
||||||
return `<a href="${url}">
|
|
||||||
<span class="slide-number-a">${a}</span>
|
|
||||||
<span class="slide-number-delimiter">${delimiter}</span>
|
|
||||||
<span class="slide-number-b">${b}</span>
|
|
||||||
</a>`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return `<a href="${url}">
|
|
||||||
<span class="slide-number-a">${a}</span>
|
|
||||||
</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
|
||||||
this.element.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,263 +0,0 @@
|
|||||||
import { isAndroid } from '../utils/device.js'
|
|
||||||
import { matches } from '../utils/util.js'
|
|
||||||
|
|
||||||
const SWIPE_THRESHOLD = 40;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls all touch interactions and navigations for
|
|
||||||
* a presentation.
|
|
||||||
*/
|
|
||||||
export default class Touch {
|
|
||||||
|
|
||||||
constructor( Reveal ) {
|
|
||||||
|
|
||||||
this.Reveal = Reveal;
|
|
||||||
|
|
||||||
// Holds information about the currently ongoing touch interaction
|
|
||||||
this.touchStartX = 0;
|
|
||||||
this.touchStartY = 0;
|
|
||||||
this.touchStartCount = 0;
|
|
||||||
this.touchCaptured = false;
|
|
||||||
|
|
||||||
this.onPointerDown = this.onPointerDown.bind( this );
|
|
||||||
this.onPointerMove = this.onPointerMove.bind( this );
|
|
||||||
this.onPointerUp = this.onPointerUp.bind( this );
|
|
||||||
this.onTouchStart = this.onTouchStart.bind( this );
|
|
||||||
this.onTouchMove = this.onTouchMove.bind( this );
|
|
||||||
this.onTouchEnd = this.onTouchEnd.bind( this );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
bind() {
|
|
||||||
|
|
||||||
let revealElement = this.Reveal.getRevealElement();
|
|
||||||
|
|
||||||
if( 'onpointerdown' in window ) {
|
|
||||||
// Use W3C pointer events
|
|
||||||
revealElement.addEventListener( 'pointerdown', this.onPointerDown, false );
|
|
||||||
revealElement.addEventListener( 'pointermove', this.onPointerMove, false );
|
|
||||||
revealElement.addEventListener( 'pointerup', this.onPointerUp, false );
|
|
||||||
}
|
|
||||||
else if( window.navigator.msPointerEnabled ) {
|
|
||||||
// IE 10 uses prefixed version of pointer events
|
|
||||||
revealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false );
|
|
||||||
revealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false );
|
|
||||||
revealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Fall back to touch events
|
|
||||||
revealElement.addEventListener( 'touchstart', this.onTouchStart, false );
|
|
||||||
revealElement.addEventListener( 'touchmove', this.onTouchMove, false );
|
|
||||||
revealElement.addEventListener( 'touchend', this.onTouchEnd, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
unbind() {
|
|
||||||
|
|
||||||
let revealElement = this.Reveal.getRevealElement();
|
|
||||||
|
|
||||||
revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );
|
|
||||||
revealElement.removeEventListener( 'pointermove', this.onPointerMove, false );
|
|
||||||
revealElement.removeEventListener( 'pointerup', this.onPointerUp, false );
|
|
||||||
|
|
||||||
revealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false );
|
|
||||||
revealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false );
|
|
||||||
revealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false );
|
|
||||||
|
|
||||||
revealElement.removeEventListener( 'touchstart', this.onTouchStart, false );
|
|
||||||
revealElement.removeEventListener( 'touchmove', this.onTouchMove, false );
|
|
||||||
revealElement.removeEventListener( 'touchend', this.onTouchEnd, false );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the target element prevents the triggering of
|
|
||||||
* swipe navigation.
|
|
||||||
*/
|
|
||||||
isSwipePrevented( target ) {
|
|
||||||
|
|
||||||
// Prevent accidental swipes when scrubbing timelines
|
|
||||||
if( matches( target, 'video, audio' ) ) return true;
|
|
||||||
|
|
||||||
while( target && typeof target.hasAttribute === 'function' ) {
|
|
||||||
if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
|
|
||||||
target = target.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the 'touchstart' event, enables support for
|
|
||||||
* swipe and pinch gestures.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onTouchStart( event ) {
|
|
||||||
|
|
||||||
if( this.isSwipePrevented( event.target ) ) return true;
|
|
||||||
|
|
||||||
this.touchStartX = event.touches[0].clientX;
|
|
||||||
this.touchStartY = event.touches[0].clientY;
|
|
||||||
this.touchStartCount = event.touches.length;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the 'touchmove' event.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onTouchMove( event ) {
|
|
||||||
|
|
||||||
if( this.isSwipePrevented( event.target ) ) return true;
|
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
|
|
||||||
// Each touch should only trigger one action
|
|
||||||
if( !this.touchCaptured ) {
|
|
||||||
this.Reveal.onUserInput( event );
|
|
||||||
|
|
||||||
let currentX = event.touches[0].clientX;
|
|
||||||
let currentY = event.touches[0].clientY;
|
|
||||||
|
|
||||||
// There was only one touch point, look for a swipe
|
|
||||||
if( event.touches.length === 1 && this.touchStartCount !== 2 ) {
|
|
||||||
|
|
||||||
let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });
|
|
||||||
|
|
||||||
let deltaX = currentX - this.touchStartX,
|
|
||||||
deltaY = currentY - this.touchStartY;
|
|
||||||
|
|
||||||
if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
|
|
||||||
this.touchCaptured = true;
|
|
||||||
if( config.navigationMode === 'linear' ) {
|
|
||||||
if( config.rtl ) {
|
|
||||||
this.Reveal.next();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.prev();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.left();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
|
|
||||||
this.touchCaptured = true;
|
|
||||||
if( config.navigationMode === 'linear' ) {
|
|
||||||
if( config.rtl ) {
|
|
||||||
this.Reveal.prev();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.right();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {
|
|
||||||
this.touchCaptured = true;
|
|
||||||
if( config.navigationMode === 'linear' ) {
|
|
||||||
this.Reveal.prev();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {
|
|
||||||
this.touchCaptured = true;
|
|
||||||
if( config.navigationMode === 'linear' ) {
|
|
||||||
this.Reveal.next();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.Reveal.down();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're embedded, only block touch events if they have
|
|
||||||
// triggered an action
|
|
||||||
if( config.embedded ) {
|
|
||||||
if( this.touchCaptured || this.Reveal.isVerticalSlide() ) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Not embedded? Block them all to avoid needless tossing
|
|
||||||
// around of the viewport in iOS
|
|
||||||
else {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// There's a bug with swiping on some Android devices unless
|
|
||||||
// the default action is always prevented
|
|
||||||
else if( isAndroid ) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the 'touchend' event.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onTouchEnd( event ) {
|
|
||||||
|
|
||||||
this.touchCaptured = false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert pointer down to touch start.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onPointerDown( event ) {
|
|
||||||
|
|
||||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
|
||||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
|
||||||
this.onTouchStart( event );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert pointer move to touch move.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onPointerMove( event ) {
|
|
||||||
|
|
||||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
|
||||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
|
||||||
this.onTouchMove( event );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert pointer up to touch end.
|
|
||||||
*
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
onPointerUp( event ) {
|
|
||||||
|
|
||||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
|
||||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
|
||||||
this.onTouchEnd( event );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
58
js/index.js
@ -1,58 +0,0 @@
|
|||||||
import Deck, { VERSION } from './reveal.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose the Reveal class to the window. To create a
|
|
||||||
* new instance:
|
|
||||||
* let deck = new Reveal( document.querySelector( '.reveal' ), {
|
|
||||||
* controls: false
|
|
||||||
* } );
|
|
||||||
* deck.initialize().then(() => {
|
|
||||||
* // reveal.js is ready
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
let Reveal = Deck;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The below is a thin shell that mimics the pre 4.0
|
|
||||||
* reveal.js API and ensures backwards compatibility.
|
|
||||||
* This API only allows for one Reveal instance per
|
|
||||||
* page, whereas the new API above lets you run many
|
|
||||||
* presentations on the same page.
|
|
||||||
*
|
|
||||||
* Reveal.initialize( { controls: false } ).then(() => {
|
|
||||||
* // reveal.js is ready
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
let enqueuedAPICalls = [];
|
|
||||||
|
|
||||||
Reveal.initialize = options => {
|
|
||||||
|
|
||||||
// Create our singleton reveal.js instance
|
|
||||||
Object.assign( Reveal, new Deck( document.querySelector( '.reveal' ), options ) );
|
|
||||||
|
|
||||||
// Invoke any enqueued API calls
|
|
||||||
enqueuedAPICalls.map( method => method( Reveal ) );
|
|
||||||
|
|
||||||
return Reveal.initialize();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pre 4.0 API let you add event listener before
|
|
||||||
* initializing. We maintain the same behavior by
|
|
||||||
* queuing up premature API calls and invoking all
|
|
||||||
* of them when Reveal.initialize is called.
|
|
||||||
*/
|
|
||||||
[ 'configure', 'on', 'off', 'addEventListener', 'removeEventListener', 'registerPlugin' ].forEach( method => {
|
|
||||||
Reveal[method] = ( ...args ) => {
|
|
||||||
enqueuedAPICalls.push( deck => deck[method].call( null, ...args ) );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
Reveal.isReady = () => false;
|
|
||||||
|
|
||||||
Reveal.VERSION = VERSION;
|
|
||||||
|
|
||||||
export default Reveal;
|
|
3059
js/reveal.js
@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Converts various color input formats to an {r:0,g:0,b:0} object.
|
|
||||||
*
|
|
||||||
* @param {string} color The string representation of a color
|
|
||||||
* @example
|
|
||||||
* colorToRgb('#000');
|
|
||||||
* @example
|
|
||||||
* colorToRgb('#000000');
|
|
||||||
* @example
|
|
||||||
* colorToRgb('rgb(0,0,0)');
|
|
||||||
* @example
|
|
||||||
* colorToRgb('rgba(0,0,0)');
|
|
||||||
*
|
|
||||||
* @return {{r: number, g: number, b: number, [a]: number}|null}
|
|
||||||
*/
|
|
||||||
export const colorToRgb = ( color ) => {
|
|
||||||
|
|
||||||
let hex3 = color.match( /^#([0-9a-f]{3})$/i );
|
|
||||||
if( hex3 && hex3[1] ) {
|
|
||||||
hex3 = hex3[1];
|
|
||||||
return {
|
|
||||||
r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
|
|
||||||
g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
|
|
||||||
b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let hex6 = color.match( /^#([0-9a-f]{6})$/i );
|
|
||||||
if( hex6 && hex6[1] ) {
|
|
||||||
hex6 = hex6[1];
|
|
||||||
return {
|
|
||||||
r: parseInt( hex6.slice( 0, 2 ), 16 ),
|
|
||||||
g: parseInt( hex6.slice( 2, 4 ), 16 ),
|
|
||||||
b: parseInt( hex6.slice( 4, 6 ), 16 )
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
|
|
||||||
if( rgb ) {
|
|
||||||
return {
|
|
||||||
r: parseInt( rgb[1], 10 ),
|
|
||||||
g: parseInt( rgb[2], 10 ),
|
|
||||||
b: parseInt( rgb[3], 10 )
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
|
|
||||||
if( rgba ) {
|
|
||||||
return {
|
|
||||||
r: parseInt( rgba[1], 10 ),
|
|
||||||
g: parseInt( rgba[2], 10 ),
|
|
||||||
b: parseInt( rgba[3], 10 ),
|
|
||||||
a: parseFloat( rgba[4] )
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates brightness on a scale of 0-255.
|
|
||||||
*
|
|
||||||
* @param {string} color See colorToRgb for supported formats.
|
|
||||||
* @see {@link colorToRgb}
|
|
||||||
*/
|
|
||||||
export const colorBrightness = ( color ) => {
|
|
||||||
|
|
||||||
if( typeof color === 'string' ) color = colorToRgb( color );
|
|
||||||
|
|
||||||
if( color ) {
|
|
||||||
return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
export const SLIDES_SELECTOR = '.slides section';
|
|
||||||
export const HORIZONTAL_SLIDES_SELECTOR = '.slides>section';
|
|
||||||
export const VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section';
|
|
||||||
|
|
||||||
// Methods that may not be invoked via the postMessage API
|
|
||||||
export const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener|showPreview/;
|
|
||||||
|
|
||||||
// Regex for retrieving the fragment style from a class attribute
|
|
||||||
export const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;
|
|
||||||
|
|
||||||
// Slide number formats
|
|
||||||
export const SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL = 'h.v';
|
|
||||||
export const SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL = 'h/v';
|
|
||||||
export const SLIDE_NUMBER_FORMAT_CURRENT = 'c';
|
|
||||||
export const SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL = 'c/t';
|
|
@ -1,8 +0,0 @@
|
|||||||
const UA = navigator.userAgent;
|
|
||||||
|
|
||||||
export const isMobile = /(iphone|ipod|ipad|android)/gi.test( UA ) ||
|
|
||||||
( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); // iPadOS
|
|
||||||
|
|
||||||
export const isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
|
|
||||||
|
|
||||||
export const isAndroid = /android/gi.test( UA );
|
|
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Loads a JavaScript file from the given URL and executes it.
|
|
||||||
*
|
|
||||||
* @param {string} url Address of the .js file to load
|
|
||||||
* @param {function} callback Method to invoke when the script
|
|
||||||
* has loaded and executed
|
|
||||||
*/
|
|
||||||
export const loadScript = ( url, callback ) => {
|
|
||||||
|
|
||||||
const script = document.createElement( 'script' );
|
|
||||||
script.type = 'text/javascript';
|
|
||||||
script.async = false;
|
|
||||||
script.defer = false;
|
|
||||||
script.src = url;
|
|
||||||
|
|
||||||
if( typeof callback === 'function' ) {
|
|
||||||
|
|
||||||
// Success callback
|
|
||||||
script.onload = script.onreadystatechange = event => {
|
|
||||||
if( event.type === 'load' || /loaded|complete/.test( script.readyState ) ) {
|
|
||||||
|
|
||||||
// Kill event listeners
|
|
||||||
script.onload = script.onreadystatechange = script.onerror = null;
|
|
||||||
|
|
||||||
callback();
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Error callback
|
|
||||||
script.onerror = err => {
|
|
||||||
|
|
||||||
// Kill event listeners
|
|
||||||
script.onload = script.onreadystatechange = script.onerror = null;
|
|
||||||
|
|
||||||
callback( new Error( 'Failed loading script: ' + script.src + '\n' + err ) );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the script at the end of <head>
|
|
||||||
const head = document.querySelector( 'head' );
|
|
||||||
head.insertBefore( script, head.lastChild );
|
|
||||||
|
|
||||||
}
|
|
313
js/utils/util.js
@ -1,313 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extend object a with the properties of object b.
|
|
||||||
* If there's a conflict, object b takes precedence.
|
|
||||||
*
|
|
||||||
* @param {object} a
|
|
||||||
* @param {object} b
|
|
||||||
*/
|
|
||||||
export const extend = ( a, b ) => {
|
|
||||||
|
|
||||||
for( let i in b ) {
|
|
||||||
a[ i ] = b[ i ];
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* querySelectorAll but returns an Array.
|
|
||||||
*/
|
|
||||||
export const queryAll = ( el, selector ) => {
|
|
||||||
|
|
||||||
return Array.from( el.querySelectorAll( selector ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* classList.toggle() with cross browser support
|
|
||||||
*/
|
|
||||||
export const toggleClass = ( el, className, value ) => {
|
|
||||||
if( value ) {
|
|
||||||
el.classList.add( className );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
el.classList.remove( className );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility for deserializing a value.
|
|
||||||
*
|
|
||||||
* @param {*} value
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
export const deserialize = ( value ) => {
|
|
||||||
|
|
||||||
if( typeof value === 'string' ) {
|
|
||||||
if( value === 'null' ) return null;
|
|
||||||
else if( value === 'true' ) return true;
|
|
||||||
else if( value === 'false' ) return false;
|
|
||||||
else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the distance in pixels between point a
|
|
||||||
* and point b.
|
|
||||||
*
|
|
||||||
* @param {object} a point with x/y properties
|
|
||||||
* @param {object} b point with x/y properties
|
|
||||||
*
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
export const distanceBetween = ( a, b ) => {
|
|
||||||
|
|
||||||
let dx = a.x - b.x,
|
|
||||||
dy = a.y - b.y;
|
|
||||||
|
|
||||||
return Math.sqrt( dx*dx + dy*dy );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a CSS transform to the target element.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @param {string} transform
|
|
||||||
*/
|
|
||||||
export const transformElement = ( element, transform ) => {
|
|
||||||
|
|
||||||
element.style.transform = transform;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element.matches with IE support.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} target The element to match
|
|
||||||
* @param {String} selector The CSS selector to match
|
|
||||||
* the element against
|
|
||||||
*
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
export const matches = ( target, selector ) => {
|
|
||||||
|
|
||||||
let matchesMethod = target.matches || target.matchesSelector || target.msMatchesSelector;
|
|
||||||
|
|
||||||
return !!( matchesMethod && matchesMethod.call( target, selector ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the closest parent that matches the given
|
|
||||||
* selector.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} target The child element
|
|
||||||
* @param {String} selector The CSS selector to match
|
|
||||||
* the parents against
|
|
||||||
*
|
|
||||||
* @return {HTMLElement} The matched parent or null
|
|
||||||
* if no matching parent was found
|
|
||||||
*/
|
|
||||||
export const closest = ( target, selector ) => {
|
|
||||||
|
|
||||||
// Native Element.closest
|
|
||||||
if( typeof target.closest === 'function' ) {
|
|
||||||
return target.closest( selector );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Polyfill
|
|
||||||
while( target ) {
|
|
||||||
if( matches( target, selector ) ) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep searching
|
|
||||||
target = target.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handling the fullscreen functionality via the fullscreen API
|
|
||||||
*
|
|
||||||
* @see http://fullscreen.spec.whatwg.org/
|
|
||||||
* @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
|
|
||||||
*/
|
|
||||||
export const enterFullscreen = element => {
|
|
||||||
|
|
||||||
element = element || document.documentElement;
|
|
||||||
|
|
||||||
// Check which implementation is available
|
|
||||||
let requestMethod = element.requestFullscreen ||
|
|
||||||
element.webkitRequestFullscreen ||
|
|
||||||
element.webkitRequestFullScreen ||
|
|
||||||
element.mozRequestFullScreen ||
|
|
||||||
element.msRequestFullscreen;
|
|
||||||
|
|
||||||
if( requestMethod ) {
|
|
||||||
requestMethod.apply( element );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an HTML element and returns a reference to it.
|
|
||||||
* If the element already exists the existing instance will
|
|
||||||
* be returned.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} container
|
|
||||||
* @param {string} tagname
|
|
||||||
* @param {string} classname
|
|
||||||
* @param {string} innerHTML
|
|
||||||
*
|
|
||||||
* @return {HTMLElement}
|
|
||||||
*/
|
|
||||||
export const createSingletonNode = ( container, tagname, classname, innerHTML='' ) => {
|
|
||||||
|
|
||||||
// Find all nodes matching the description
|
|
||||||
let nodes = container.querySelectorAll( '.' + classname );
|
|
||||||
|
|
||||||
// Check all matches to find one which is a direct child of
|
|
||||||
// the specified container
|
|
||||||
for( let i = 0; i < nodes.length; i++ ) {
|
|
||||||
let testNode = nodes[i];
|
|
||||||
if( testNode.parentNode === container ) {
|
|
||||||
return testNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no node was found, create it now
|
|
||||||
let node = document.createElement( tagname );
|
|
||||||
node.className = classname;
|
|
||||||
node.innerHTML = innerHTML;
|
|
||||||
container.appendChild( node );
|
|
||||||
|
|
||||||
return node;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injects the given CSS styles into the DOM.
|
|
||||||
*
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
export const createStyleSheet = ( value ) => {
|
|
||||||
|
|
||||||
let tag = document.createElement( 'style' );
|
|
||||||
tag.type = 'text/css';
|
|
||||||
|
|
||||||
if( value && value.length > 0 ) {
|
|
||||||
if( tag.styleSheet ) {
|
|
||||||
tag.styleSheet.cssText = value;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
tag.appendChild( document.createTextNode( value ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.head.appendChild( tag );
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a key:value hash of all query params.
|
|
||||||
*/
|
|
||||||
export const getQueryHash = () => {
|
|
||||||
|
|
||||||
let query = {};
|
|
||||||
|
|
||||||
location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, a => {
|
|
||||||
query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Basic deserialization
|
|
||||||
for( let i in query ) {
|
|
||||||
let value = query[ i ];
|
|
||||||
|
|
||||||
query[ i ] = deserialize( unescape( value ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not accept new dependencies via query config to avoid
|
|
||||||
// the potential of malicious script injection
|
|
||||||
if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
|
|
||||||
|
|
||||||
return query;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the remaining height within the parent of the
|
|
||||||
* target element.
|
|
||||||
*
|
|
||||||
* remaining height = [ configured parent height ] - [ current parent height ]
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @param {number} [height]
|
|
||||||
*/
|
|
||||||
export const getRemainingHeight = ( element, height = 0 ) => {
|
|
||||||
|
|
||||||
if( element ) {
|
|
||||||
let newHeight, oldHeight = element.style.height;
|
|
||||||
|
|
||||||
// Change the .stretch element height to 0 in order find the height of all
|
|
||||||
// the other elements
|
|
||||||
element.style.height = '0px';
|
|
||||||
|
|
||||||
// In Overview mode, the parent (.slide) height is set of 700px.
|
|
||||||
// Restore it temporarily to its natural height.
|
|
||||||
element.parentNode.style.height = 'auto';
|
|
||||||
|
|
||||||
newHeight = height - element.parentNode.offsetHeight;
|
|
||||||
|
|
||||||
// Restore the old height, just in case
|
|
||||||
element.style.height = oldHeight + 'px';
|
|
||||||
|
|
||||||
// Clear the parent (.slide) height. .removeProperty works in IE9+
|
|
||||||
element.parentNode.style.removeProperty('height');
|
|
||||||
|
|
||||||
return newHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return height;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtensionToMimeMap = {
|
|
||||||
'mp4': 'video/mp4',
|
|
||||||
'm4a': 'video/mp4',
|
|
||||||
'ogv': 'video/ogg',
|
|
||||||
'mpeg': 'video/mpeg',
|
|
||||||
'webm': 'video/webm'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guess the MIME type for common file formats.
|
|
||||||
*/
|
|
||||||
export const getMimeTypeFromFile = ( filename='' ) => {
|
|
||||||
return fileExtensionToMimeMap[filename.split('.').pop()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a string for RFC3986-compliant URL format.
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#encoding_for_rfc3986
|
|
||||||
*
|
|
||||||
* @param {string} url
|
|
||||||
*/
|
|
||||||
export const encodeRFC3986URI = ( url='' ) => {
|
|
||||||
return encodeURI(url)
|
|
||||||
.replace(/%5B/g, "[")
|
|
||||||
.replace(/%5D/g, "]")
|
|
||||||
.replace(
|
|
||||||
/[!'()*]/g,
|
|
||||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
||||||
);
|
|
||||||
}
|
|
10781
package-lock.json
generated
104
package.json
@ -1,104 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "reveal.js",
|
|
||||||
"version": "5.0.4",
|
|
||||||
"description": "The HTML Presentation Framework",
|
|
||||||
"homepage": "https://revealjs.com",
|
|
||||||
"subdomain": "revealjs",
|
|
||||||
"main": "dist/reveal.js",
|
|
||||||
"module": "dist/reveal.esm.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"test": "gulp test",
|
|
||||||
"start": "gulp serve",
|
|
||||||
"build": "gulp build"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "Hakim El Hattab",
|
|
||||||
"email": "hakim.elhattab@gmail.com",
|
|
||||||
"web": "https://hakim.se"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/hakimel/reveal.js.git"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"reveal",
|
|
||||||
"slides",
|
|
||||||
"presentation"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.23.2",
|
|
||||||
"@babel/eslint-parser": "^7.22.15",
|
|
||||||
"@babel/preset-env": "^7.23.2",
|
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
|
||||||
"babel-plugin-transform-html-import-to-string": "2.0.0",
|
|
||||||
"colors": "^1.4.0",
|
|
||||||
"core-js": "^3.33.1",
|
|
||||||
"fitty": "^2.3.7",
|
|
||||||
"glob": "^10.3.10",
|
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"gulp-autoprefixer": "^8.0.0",
|
|
||||||
"gulp-clean-css": "^4.3.0",
|
|
||||||
"gulp-connect": "^5.7.0",
|
|
||||||
"gulp-eslint": "^6.0.0",
|
|
||||||
"gulp-header": "^2.0.9",
|
|
||||||
"gulp-tap": "^2.0.0",
|
|
||||||
"gulp-zip": "^5.1.0",
|
|
||||||
"highlight.js": "^11.9.0",
|
|
||||||
"marked": "^4.3.0",
|
|
||||||
"node-qunit-puppeteer": "^2.1.2",
|
|
||||||
"qunit": "^2.20.0",
|
|
||||||
"rollup": "^4.1.5",
|
|
||||||
"sass": "^1.69.5",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"chokidar": "3.5.3",
|
|
||||||
"glob-parent": "6.0.2"
|
|
||||||
},
|
|
||||||
"browserslist": "> 2%, not dead",
|
|
||||||
"eslintConfig": {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module",
|
|
||||||
"allowImportExportEverywhere": true,
|
|
||||||
"requireConfigFile": false
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"module": false,
|
|
||||||
"console": false,
|
|
||||||
"unescape": false,
|
|
||||||
"define": false,
|
|
||||||
"exports": false
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"curly": 0,
|
|
||||||
"eqeqeq": 2,
|
|
||||||
"wrap-iife": [
|
|
||||||
2,
|
|
||||||
"any"
|
|
||||||
],
|
|
||||||
"no-use-before-define": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"functions": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"new-cap": 2,
|
|
||||||
"no-caller": 2,
|
|
||||||
"dot-notation": 0,
|
|
||||||
"no-eq-null": 2,
|
|
||||||
"no-unused-expressions": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
Monokai style - ported by Luigi Maselli - http://grigio.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
background: #272822;
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-tag,
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-strong,
|
|
||||||
.hljs-name {
|
|
||||||
color: #f92672;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-code {
|
|
||||||
color: #66d9ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-class .hljs-title {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attribute,
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-link {
|
|
||||||
color: #bf79db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-subst,
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-emphasis,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-builtin-name,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo,
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-tag,
|
|
||||||
.hljs-template-variable {
|
|
||||||
color: #a6e22e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote,
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-meta {
|
|
||||||
color: #75715e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-doctag,
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-selector-id {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
@ -1,439 +0,0 @@
|
|||||||
import hljs from 'highlight.js';
|
|
||||||
|
|
||||||
/* highlightjs-line-numbers.js 2.8.0 | (C) 2018 Yauheni Pakala | MIT License | github.com/wcoder/highlightjs-line-numbers.js */
|
|
||||||
!function(r,o){"use strict";var e,i="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",a=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),a=parseInt(r.dataset.lineNumber);if(o==a)return n;var i,l=t.textContent,s=r.textContent;for(a<o&&(i=o,o=a,a=i,i=l,l=s,s=i);0!==n.indexOf(l);)l=l.slice(1);for(;-1===n.lastIndexOf(s);)s=s.slice(0,-1);for(var c=l,u=function(e){for(var n=e;"TABLE"!==n.nodeName;)n=n.parentNode;return n}(t),d=o+1;d<a;++d){var f=p('.{0}[{1}="{2}"]',[h,m,d]);c+="\n"+u.querySelector(f).textContent}return c+="\n"+s}function n(e){try{var n=o.querySelectorAll("code.hljs,code.nohighlight");for(var t in n)n.hasOwnProperty(t)&&(n[t].classList.contains("nohljsln")||d(n[t],e))}catch(e){r.console.error("LineNumbers error: ",e)}}function d(e,n){if("object"==typeof e)e.innerHTML=f(e,n)}function f(e,n){var t,r,o=(t=e,{singleLine:function(e){return!!e.singleLine&&e.singleLine}(r=(r=n)||{}),startFrom:function(e,n){var t=1;isFinite(n.startFrom)&&(t=n.startFrom);var r=function(e,n){return e.hasAttribute(n)?e.getAttribute(n):null}(e,"data-ln-start-from");return null!==r&&(t=function(e,n){if(!e)return n;var t=Number(e);return isFinite(t)?t:n}(r,1)),t}(t,r)});return function e(n){var t=n.childNodes;for(var r in t){var o;t.hasOwnProperty(r)&&(o=t[r],0<(o.textContent.trim().match(a)||[]).length&&(0<o.childNodes.length?e(o):v(o.parentNode)))}}(e),function(e,n){var t=g(e);""===t[t.length-1].trim()&&t.pop();if(1<t.length||n.singleLine){for(var r="",o=0,a=t.length;o<a;o++)r+=p('<tr><td class="{0} {1}" {3}="{5}"><div class="{2}" {3}="{5}"></div></td><td class="{0} {4}" {3}="{5}">{6}</td></tr>',[l,s,c,m,h,o+n.startFrom,0<t[o].length?t[o]:" "]);return p('<table class="{0}">{1}</table>',[i,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r<t.length;r++){o+=p('<span class="{0}">{1}</span>\n',[n,0<t[r].length?t[r]:" "])}e.innerHTML=o.trim()}}function g(e){return 0===e.length?[]:e.split(a)}function p(e,t){return e.replace(/\{(\d+)\}/g,function(e,n){return void 0!==t[n]?t[n]:e})}hljs?(hljs.initLineNumbersOnLoad=function(e){"interactive"===o.readyState||"complete"===o.readyState?n(e):r.addEventListener("DOMContentLoaded",function(){n(e)})},hljs.lineNumbersBlock=d,hljs.lineNumbersValue=function(e,n){if("string"!=typeof e)return;var t=document.createElement("code");return t.innerHTML=e,f(t,n)},(e=o.createElement("style")).type="text/css",e.innerHTML=p(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[i,c,m]),o.getElementsByTagName("head")[0].appendChild(e)):r.console.error("highlight.js not detected!"),document.addEventListener("copy",function(e){var n,t=window.getSelection();!function(e){for(var n=e;n;){if(n.className&&-1!==n.className.indexOf("hljs-ln-code"))return 1;n=n.parentNode}}(t.anchorNode)||(n=-1!==window.navigator.userAgent.indexOf("Edge")?u(t):t.toString(),e.clipboardData.setData("text/plain",n),e.preventDefault())})}(window,document);
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* reveal.js plugin that adds syntax highlight support.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Plugin = {
|
|
||||||
|
|
||||||
id: 'highlight',
|
|
||||||
|
|
||||||
HIGHLIGHT_STEP_DELIMITER: '|',
|
|
||||||
HIGHLIGHT_LINE_DELIMITER: ',',
|
|
||||||
HIGHLIGHT_LINE_RANGE_DELIMITER: '-',
|
|
||||||
|
|
||||||
hljs,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights code blocks within the given deck.
|
|
||||||
*
|
|
||||||
* Note that this can be called multiple times if
|
|
||||||
* there are multiple presentations on one page.
|
|
||||||
*
|
|
||||||
* @param {Reveal} reveal the reveal.js instance
|
|
||||||
*/
|
|
||||||
init: function( reveal ) {
|
|
||||||
|
|
||||||
// Read the plugin config options and provide fallbacks
|
|
||||||
let config = reveal.getConfig().highlight || {};
|
|
||||||
|
|
||||||
config.highlightOnLoad = typeof config.highlightOnLoad === 'boolean' ? config.highlightOnLoad : true;
|
|
||||||
config.escapeHTML = typeof config.escapeHTML === 'boolean' ? config.escapeHTML : true;
|
|
||||||
|
|
||||||
Array.from( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( block => {
|
|
||||||
|
|
||||||
block.parentNode.classList.add('code-wrapper');
|
|
||||||
|
|
||||||
// Code can optionally be wrapped in script template to avoid
|
|
||||||
// HTML being parsed by the browser (i.e. when you need to
|
|
||||||
// include <, > or & in your code).
|
|
||||||
let substitute = block.querySelector( 'script[type="text/template"]' );
|
|
||||||
if( substitute ) {
|
|
||||||
// textContent handles the HTML entity escapes for us
|
|
||||||
block.textContent = substitute.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim whitespace if the "data-trim" attribute is present
|
|
||||||
if( block.hasAttribute( 'data-trim' ) && typeof block.innerHTML.trim === 'function' ) {
|
|
||||||
block.innerHTML = betterTrim( block );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape HTML tags unless the "data-noescape" attrbute is present
|
|
||||||
if( config.escapeHTML && !block.hasAttribute( 'data-noescape' )) {
|
|
||||||
block.innerHTML = block.innerHTML.replace( /</g,"<").replace(/>/g, '>' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-highlight when focus is lost (for contenteditable code)
|
|
||||||
block.addEventListener( 'focusout', function( event ) {
|
|
||||||
hljs.highlightElement( event.currentTarget );
|
|
||||||
}, false );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Triggers a callback function before we trigger highlighting
|
|
||||||
if( typeof config.beforeHighlight === 'function' ) {
|
|
||||||
config.beforeHighlight( hljs );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run initial highlighting for all code
|
|
||||||
if( config.highlightOnLoad ) {
|
|
||||||
Array.from( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( block => {
|
|
||||||
Plugin.highlightBlock( block );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're printing to PDF, scroll the code highlights of
|
|
||||||
// all blocks in the deck into view at once
|
|
||||||
reveal.on( 'pdf-ready', function() {
|
|
||||||
[].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code[data-line-numbers].current-fragment' ) ).forEach( function( block ) {
|
|
||||||
Plugin.scrollHighlightedLineIntoView( block, {}, true );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights a code block. If the <code> node has the
|
|
||||||
* 'data-line-numbers' attribute we also generate slide
|
|
||||||
* numbers.
|
|
||||||
*
|
|
||||||
* If the block contains multiple line highlight steps,
|
|
||||||
* we clone the block and create a fragment for each step.
|
|
||||||
*/
|
|
||||||
highlightBlock: function( block ) {
|
|
||||||
|
|
||||||
hljs.highlightElement( block );
|
|
||||||
|
|
||||||
// Don't generate line numbers for empty code blocks
|
|
||||||
if( block.innerHTML.trim().length === 0 ) return;
|
|
||||||
|
|
||||||
if( block.hasAttribute( 'data-line-numbers' ) ) {
|
|
||||||
hljs.lineNumbersBlock( block, { singleLine: true } );
|
|
||||||
|
|
||||||
var scrollState = { currentBlock: block };
|
|
||||||
|
|
||||||
// If there is more than one highlight step, generate
|
|
||||||
// fragments
|
|
||||||
var highlightSteps = Plugin.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) );
|
|
||||||
if( highlightSteps.length > 1 ) {
|
|
||||||
|
|
||||||
// If the original code block has a fragment-index,
|
|
||||||
// each clone should follow in an incremental sequence
|
|
||||||
var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 );
|
|
||||||
|
|
||||||
if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) {
|
|
||||||
fragmentIndex = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate fragments for all steps except the original block
|
|
||||||
highlightSteps.slice(1).forEach( function( highlight ) {
|
|
||||||
|
|
||||||
var fragmentBlock = block.cloneNode( true );
|
|
||||||
fragmentBlock.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlight ] ) );
|
|
||||||
fragmentBlock.classList.add( 'fragment' );
|
|
||||||
block.parentNode.appendChild( fragmentBlock );
|
|
||||||
Plugin.highlightLines( fragmentBlock );
|
|
||||||
|
|
||||||
if( typeof fragmentIndex === 'number' ) {
|
|
||||||
fragmentBlock.setAttribute( 'data-fragment-index', fragmentIndex );
|
|
||||||
fragmentIndex += 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fragmentBlock.removeAttribute( 'data-fragment-index' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll highlights into view as we step through them
|
|
||||||
fragmentBlock.addEventListener( 'visible', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock, scrollState ) );
|
|
||||||
fragmentBlock.addEventListener( 'hidden', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock.previousElementSibling, scrollState ) );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
block.removeAttribute( 'data-fragment-index' );
|
|
||||||
block.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlightSteps[0] ] ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll the first highlight into view when the slide
|
|
||||||
// becomes visible. Note supported in IE11 since it lacks
|
|
||||||
// support for Element.closest.
|
|
||||||
var slide = typeof block.closest === 'function' ? block.closest( 'section:not(.stack)' ) : null;
|
|
||||||
if( slide ) {
|
|
||||||
var scrollFirstHighlightIntoView = function() {
|
|
||||||
Plugin.scrollHighlightedLineIntoView( block, scrollState, true );
|
|
||||||
slide.removeEventListener( 'visible', scrollFirstHighlightIntoView );
|
|
||||||
}
|
|
||||||
slide.addEventListener( 'visible', scrollFirstHighlightIntoView );
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.highlightLines( block );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animates scrolling to the first highlighted line
|
|
||||||
* in the given code block.
|
|
||||||
*/
|
|
||||||
scrollHighlightedLineIntoView: function( block, scrollState, skipAnimation ) {
|
|
||||||
|
|
||||||
cancelAnimationFrame( scrollState.animationFrameID );
|
|
||||||
|
|
||||||
// Match the scroll position of the currently visible
|
|
||||||
// code block
|
|
||||||
if( scrollState.currentBlock ) {
|
|
||||||
block.scrollTop = scrollState.currentBlock.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember the current code block so that we can match
|
|
||||||
// its scroll position when showing/hiding fragments
|
|
||||||
scrollState.currentBlock = block;
|
|
||||||
|
|
||||||
var highlightBounds = this.getHighlightedLineBounds( block )
|
|
||||||
var viewportHeight = block.offsetHeight;
|
|
||||||
|
|
||||||
// Subtract padding from the viewport height
|
|
||||||
var blockStyles = getComputedStyle( block );
|
|
||||||
viewportHeight -= parseInt( blockStyles.paddingTop ) + parseInt( blockStyles.paddingBottom );
|
|
||||||
|
|
||||||
// Scroll position which centers all highlights
|
|
||||||
var startTop = block.scrollTop;
|
|
||||||
var targetTop = highlightBounds.top + ( Math.min( highlightBounds.bottom - highlightBounds.top, viewportHeight ) - viewportHeight ) / 2;
|
|
||||||
|
|
||||||
// Account for offsets in position applied to the
|
|
||||||
// <table> that holds our lines of code
|
|
||||||
var lineTable = block.querySelector( '.hljs-ln' );
|
|
||||||
if( lineTable ) targetTop += lineTable.offsetTop - parseInt( blockStyles.paddingTop );
|
|
||||||
|
|
||||||
// Make sure the scroll target is within bounds
|
|
||||||
targetTop = Math.max( Math.min( targetTop, block.scrollHeight - viewportHeight ), 0 );
|
|
||||||
|
|
||||||
if( skipAnimation === true || startTop === targetTop ) {
|
|
||||||
block.scrollTop = targetTop;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
// Don't attempt to scroll if there is no overflow
|
|
||||||
if( block.scrollHeight <= viewportHeight ) return;
|
|
||||||
|
|
||||||
var time = 0;
|
|
||||||
var animate = function() {
|
|
||||||
time = Math.min( time + 0.02, 1 );
|
|
||||||
|
|
||||||
// Update our eased scroll position
|
|
||||||
block.scrollTop = startTop + ( targetTop - startTop ) * Plugin.easeInOutQuart( time );
|
|
||||||
|
|
||||||
// Keep animating unless we've reached the end
|
|
||||||
if( time < 1 ) {
|
|
||||||
scrollState.animationFrameID = requestAnimationFrame( animate );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The easing function used when scrolling.
|
|
||||||
*/
|
|
||||||
easeInOutQuart: function( t ) {
|
|
||||||
|
|
||||||
// easeInOutQuart
|
|
||||||
return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getHighlightedLineBounds: function( block ) {
|
|
||||||
|
|
||||||
var highlightedLines = block.querySelectorAll( '.highlight-line' );
|
|
||||||
if( highlightedLines.length === 0 ) {
|
|
||||||
return { top: 0, bottom: 0 };
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var firstHighlight = highlightedLines[0];
|
|
||||||
var lastHighlight = highlightedLines[ highlightedLines.length -1 ];
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: firstHighlight.offsetTop,
|
|
||||||
bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visually emphasize specific lines within a code block.
|
|
||||||
* This only works on blocks with line numbering turned on.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} block a <code> block
|
|
||||||
* @param {String} [linesToHighlight] The lines that should be
|
|
||||||
* highlighted in this format:
|
|
||||||
* "1" = highlights line 1
|
|
||||||
* "2,5" = highlights lines 2 & 5
|
|
||||||
* "2,5-7" = highlights lines 2, 5, 6 & 7
|
|
||||||
*/
|
|
||||||
highlightLines: function( block, linesToHighlight ) {
|
|
||||||
|
|
||||||
var highlightSteps = Plugin.deserializeHighlightSteps( linesToHighlight || block.getAttribute( 'data-line-numbers' ) );
|
|
||||||
|
|
||||||
if( highlightSteps.length ) {
|
|
||||||
|
|
||||||
highlightSteps[0].forEach( function( highlight ) {
|
|
||||||
|
|
||||||
var elementsToHighlight = [];
|
|
||||||
|
|
||||||
// Highlight a range
|
|
||||||
if( typeof highlight.end === 'number' ) {
|
|
||||||
elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child(n+'+highlight.start+'):nth-child(-n+'+highlight.end+')' ) );
|
|
||||||
}
|
|
||||||
// Highlight a single line
|
|
||||||
else if( typeof highlight.start === 'number' ) {
|
|
||||||
elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child('+highlight.start+')' ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( elementsToHighlight.length ) {
|
|
||||||
elementsToHighlight.forEach( function( lineElement ) {
|
|
||||||
lineElement.classList.add( 'highlight-line' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
block.classList.add( 'has-highlights' );
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and formats a user-defined string of line
|
|
||||||
* numbers to highlight.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* Plugin.deserializeHighlightSteps( '1,2|3,5-10' )
|
|
||||||
* // [
|
|
||||||
* // [ { start: 1 }, { start: 2 } ],
|
|
||||||
* // [ { start: 3 }, { start: 5, end: 10 } ]
|
|
||||||
* // ]
|
|
||||||
*/
|
|
||||||
deserializeHighlightSteps: function( highlightSteps ) {
|
|
||||||
|
|
||||||
// Remove whitespace
|
|
||||||
highlightSteps = highlightSteps.replace( /\s/g, '' );
|
|
||||||
|
|
||||||
// Divide up our line number groups
|
|
||||||
highlightSteps = highlightSteps.split( Plugin.HIGHLIGHT_STEP_DELIMITER );
|
|
||||||
|
|
||||||
return highlightSteps.map( function( highlights ) {
|
|
||||||
|
|
||||||
return highlights.split( Plugin.HIGHLIGHT_LINE_DELIMITER ).map( function( highlight ) {
|
|
||||||
|
|
||||||
// Parse valid line numbers
|
|
||||||
if( /^[\d-]+$/.test( highlight ) ) {
|
|
||||||
|
|
||||||
highlight = highlight.split( Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER );
|
|
||||||
|
|
||||||
var lineStart = parseInt( highlight[0], 10 ),
|
|
||||||
lineEnd = parseInt( highlight[1], 10 );
|
|
||||||
|
|
||||||
if( isNaN( lineEnd ) ) {
|
|
||||||
return {
|
|
||||||
start: lineStart
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {
|
|
||||||
start: lineStart,
|
|
||||||
end: lineEnd
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// If no line numbers are provided, no code will be highlighted
|
|
||||||
else {
|
|
||||||
|
|
||||||
return {};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes parsed line number data into a string so
|
|
||||||
* that we can store it in the DOM.
|
|
||||||
*/
|
|
||||||
serializeHighlightSteps: function( highlightSteps ) {
|
|
||||||
|
|
||||||
return highlightSteps.map( function( highlights ) {
|
|
||||||
|
|
||||||
return highlights.map( function( highlight ) {
|
|
||||||
|
|
||||||
// Line range
|
|
||||||
if( typeof highlight.end === 'number' ) {
|
|
||||||
return highlight.start + Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER + highlight.end;
|
|
||||||
}
|
|
||||||
// Single line
|
|
||||||
else if( typeof highlight.start === 'number' ) {
|
|
||||||
return highlight.start;
|
|
||||||
}
|
|
||||||
// All lines
|
|
||||||
else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
} ).join( Plugin.HIGHLIGHT_LINE_DELIMITER );
|
|
||||||
|
|
||||||
} ).join( Plugin.HIGHLIGHT_STEP_DELIMITER );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to perform a better "data-trim" on code snippets
|
|
||||||
// Will slice an indentation amount on each line of the snippet (amount based on the line having the lowest indentation length)
|
|
||||||
function betterTrim(snippetEl) {
|
|
||||||
// Helper functions
|
|
||||||
function trimLeft(val) {
|
|
||||||
// Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill
|
|
||||||
return val.replace(/^[\s\uFEFF\xA0]+/g, '');
|
|
||||||
}
|
|
||||||
function trimLineBreaks(input) {
|
|
||||||
var lines = input.split('\n');
|
|
||||||
|
|
||||||
// Trim line-breaks from the beginning
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].trim() === '') {
|
|
||||||
lines.splice(i--, 1);
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim line-breaks from the end
|
|
||||||
for (var i = lines.length-1; i >= 0; i--) {
|
|
||||||
if (lines[i].trim() === '') {
|
|
||||||
lines.splice(i, 1);
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function for betterTrim()
|
|
||||||
return (function(snippetEl) {
|
|
||||||
var content = trimLineBreaks(snippetEl.innerHTML);
|
|
||||||
var lines = content.split('\n');
|
|
||||||
// Calculate the minimum amount to remove on each line start of the snippet (can be 0)
|
|
||||||
var pad = lines.reduce(function(acc, line) {
|
|
||||||
if (line.length > 0 && trimLeft(line).length > 0 && acc > line.length - trimLeft(line).length) {
|
|
||||||
return line.length - trimLeft(line).length;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, Number.POSITIVE_INFINITY);
|
|
||||||
// Slice each line with this amount
|
|
||||||
return lines.map(function(line, index) {
|
|
||||||
return line.slice(pad);
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
})(snippetEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => Plugin;
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
Zenburn style from voldmar.ru (c) Vladimir Epifanov <voldmar@voldmar.ru>
|
|
||||||
based on dark.css by Ivan Sagalaev
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
background: #3f3f3f;
|
|
||||||
color: #dcdcdc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-tag {
|
|
||||||
color: #e3ceab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-template-tag {
|
|
||||||
color: #dcdcdc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-number {
|
|
||||||
color: #8cd0d3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-attribute {
|
|
||||||
color: #efdcbc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal {
|
|
||||||
color: #efefaf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-subst {
|
|
||||||
color: #8f8f8f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-type {
|
|
||||||
color: #efef8f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-link {
|
|
||||||
color: #dca3a3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-builtin-name {
|
|
||||||
color: #cc9393;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote,
|
|
||||||
.hljs-meta {
|
|
||||||
color: #7f9f7f;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
@ -1,491 +0,0 @@
|
|||||||
/*!
|
|
||||||
* The reveal.js markdown plugin. Handles parsing of
|
|
||||||
* markdown inside of presentations as well as loading
|
|
||||||
* of external markdown documents.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { marked } from 'marked';
|
|
||||||
|
|
||||||
const DEFAULT_SLIDE_SEPARATOR = '\r?\n---\r?\n',
|
|
||||||
DEFAULT_VERTICAL_SEPARATOR = null,
|
|
||||||
DEFAULT_NOTES_SEPARATOR = '^\s*notes?:',
|
|
||||||
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
|
|
||||||
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
|
|
||||||
|
|
||||||
const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
|
|
||||||
|
|
||||||
// match an optional line number offset and highlight line numbers
|
|
||||||
// [<line numbers>] or [<offset>: <line numbers>]
|
|
||||||
const CODE_LINE_NUMBER_REGEX = /\[\s*((\d*):)?\s*([\s\d,|-]*)\]/;
|
|
||||||
|
|
||||||
const HTML_ESCAPE_MAP = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
|
|
||||||
const Plugin = () => {
|
|
||||||
|
|
||||||
// The reveal.js instance this plugin is attached to
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the markdown contents of a slide section
|
|
||||||
* element. Normalizes leading tabs/whitespace.
|
|
||||||
*/
|
|
||||||
function getMarkdownFromSlide( section ) {
|
|
||||||
|
|
||||||
// look for a <script> or <textarea data-template> wrapper
|
|
||||||
const template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
|
|
||||||
|
|
||||||
// strip leading whitespace so it isn't evaluated as code
|
|
||||||
let text = ( template || section ).textContent;
|
|
||||||
|
|
||||||
// restore script end tags
|
|
||||||
text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
|
|
||||||
|
|
||||||
const leadingWs = text.match( /^\n?(\s*)/ )[1].length,
|
|
||||||
leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
|
|
||||||
|
|
||||||
if( leadingTabs > 0 ) {
|
|
||||||
text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}(.*)','g'), function(m, p1) { return '\n' + p1 ; } );
|
|
||||||
}
|
|
||||||
else if( leadingWs > 1 ) {
|
|
||||||
text = text.replace( new RegExp('\\n? {' + leadingWs + '}(.*)', 'g'), function(m, p1) { return '\n' + p1 ; } );
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a markdown slide section element, this will
|
|
||||||
* return all arguments that aren't related to markdown
|
|
||||||
* parsing. Used to forward any other user-defined arguments
|
|
||||||
* to the output markdown slide.
|
|
||||||
*/
|
|
||||||
function getForwardedAttributes( section ) {
|
|
||||||
|
|
||||||
const attributes = section.attributes;
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for( let i = 0, len = attributes.length; i < len; i++ ) {
|
|
||||||
const name = attributes[i].name,
|
|
||||||
value = attributes[i].value;
|
|
||||||
|
|
||||||
// disregard attributes that are used for markdown loading/parsing
|
|
||||||
if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
|
|
||||||
|
|
||||||
if( value ) {
|
|
||||||
result.push( name + '="' + value + '"' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result.push( name );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.join( ' ' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inspects the given options and fills out default
|
|
||||||
* values for what's not defined.
|
|
||||||
*/
|
|
||||||
function getSlidifyOptions( options ) {
|
|
||||||
const markdownConfig = deck?.getConfig?.().markdown;
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
options.separator = options.separator || markdownConfig?.separator || DEFAULT_SLIDE_SEPARATOR;
|
|
||||||
options.verticalSeparator = options.verticalSeparator || markdownConfig?.verticalSeparator || DEFAULT_VERTICAL_SEPARATOR;
|
|
||||||
options.notesSeparator = options.notesSeparator || markdownConfig?.notesSeparator || DEFAULT_NOTES_SEPARATOR;
|
|
||||||
options.attributes = options.attributes || '';
|
|
||||||
|
|
||||||
return options;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function for constructing a markdown slide.
|
|
||||||
*/
|
|
||||||
function createMarkdownSlide( content, options ) {
|
|
||||||
|
|
||||||
options = getSlidifyOptions( options );
|
|
||||||
|
|
||||||
const notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
|
|
||||||
|
|
||||||
if( notesMatch.length === 2 ) {
|
|
||||||
content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent script end tags in the content from interfering
|
|
||||||
// with parsing
|
|
||||||
content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
|
|
||||||
|
|
||||||
return '<script type="text/template">' + content + '</script>';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a data string into multiple slides based
|
|
||||||
* on the passed in separator arguments.
|
|
||||||
*/
|
|
||||||
function slidify( markdown, options ) {
|
|
||||||
|
|
||||||
options = getSlidifyOptions( options );
|
|
||||||
|
|
||||||
const separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
|
|
||||||
horizontalSeparatorRegex = new RegExp( options.separator );
|
|
||||||
|
|
||||||
let matches,
|
|
||||||
lastIndex = 0,
|
|
||||||
isHorizontal,
|
|
||||||
wasHorizontal = true,
|
|
||||||
content,
|
|
||||||
sectionStack = [];
|
|
||||||
|
|
||||||
// iterate until all blocks between separators are stacked up
|
|
||||||
while( matches = separatorRegex.exec( markdown ) ) {
|
|
||||||
const notes = null;
|
|
||||||
|
|
||||||
// determine direction (horizontal by default)
|
|
||||||
isHorizontal = horizontalSeparatorRegex.test( matches[0] );
|
|
||||||
|
|
||||||
if( !isHorizontal && wasHorizontal ) {
|
|
||||||
// create vertical stack
|
|
||||||
sectionStack.push( [] );
|
|
||||||
}
|
|
||||||
|
|
||||||
// pluck slide content from markdown input
|
|
||||||
content = markdown.substring( lastIndex, matches.index );
|
|
||||||
|
|
||||||
if( isHorizontal && wasHorizontal ) {
|
|
||||||
// add to horizontal stack
|
|
||||||
sectionStack.push( content );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// add to vertical stack
|
|
||||||
sectionStack[sectionStack.length-1].push( content );
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = separatorRegex.lastIndex;
|
|
||||||
wasHorizontal = isHorizontal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the remaining slide
|
|
||||||
( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
|
|
||||||
|
|
||||||
let markdownSections = '';
|
|
||||||
|
|
||||||
// flatten the hierarchical stack, and insert <section data-markdown> tags
|
|
||||||
for( let i = 0, len = sectionStack.length; i < len; i++ ) {
|
|
||||||
// vertical
|
|
||||||
if( sectionStack[i] instanceof Array ) {
|
|
||||||
markdownSections += '<section '+ options.attributes +'>';
|
|
||||||
|
|
||||||
sectionStack[i].forEach( function( child ) {
|
|
||||||
markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
|
|
||||||
} );
|
|
||||||
|
|
||||||
markdownSections += '</section>';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return markdownSections;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses any current data-markdown slides, splits
|
|
||||||
* multi-slide markdown into separate sections and
|
|
||||||
* handles loading of external markdown.
|
|
||||||
*/
|
|
||||||
function processSlides( scope ) {
|
|
||||||
|
|
||||||
return new Promise( function( resolve ) {
|
|
||||||
|
|
||||||
const externalPromises = [];
|
|
||||||
|
|
||||||
[].slice.call( scope.querySelectorAll( 'section[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) {
|
|
||||||
|
|
||||||
if( section.getAttribute( 'data-markdown' ).length ) {
|
|
||||||
|
|
||||||
externalPromises.push( loadExternalMarkdown( section ).then(
|
|
||||||
|
|
||||||
// Finished loading external file
|
|
||||||
function( xhr, url ) {
|
|
||||||
section.outerHTML = slidify( xhr.responseText, {
|
|
||||||
separator: section.getAttribute( 'data-separator' ),
|
|
||||||
verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
|
|
||||||
notesSeparator: section.getAttribute( 'data-separator-notes' ),
|
|
||||||
attributes: getForwardedAttributes( section )
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Failed to load markdown
|
|
||||||
function( xhr, url ) {
|
|
||||||
section.outerHTML = '<section data-state="alert">' +
|
|
||||||
'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
|
|
||||||
'Check your browser\'s JavaScript console for more details.' +
|
|
||||||
'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
|
|
||||||
'</section>';
|
|
||||||
}
|
|
||||||
|
|
||||||
) );
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
section.outerHTML = slidify( getMarkdownFromSlide( section ), {
|
|
||||||
separator: section.getAttribute( 'data-separator' ),
|
|
||||||
verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
|
|
||||||
notesSeparator: section.getAttribute( 'data-separator-notes' ),
|
|
||||||
attributes: getForwardedAttributes( section )
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all( externalPromises ).then( resolve );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadExternalMarkdown( section ) {
|
|
||||||
|
|
||||||
return new Promise( function( resolve, reject ) {
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest(),
|
|
||||||
url = section.getAttribute( 'data-markdown' );
|
|
||||||
|
|
||||||
const datacharset = section.getAttribute( 'data-charset' );
|
|
||||||
|
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
|
|
||||||
if( datacharset !== null && datacharset !== '' ) {
|
|
||||||
xhr.overrideMimeType( 'text/html; charset=' + datacharset );
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function( section, xhr ) {
|
|
||||||
if( xhr.readyState === 4 ) {
|
|
||||||
// file protocol yields status code 0 (useful for local debug, mobile applications etc.)
|
|
||||||
if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
|
|
||||||
|
|
||||||
resolve( xhr, url );
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
reject( xhr, url );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.bind( this, section, xhr );
|
|
||||||
|
|
||||||
xhr.open( 'GET', url, true );
|
|
||||||
|
|
||||||
try {
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
catch ( e ) {
|
|
||||||
console.warn( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
|
|
||||||
resolve( xhr, url );
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a node value has the attributes pattern.
|
|
||||||
* If yes, extract it and add that value as one or several attributes
|
|
||||||
* to the target element.
|
|
||||||
*
|
|
||||||
* You need Cache Killer on Chrome to see the effect on any FOM transformation
|
|
||||||
* directly on refresh (F5)
|
|
||||||
* http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
|
|
||||||
*/
|
|
||||||
function addAttributeInElement( node, elementTarget, separator ) {
|
|
||||||
|
|
||||||
const markdownClassesInElementsRegex = new RegExp( separator, 'mg' );
|
|
||||||
const markdownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' );
|
|
||||||
let nodeValue = node.nodeValue;
|
|
||||||
let matches,
|
|
||||||
matchesClass;
|
|
||||||
if( matches = markdownClassesInElementsRegex.exec( nodeValue ) ) {
|
|
||||||
|
|
||||||
const classes = matches[1];
|
|
||||||
nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( markdownClassesInElementsRegex.lastIndex );
|
|
||||||
node.nodeValue = nodeValue;
|
|
||||||
while( matchesClass = markdownClassRegex.exec( classes ) ) {
|
|
||||||
if( matchesClass[2] ) {
|
|
||||||
elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
|
|
||||||
} else {
|
|
||||||
elementTarget.setAttribute( matchesClass[3], "" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add attributes to the parent element of a text node,
|
|
||||||
* or the element of an attribute node.
|
|
||||||
*/
|
|
||||||
function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
|
|
||||||
|
|
||||||
if ( element !== null && element.childNodes !== undefined && element.childNodes.length > 0 ) {
|
|
||||||
let previousParentElement = element;
|
|
||||||
for( let i = 0; i < element.childNodes.length; i++ ) {
|
|
||||||
const childElement = element.childNodes[i];
|
|
||||||
if ( i > 0 ) {
|
|
||||||
let j = i - 1;
|
|
||||||
while ( j >= 0 ) {
|
|
||||||
const aPreviousChildElement = element.childNodes[j];
|
|
||||||
if ( typeof aPreviousChildElement.setAttribute === 'function' && aPreviousChildElement.tagName !== "BR" ) {
|
|
||||||
previousParentElement = aPreviousChildElement;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
j = j - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let parentSection = section;
|
|
||||||
if( childElement.nodeName === "section" ) {
|
|
||||||
parentSection = childElement ;
|
|
||||||
previousParentElement = childElement ;
|
|
||||||
}
|
|
||||||
if ( typeof childElement.setAttribute === 'function' || childElement.nodeType === Node.COMMENT_NODE ) {
|
|
||||||
addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element.nodeType === Node.COMMENT_NODE ) {
|
|
||||||
if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) === false ) {
|
|
||||||
addAttributeInElement( element, section, separatorSectionAttributes );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts any current data-markdown slides in the
|
|
||||||
* DOM to HTML.
|
|
||||||
*/
|
|
||||||
function convertSlides() {
|
|
||||||
|
|
||||||
const sections = deck.getRevealElement().querySelectorAll( '[data-markdown]:not([data-markdown-parsed])');
|
|
||||||
|
|
||||||
[].slice.call( sections ).forEach( function( section ) {
|
|
||||||
|
|
||||||
section.setAttribute( 'data-markdown-parsed', true )
|
|
||||||
|
|
||||||
const notes = section.querySelector( 'aside.notes' );
|
|
||||||
const markdown = getMarkdownFromSlide( section );
|
|
||||||
|
|
||||||
section.innerHTML = marked( markdown );
|
|
||||||
addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) ||
|
|
||||||
section.parentNode.getAttribute( 'data-element-attributes' ) ||
|
|
||||||
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
|
|
||||||
section.getAttribute( 'data-attributes' ) ||
|
|
||||||
section.parentNode.getAttribute( 'data-attributes' ) ||
|
|
||||||
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
|
|
||||||
|
|
||||||
// If there were notes, we need to re-add them after
|
|
||||||
// having overwritten the section's HTML
|
|
||||||
if( notes ) {
|
|
||||||
section.appendChild( notes );
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeForHTML( input ) {
|
|
||||||
|
|
||||||
return input.replace( /([&<>'"])/g, char => HTML_ESCAPE_MAP[char] );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'markdown',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts processing and converting Markdown within the
|
|
||||||
* current reveal.js deck.
|
|
||||||
*/
|
|
||||||
init: function( reveal ) {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
|
|
||||||
let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {};
|
|
||||||
|
|
||||||
if( !renderer ) {
|
|
||||||
renderer = new marked.Renderer();
|
|
||||||
|
|
||||||
renderer.code = ( code, language ) => {
|
|
||||||
|
|
||||||
// Off by default
|
|
||||||
let lineNumberOffset = '';
|
|
||||||
let lineNumbers = '';
|
|
||||||
|
|
||||||
// Users can opt in to show line numbers and highlight
|
|
||||||
// specific lines.
|
|
||||||
// ```javascript [] show line numbers
|
|
||||||
// ```javascript [1,4-8] highlights lines 1 and 4-8
|
|
||||||
// optional line number offset:
|
|
||||||
// ```javascript [25: 1,4-8] start line numbering at 25,
|
|
||||||
// highlights lines 1 (numbered as 25) and 4-8 (numbered as 28-32)
|
|
||||||
if( CODE_LINE_NUMBER_REGEX.test( language ) ) {
|
|
||||||
let lineNumberOffsetMatch = language.match( CODE_LINE_NUMBER_REGEX )[2];
|
|
||||||
if (lineNumberOffsetMatch){
|
|
||||||
lineNumberOffset = `data-ln-start-from="${lineNumberOffsetMatch.trim()}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
lineNumbers = language.match( CODE_LINE_NUMBER_REGEX )[3].trim();
|
|
||||||
lineNumbers = `data-line-numbers="${lineNumbers}"`;
|
|
||||||
language = language.replace( CODE_LINE_NUMBER_REGEX, '' ).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape before this gets injected into the DOM to
|
|
||||||
// avoid having the HTML parser alter our code before
|
|
||||||
// highlight.js is able to read it
|
|
||||||
code = escapeForHTML( code );
|
|
||||||
|
|
||||||
// return `<pre><code ${lineNumbers} class="${language}">${code}</code></pre>`;
|
|
||||||
|
|
||||||
return `<pre><code ${lineNumbers} ${lineNumberOffset} class="${language}">${code}</code></pre>`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if( animateLists === true ) {
|
|
||||||
renderer.listitem = text => `<li class="fragment">${text}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
marked.setOptions( {
|
|
||||||
renderer,
|
|
||||||
...markedOptions
|
|
||||||
} );
|
|
||||||
|
|
||||||
return processSlides( deck.getRevealElement() ).then( convertSlides );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: Do these belong in the API?
|
|
||||||
processSlides: processSlides,
|
|
||||||
convertSlides: convertSlides,
|
|
||||||
slidify: slidify,
|
|
||||||
marked: marked
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Plugin;
|
|
@ -1,96 +0,0 @@
|
|||||||
/**
|
|
||||||
* A plugin which enables rendering of math equations inside
|
|
||||||
* of reveal.js slides. Essentially a thin wrapper for KaTeX.
|
|
||||||
*
|
|
||||||
* @author Hakim El Hattab
|
|
||||||
* @author Gerhard Burger
|
|
||||||
*/
|
|
||||||
export const KaTeX = () => {
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
let defaultOptions = {
|
|
||||||
version: 'latest',
|
|
||||||
delimiters: [
|
|
||||||
{left: '$$', right: '$$', display: true}, // Note: $$ has to come before $
|
|
||||||
{left: '$', right: '$', display: false},
|
|
||||||
{left: '\\(', right: '\\)', display: false},
|
|
||||||
{left: '\\[', right: '\\]', display: true}
|
|
||||||
],
|
|
||||||
ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre']
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadCss = src => {
|
|
||||||
let link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = src;
|
|
||||||
document.head.appendChild(link);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a JavaScript file and returns a Promise for when it is loaded
|
|
||||||
* Credits: https://aaronsmith.online/easily-load-an-external-script-using-javascript/
|
|
||||||
*/
|
|
||||||
const loadScript = src => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.type = 'text/javascript'
|
|
||||||
script.onload = resolve
|
|
||||||
script.onerror = reject
|
|
||||||
script.src = src
|
|
||||||
document.head.append(script)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadScripts(urls) {
|
|
||||||
for(const url of urls) {
|
|
||||||
await loadScript(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'katex',
|
|
||||||
|
|
||||||
init: function (reveal) {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
|
|
||||||
let revealOptions = deck.getConfig().katex || {};
|
|
||||||
|
|
||||||
let options = {...defaultOptions, ...revealOptions};
|
|
||||||
const {local, version, extensions, ...katexOptions} = options;
|
|
||||||
|
|
||||||
let baseUrl = options.local || 'https://cdn.jsdelivr.net/npm/katex';
|
|
||||||
let versionString = options.local ? '' : '@' + options.version;
|
|
||||||
|
|
||||||
let cssUrl = baseUrl + versionString + '/dist/katex.min.css';
|
|
||||||
let katexUrl = baseUrl + versionString + '/dist/katex.min.js';
|
|
||||||
let mhchemUrl = baseUrl + versionString + '/dist/contrib/mhchem.min.js'
|
|
||||||
let karUrl = baseUrl + versionString + '/dist/contrib/auto-render.min.js';
|
|
||||||
|
|
||||||
let katexScripts = [katexUrl];
|
|
||||||
if(options.extensions && options.extensions.includes("mhchem")) {
|
|
||||||
katexScripts.push(mhchemUrl);
|
|
||||||
}
|
|
||||||
katexScripts.push(karUrl);
|
|
||||||
|
|
||||||
const renderMath = () => {
|
|
||||||
renderMathInElement(reveal.getSlidesElement(), katexOptions);
|
|
||||||
deck.layout();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCss(cssUrl);
|
|
||||||
|
|
||||||
// For some reason dynamically loading with defer attribute doesn't result in the expected behavior, the below code does
|
|
||||||
loadScripts(katexScripts).then(() => {
|
|
||||||
if( deck.isReady() ) {
|
|
||||||
renderMath();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
deck.on( 'ready', renderMath.bind( this ) );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
const t=()=>{let t,e={messageStyle:"none",tex2jax:{inlineMath:[["$","$"],["\\(","\\)"]],skipTags:["script","noscript","style","textarea","pre"]},skipStartupTypeset:!0};return{id:"mathjax2",init:function(a){t=a;let n=t.getConfig().mathjax2||t.getConfig().math||{},i={...e,...n},s=(i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js")+"?config="+(i.config||"TeX-AMS_HTML-full");i.tex2jax={...e.tex2jax,...n.tex2jax},i.mathjax=i.config=null,function(t,e){let a=document.querySelector("head"),n=document.createElement("script");n.type="text/javascript",n.src=t;let i=()=>{"function"==typeof e&&(e.call(),e=null)};n.onload=i,n.onreadystatechange=()=>{"loaded"===this.readyState&&i()},a.appendChild(n)}(s,(function(){MathJax.Hub.Config(i),MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.getRevealElement()]),MathJax.Hub.Queue(t.layout),t.on("slidechanged",(function(t){MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.currentSlide])}))}))}}},e=t;
|
|
||||||
/*!
|
|
||||||
* This plugin is a wrapper for the MathJax2,
|
|
||||||
* MathJax3 and KaTeX typesetter plugins.
|
|
||||||
*/
|
|
||||||
var a=Plugin=Object.assign(e(),{KaTeX:()=>{let t,e={version:"latest",delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],ignoredTags:["script","noscript","style","textarea","pre"]};const a=t=>new Promise(((e,a)=>{const n=document.createElement("script");n.type="text/javascript",n.onload=e,n.onerror=a,n.src=t,document.head.append(n)}));return{id:"katex",init:function(n){t=n;let i=t.getConfig().katex||{},s={...e,...i};const{local:l,version:o,extensions:r,...c}=s;let d=s.local||"https://cdn.jsdelivr.net/npm/katex",p=s.local?"":"@"+s.version,u=d+p+"/dist/katex.min.css",h=d+p+"/dist/contrib/mhchem.min.js",x=d+p+"/dist/contrib/auto-render.min.js",m=[d+p+"/dist/katex.min.js"];s.extensions&&s.extensions.includes("mhchem")&&m.push(h),m.push(x);const f=()=>{renderMathInElement(n.getSlidesElement(),c),t.layout()};(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)})(u),async function(t){for(const e of t)await a(e)}(m).then((()=>{t.isReady()?f():t.on("ready",f.bind(this))}))}}},MathJax2:t,MathJax3:()=>{let t,e={tex:{inlineMath:[["$","$"],["\\(","\\)"]]},options:{skipHtmlTags:["script","noscript","style","textarea","pre"]},startup:{ready:()=>{MathJax.startup.defaultReady(),MathJax.startup.promise.then((()=>{Reveal.layout()}))}}};return{id:"mathjax3",init:function(a){t=a;let n=t.getConfig().mathjax3||{},i={...e,...n};i.tex={...e.tex,...n.tex},i.options={...e.options,...n.options},i.startup={...e.startup,...n.startup};let s=i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";i.mathjax=null,window.MathJax=i,function(t,e){let a=document.createElement("script");a.type="text/javascript",a.id="MathJax-script",a.src=t,a.async=!0,a.onload=()=>{"function"==typeof e&&(e.call(),e=null)},document.head.appendChild(a)}(s,(function(){Reveal.addEventListener("slidechanged",(function(t){MathJax.typeset()}))}))}}}});export{a as default};
|
|
@ -1 +0,0 @@
|
|||||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).RevealMath=e()}(this,(function(){"use strict";const t=()=>{let t,e={messageStyle:"none",tex2jax:{inlineMath:[["$","$"],["\\(","\\)"]],skipTags:["script","noscript","style","textarea","pre"]},skipStartupTypeset:!0};return{id:"mathjax2",init:function(n){t=n;let a=t.getConfig().mathjax2||t.getConfig().math||{},i={...e,...a},s=(i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js")+"?config="+(i.config||"TeX-AMS_HTML-full");i.tex2jax={...e.tex2jax,...a.tex2jax},i.mathjax=i.config=null,function(t,e){let n=document.querySelector("head"),a=document.createElement("script");a.type="text/javascript",a.src=t;let i=()=>{"function"==typeof e&&(e.call(),e=null)};a.onload=i,a.onreadystatechange=()=>{"loaded"===this.readyState&&i()},n.appendChild(a)}(s,(function(){MathJax.Hub.Config(i),MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.getRevealElement()]),MathJax.Hub.Queue(t.layout),t.on("slidechanged",(function(t){MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.currentSlide])}))}))}}},e=t;return Plugin=Object.assign(e(),{KaTeX:()=>{let t,e={version:"latest",delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],ignoredTags:["script","noscript","style","textarea","pre"]};const n=t=>new Promise(((e,n)=>{const a=document.createElement("script");a.type="text/javascript",a.onload=e,a.onerror=n,a.src=t,document.head.append(a)}));return{id:"katex",init:function(a){t=a;let i=t.getConfig().katex||{},s={...e,...i};const{local:o,version:l,extensions:r,...c}=s;let d=s.local||"https://cdn.jsdelivr.net/npm/katex",u=s.local?"":"@"+s.version,p=d+u+"/dist/katex.min.css",h=d+u+"/dist/contrib/mhchem.min.js",x=d+u+"/dist/contrib/auto-render.min.js",m=[d+u+"/dist/katex.min.js"];s.extensions&&s.extensions.includes("mhchem")&&m.push(h),m.push(x);const f=()=>{renderMathInElement(a.getSlidesElement(),c),t.layout()};(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)})(p),async function(t){for(const e of t)await n(e)}(m).then((()=>{t.isReady()?f():t.on("ready",f.bind(this))}))}}},MathJax2:t,MathJax3:()=>{let t,e={tex:{inlineMath:[["$","$"],["\\(","\\)"]]},options:{skipHtmlTags:["script","noscript","style","textarea","pre"]},startup:{ready:()=>{MathJax.startup.defaultReady(),MathJax.startup.promise.then((()=>{Reveal.layout()}))}}};return{id:"mathjax3",init:function(n){t=n;let a=t.getConfig().mathjax3||{},i={...e,...a};i.tex={...e.tex,...a.tex},i.options={...e.options,...a.options},i.startup={...e.startup,...a.startup};let s=i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";i.mathjax=null,window.MathJax=i,function(t,e){let n=document.createElement("script");n.type="text/javascript",n.id="MathJax-script",n.src=t,n.async=!0,n.onload=()=>{"function"==typeof e&&(e.call(),e=null)},document.head.appendChild(n)}(s,(function(){Reveal.addEventListener("slidechanged",(function(t){MathJax.typeset()}))}))}}}})}));
|
|
@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* A plugin which enables rendering of math equations inside
|
|
||||||
* of reveal.js slides. Essentially a thin wrapper for MathJax.
|
|
||||||
*
|
|
||||||
* @author Hakim El Hattab
|
|
||||||
*/
|
|
||||||
export const MathJax2 = () => {
|
|
||||||
|
|
||||||
// The reveal.js instance this plugin is attached to
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
let defaultOptions = {
|
|
||||||
messageStyle: 'none',
|
|
||||||
tex2jax: {
|
|
||||||
inlineMath: [ [ '$', '$' ], [ '\\(', '\\)' ] ],
|
|
||||||
skipTags: [ 'script', 'noscript', 'style', 'textarea', 'pre' ]
|
|
||||||
},
|
|
||||||
skipStartupTypeset: true
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadScript( url, callback ) {
|
|
||||||
|
|
||||||
let head = document.querySelector( 'head' );
|
|
||||||
let script = document.createElement( 'script' );
|
|
||||||
script.type = 'text/javascript';
|
|
||||||
script.src = url;
|
|
||||||
|
|
||||||
// Wrapper for callback to make sure it only fires once
|
|
||||||
let finish = () => {
|
|
||||||
if( typeof callback === 'function' ) {
|
|
||||||
callback.call();
|
|
||||||
callback = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
script.onload = finish;
|
|
||||||
|
|
||||||
// IE
|
|
||||||
script.onreadystatechange = () => {
|
|
||||||
if ( this.readyState === 'loaded' ) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal browsers
|
|
||||||
head.appendChild( script );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'mathjax2',
|
|
||||||
|
|
||||||
init: function( reveal ) {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
|
|
||||||
let revealOptions = deck.getConfig().mathjax2 || deck.getConfig().math || {};
|
|
||||||
|
|
||||||
let options = { ...defaultOptions, ...revealOptions };
|
|
||||||
let mathjax = options.mathjax || 'https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js';
|
|
||||||
let config = options.config || 'TeX-AMS_HTML-full';
|
|
||||||
let url = mathjax + '?config=' + config;
|
|
||||||
|
|
||||||
options.tex2jax = { ...defaultOptions.tex2jax, ...revealOptions.tex2jax };
|
|
||||||
|
|
||||||
options.mathjax = options.config = null;
|
|
||||||
|
|
||||||
loadScript( url, function() {
|
|
||||||
|
|
||||||
MathJax.Hub.Config( options );
|
|
||||||
|
|
||||||
// Typeset followed by an immediate reveal.js layout since
|
|
||||||
// the typesetting process could affect slide height
|
|
||||||
MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, deck.getRevealElement() ] );
|
|
||||||
MathJax.Hub.Queue( deck.layout );
|
|
||||||
|
|
||||||
// Reprocess equations in slides when they turn visible
|
|
||||||
deck.on( 'slidechanged', function( event ) {
|
|
||||||
|
|
||||||
MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, event.currentSlide ] );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* A plugin which enables rendering of math equations inside
|
|
||||||
* of reveal.js slides. Essentially a thin wrapper for MathJax 3
|
|
||||||
*
|
|
||||||
* @author Hakim El Hattab
|
|
||||||
* @author Gerhard Burger
|
|
||||||
*/
|
|
||||||
export const MathJax3 = () => {
|
|
||||||
|
|
||||||
// The reveal.js instance this plugin is attached to
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
let defaultOptions = {
|
|
||||||
tex: {
|
|
||||||
inlineMath: [ [ '$', '$' ], [ '\\(', '\\)' ] ]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
skipHtmlTags: [ 'script', 'noscript', 'style', 'textarea', 'pre' ]
|
|
||||||
},
|
|
||||||
startup: {
|
|
||||||
ready: () => {
|
|
||||||
MathJax.startup.defaultReady();
|
|
||||||
MathJax.startup.promise.then(() => {
|
|
||||||
Reveal.layout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadScript( url, callback ) {
|
|
||||||
|
|
||||||
let script = document.createElement( 'script' );
|
|
||||||
script.type = "text/javascript"
|
|
||||||
script.id = "MathJax-script"
|
|
||||||
script.src = url;
|
|
||||||
script.async = true
|
|
||||||
|
|
||||||
// Wrapper for callback to make sure it only fires once
|
|
||||||
script.onload = () => {
|
|
||||||
if (typeof callback === 'function') {
|
|
||||||
callback.call();
|
|
||||||
callback = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.head.appendChild( script );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'mathjax3',
|
|
||||||
init: function(reveal) {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
|
|
||||||
let revealOptions = deck.getConfig().mathjax3 || {};
|
|
||||||
let options = {...defaultOptions, ...revealOptions};
|
|
||||||
options.tex = {...defaultOptions.tex, ...revealOptions.tex}
|
|
||||||
options.options = {...defaultOptions.options, ...revealOptions.options}
|
|
||||||
options.startup = {...defaultOptions.startup, ...revealOptions.startup}
|
|
||||||
|
|
||||||
let url = options.mathjax || 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
|
|
||||||
options.mathjax = null;
|
|
||||||
|
|
||||||
window.MathJax = options;
|
|
||||||
|
|
||||||
loadScript( url, function() {
|
|
||||||
// Reprocess equations in slides when they turn visible
|
|
||||||
Reveal.addEventListener( 'slidechanged', function( event ) {
|
|
||||||
MathJax.typeset();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -1,15 +0,0 @@
|
|||||||
import {KaTeX} from "./katex";
|
|
||||||
import {MathJax2} from "./mathjax2";
|
|
||||||
import {MathJax3} from "./mathjax3";
|
|
||||||
|
|
||||||
const defaultTypesetter = MathJax2;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* This plugin is a wrapper for the MathJax2,
|
|
||||||
* MathJax3 and KaTeX typesetter plugins.
|
|
||||||
*/
|
|
||||||
export default Plugin = Object.assign( defaultTypesetter(), {
|
|
||||||
KaTeX,
|
|
||||||
MathJax2,
|
|
||||||
MathJax3
|
|
||||||
} );
|
|
@ -1,265 +0,0 @@
|
|||||||
import speakerViewHTML from './speaker-view.html'
|
|
||||||
|
|
||||||
import { marked } from 'marked';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles opening of and synchronization with the reveal.js
|
|
||||||
* notes window.
|
|
||||||
*
|
|
||||||
* Handshake process:
|
|
||||||
* 1. This window posts 'connect' to notes window
|
|
||||||
* - Includes URL of presentation to show
|
|
||||||
* 2. Notes window responds with 'connected' when it is available
|
|
||||||
* 3. This window proceeds to send the current presentation state
|
|
||||||
* to the notes window
|
|
||||||
*/
|
|
||||||
const Plugin = () => {
|
|
||||||
|
|
||||||
let connectInterval;
|
|
||||||
let speakerWindow = null;
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a new speaker view window.
|
|
||||||
*/
|
|
||||||
function openSpeakerWindow() {
|
|
||||||
|
|
||||||
// If a window is already open, focus it
|
|
||||||
if( speakerWindow && !speakerWindow.closed ) {
|
|
||||||
speakerWindow.focus();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
|
|
||||||
speakerWindow.marked = marked;
|
|
||||||
speakerWindow.document.write( speakerViewHTML );
|
|
||||||
|
|
||||||
if( !speakerWindow ) {
|
|
||||||
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reconnect with an existing speaker view window.
|
|
||||||
*/
|
|
||||||
function reconnectSpeakerWindow( reconnectWindow ) {
|
|
||||||
|
|
||||||
if( speakerWindow && !speakerWindow.closed ) {
|
|
||||||
speakerWindow.focus();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
speakerWindow = reconnectWindow;
|
|
||||||
window.addEventListener( 'message', onPostMessage );
|
|
||||||
onConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to the notes window through a postmessage handshake.
|
|
||||||
* Using postmessage enables us to work in situations where the
|
|
||||||
* origins differ, such as a presentation being opened from the
|
|
||||||
* file system.
|
|
||||||
*/
|
|
||||||
function connect() {
|
|
||||||
|
|
||||||
const presentationURL = deck.getConfig().url;
|
|
||||||
|
|
||||||
const url = typeof presentationURL === 'string' ? presentationURL :
|
|
||||||
window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;
|
|
||||||
|
|
||||||
// Keep trying to connect until we get a 'connected' message back
|
|
||||||
connectInterval = setInterval( function() {
|
|
||||||
speakerWindow.postMessage( JSON.stringify( {
|
|
||||||
namespace: 'reveal-notes',
|
|
||||||
type: 'connect',
|
|
||||||
state: deck.getState(),
|
|
||||||
url
|
|
||||||
} ), '*' );
|
|
||||||
}, 500 );
|
|
||||||
|
|
||||||
window.addEventListener( 'message', onPostMessage );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the specified Reveal.js method with the provided argument
|
|
||||||
* and then pushes the result to the notes frame.
|
|
||||||
*/
|
|
||||||
function callRevealApi( methodName, methodArguments, callId ) {
|
|
||||||
|
|
||||||
let result = deck[methodName].apply( deck, methodArguments );
|
|
||||||
speakerWindow.postMessage( JSON.stringify( {
|
|
||||||
namespace: 'reveal-notes',
|
|
||||||
type: 'return',
|
|
||||||
result,
|
|
||||||
callId
|
|
||||||
} ), '*' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts the current slide data to the notes window.
|
|
||||||
*/
|
|
||||||
function post( event ) {
|
|
||||||
|
|
||||||
let slideElement = deck.getCurrentSlide(),
|
|
||||||
notesElements = slideElement.querySelectorAll( 'aside.notes' ),
|
|
||||||
fragmentElement = slideElement.querySelector( '.current-fragment' );
|
|
||||||
|
|
||||||
let messageData = {
|
|
||||||
namespace: 'reveal-notes',
|
|
||||||
type: 'state',
|
|
||||||
notes: '',
|
|
||||||
markdown: false,
|
|
||||||
whitespace: 'normal',
|
|
||||||
state: deck.getState()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Look for notes defined in a slide attribute
|
|
||||||
if( slideElement.hasAttribute( 'data-notes' ) ) {
|
|
||||||
messageData.notes = slideElement.getAttribute( 'data-notes' );
|
|
||||||
messageData.whitespace = 'pre-wrap';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for notes defined in a fragment
|
|
||||||
if( fragmentElement ) {
|
|
||||||
let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
|
|
||||||
if( fragmentNotes ) {
|
|
||||||
messageData.notes = fragmentNotes.innerHTML;
|
|
||||||
messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';
|
|
||||||
|
|
||||||
// Ignore other slide notes
|
|
||||||
notesElements = null;
|
|
||||||
}
|
|
||||||
else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
|
|
||||||
messageData.notes = fragmentElement.getAttribute( 'data-notes' );
|
|
||||||
messageData.whitespace = 'pre-wrap';
|
|
||||||
|
|
||||||
// In case there are slide notes
|
|
||||||
notesElements = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for notes defined in an aside element
|
|
||||||
if( notesElements && notesElements.length ) {
|
|
||||||
// Ignore notes inside of fragments since those are shown
|
|
||||||
// individually when stepping through fragments
|
|
||||||
notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );
|
|
||||||
|
|
||||||
messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );
|
|
||||||
messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given event is from the same origin as the
|
|
||||||
* current window.
|
|
||||||
*/
|
|
||||||
function isSameOriginEvent( event ) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
return window.location.origin === event.source.location.origin;
|
|
||||||
}
|
|
||||||
catch ( error ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPostMessage( event ) {
|
|
||||||
|
|
||||||
// Only allow same-origin messages
|
|
||||||
// (added 12/5/22 as a XSS safeguard)
|
|
||||||
if( isSameOriginEvent( event ) ) {
|
|
||||||
|
|
||||||
let data = JSON.parse( event.data );
|
|
||||||
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
|
|
||||||
clearInterval( connectInterval );
|
|
||||||
onConnected();
|
|
||||||
}
|
|
||||||
else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
|
|
||||||
callRevealApi( data.methodName, data.arguments, data.callId );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called once we have established a connection to the notes
|
|
||||||
* window.
|
|
||||||
*/
|
|
||||||
function onConnected() {
|
|
||||||
|
|
||||||
// Monitor events that trigger a change in state
|
|
||||||
deck.on( 'slidechanged', post );
|
|
||||||
deck.on( 'fragmentshown', post );
|
|
||||||
deck.on( 'fragmenthidden', post );
|
|
||||||
deck.on( 'overviewhidden', post );
|
|
||||||
deck.on( 'overviewshown', post );
|
|
||||||
deck.on( 'paused', post );
|
|
||||||
deck.on( 'resumed', post );
|
|
||||||
|
|
||||||
// Post the initial state
|
|
||||||
post();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'notes',
|
|
||||||
|
|
||||||
init: function( reveal ) {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
|
|
||||||
if( !/receiver/i.test( window.location.search ) ) {
|
|
||||||
|
|
||||||
// If the there's a 'notes' query set, open directly
|
|
||||||
if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
|
|
||||||
openSpeakerWindow();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Keep listening for speaker view hearbeats. If we receive a
|
|
||||||
// heartbeat from an orphaned window, reconnect it. This ensures
|
|
||||||
// that we remain connected to the notes even if the presentation
|
|
||||||
// is reloaded.
|
|
||||||
window.addEventListener( 'message', event => {
|
|
||||||
|
|
||||||
if( !speakerWindow && typeof event.data === 'string' ) {
|
|
||||||
let data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = JSON.parse( event.data );
|
|
||||||
}
|
|
||||||
catch( error ) {}
|
|
||||||
|
|
||||||
if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
|
|
||||||
reconnectSpeakerWindow( event.source );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the notes when the 's' key is hit
|
|
||||||
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
|
|
||||||
openSpeakerWindow();
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
open: openSpeakerWindow
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Plugin;
|
|
@ -1,891 +0,0 @@
|
|||||||
<!--
|
|
||||||
NOTE: You need to build the notes plugin after making changes to this file.
|
|
||||||
-->
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
|
|
||||||
<title>reveal.js - Speaker View</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Helvetica;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#current-slide,
|
|
||||||
#upcoming-slide,
|
|
||||||
#speaker-controls {
|
|
||||||
padding: 6px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#current-slide iframe,
|
|
||||||
#upcoming-slide iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
#current-slide .label,
|
|
||||||
#upcoming-slide .label {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#connection-status {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 20;
|
|
||||||
padding: 30% 20% 20% 20%;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #222;
|
|
||||||
background: #fff;
|
|
||||||
text-align: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-element {
|
|
||||||
height: 34px;
|
|
||||||
line-height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
text-shadow: none;
|
|
||||||
background: rgba( 220, 220, 220, 0.8 );
|
|
||||||
color: #222;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-element.interactive:hover {
|
|
||||||
background: rgba( 220, 220, 220, 1 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#current-slide {
|
|
||||||
position: absolute;
|
|
||||||
width: 60%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upcoming-slide {
|
|
||||||
position: absolute;
|
|
||||||
width: 40%;
|
|
||||||
height: 40%;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speaker controls */
|
|
||||||
#speaker-controls {
|
|
||||||
position: absolute;
|
|
||||||
top: 40%;
|
|
||||||
right: 0;
|
|
||||||
width: 40%;
|
|
||||||
height: 60%;
|
|
||||||
overflow: auto;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time.hidden,
|
|
||||||
.speaker-controls-notes.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .label,
|
|
||||||
.speaker-controls-pace .label,
|
|
||||||
.speaker-controls-notes .label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 0.66em;
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time, .speaker-controls-pace {
|
|
||||||
border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .reset-button {
|
|
||||||
opacity: 0;
|
|
||||||
float: right;
|
|
||||||
color: #666;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.speaker-controls-time:hover .reset-button {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .timer,
|
|
||||||
.speaker-controls-time .clock {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .timer,
|
|
||||||
.speaker-controls-time .clock,
|
|
||||||
.speaker-controls-time .pacing .hours-value,
|
|
||||||
.speaker-controls-time .pacing .minutes-value,
|
|
||||||
.speaker-controls-time .pacing .seconds-value {
|
|
||||||
font-size: 1.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .timer {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .clock {
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time span.mute {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .pacing-title {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .pacing.ahead {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .pacing.on-track {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-time .pacing.behind {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-notes {
|
|
||||||
padding: 10px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-controls-notes .value {
|
|
||||||
margin-top: 5px;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout selector */
|
|
||||||
#speaker-layout {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
color: #222;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
#speaker-layout select {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
font-size: 1em;
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
-moz-appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#speaker-layout select:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speaker layout: Wide */
|
|
||||||
body[data-speaker-layout="wide"] #current-slide,
|
|
||||||
body[data-speaker-layout="wide"] #upcoming-slide {
|
|
||||||
width: 50%;
|
|
||||||
height: 45%;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="wide"] #current-slide {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="wide"] #upcoming-slide {
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="wide"] #speaker-controls {
|
|
||||||
top: 45%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 50%;
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speaker layout: Tall */
|
|
||||||
body[data-speaker-layout="tall"] #current-slide,
|
|
||||||
body[data-speaker-layout="tall"] #upcoming-slide {
|
|
||||||
width: 45%;
|
|
||||||
height: 50%;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="tall"] #current-slide {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="tall"] #upcoming-slide {
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="tall"] #speaker-controls {
|
|
||||||
padding-top: 40px;
|
|
||||||
top: 0;
|
|
||||||
left: 45%;
|
|
||||||
width: 55%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speaker layout: Notes only */
|
|
||||||
body[data-speaker-layout="notes-only"] #current-slide,
|
|
||||||
body[data-speaker-layout="notes-only"] #upcoming-slide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-speaker-layout="notes-only"] #speaker-controls {
|
|
||||||
padding-top: 40px;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1080px) {
|
|
||||||
body[data-speaker-layout="default"] #speaker-controls {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
body[data-speaker-layout="default"] #speaker-controls {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
|
||||||
body[data-speaker-layout="default"] #speaker-controls {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="connection-status">Loading speaker view...</div>
|
|
||||||
|
|
||||||
<div id="current-slide"></div>
|
|
||||||
<div id="upcoming-slide"><span class="overlay-element label">Upcoming</span></div>
|
|
||||||
<div id="speaker-controls">
|
|
||||||
<div class="speaker-controls-time">
|
|
||||||
<h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
|
|
||||||
<div class="clock">
|
|
||||||
<span class="clock-value">0:00 AM</span>
|
|
||||||
</div>
|
|
||||||
<div class="timer">
|
|
||||||
<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
|
|
||||||
</div>
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<h4 class="label pacing-title" style="display: none">Pacing – Time to finish current slide</h4>
|
|
||||||
<div class="pacing" style="display: none">
|
|
||||||
<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="speaker-controls-notes hidden">
|
|
||||||
<h4 class="label">Notes</h4>
|
|
||||||
<div class="value"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="speaker-layout" class="overlay-element interactive">
|
|
||||||
<span class="speaker-layout-label"></span>
|
|
||||||
<select class="speaker-layout-dropdown"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
|
|
||||||
var notes,
|
|
||||||
notesValue,
|
|
||||||
currentState,
|
|
||||||
currentSlide,
|
|
||||||
upcomingSlide,
|
|
||||||
layoutLabel,
|
|
||||||
layoutDropdown,
|
|
||||||
pendingCalls = {},
|
|
||||||
lastRevealApiCallId = 0,
|
|
||||||
connected = false
|
|
||||||
|
|
||||||
var connectionStatus = document.querySelector( '#connection-status' );
|
|
||||||
|
|
||||||
var SPEAKER_LAYOUTS = {
|
|
||||||
'default': 'Default',
|
|
||||||
'wide': 'Wide',
|
|
||||||
'tall': 'Tall',
|
|
||||||
'notes-only': 'Notes only'
|
|
||||||
};
|
|
||||||
|
|
||||||
setupLayout();
|
|
||||||
|
|
||||||
let openerOrigin;
|
|
||||||
|
|
||||||
try {
|
|
||||||
openerOrigin = window.opener.location.origin;
|
|
||||||
}
|
|
||||||
catch ( error ) { console.warn( error ) }
|
|
||||||
|
|
||||||
// In order to prevent XSS, the speaker view will only run if its
|
|
||||||
// opener has the same origin as itself
|
|
||||||
if( window.location.origin !== openerOrigin ) {
|
|
||||||
connectionStatus.innerHTML = 'Cross origin error.<br>The speaker window can only be opened from the same origin.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var connectionTimeout = setTimeout( function() {
|
|
||||||
connectionStatus.innerHTML = 'Error connecting to main window.<br>Please try closing and reopening the speaker view.';
|
|
||||||
}, 5000 );
|
|
||||||
|
|
||||||
window.addEventListener( 'message', function( event ) {
|
|
||||||
|
|
||||||
clearTimeout( connectionTimeout );
|
|
||||||
connectionStatus.style.display = 'none';
|
|
||||||
|
|
||||||
var data = JSON.parse( event.data );
|
|
||||||
|
|
||||||
// The overview mode is only useful to the reveal.js instance
|
|
||||||
// where navigation occurs so we don't sync it
|
|
||||||
if( data.state ) delete data.state.overview;
|
|
||||||
|
|
||||||
// Messages sent by the notes plugin inside of the main window
|
|
||||||
if( data && data.namespace === 'reveal-notes' ) {
|
|
||||||
if( data.type === 'connect' ) {
|
|
||||||
handleConnectMessage( data );
|
|
||||||
}
|
|
||||||
else if( data.type === 'state' ) {
|
|
||||||
handleStateMessage( data );
|
|
||||||
}
|
|
||||||
else if( data.type === 'return' ) {
|
|
||||||
pendingCalls[data.callId](data.result);
|
|
||||||
delete pendingCalls[data.callId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Messages sent by the reveal.js inside of the current slide preview
|
|
||||||
else if( data && data.namespace === 'reveal' ) {
|
|
||||||
if( /ready/.test( data.eventName ) ) {
|
|
||||||
// Send a message back to notify that the handshake is complete
|
|
||||||
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
|
|
||||||
}
|
|
||||||
else if( /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
|
|
||||||
|
|
||||||
dispatchStateToMainWindow( data.state );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the presentation in the main window to match the state
|
|
||||||
* of the presentation in the notes window.
|
|
||||||
*/
|
|
||||||
const dispatchStateToMainWindow = debounce(( state ) => {
|
|
||||||
window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ state ]} ), '*' );
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously calls the Reveal.js API of the main frame.
|
|
||||||
*/
|
|
||||||
function callRevealApi( methodName, methodArguments, callback ) {
|
|
||||||
|
|
||||||
var callId = ++lastRevealApiCallId;
|
|
||||||
pendingCalls[callId] = callback;
|
|
||||||
window.opener.postMessage( JSON.stringify( {
|
|
||||||
namespace: 'reveal-notes',
|
|
||||||
type: 'call',
|
|
||||||
callId: callId,
|
|
||||||
methodName: methodName,
|
|
||||||
arguments: methodArguments
|
|
||||||
} ), '*' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the main window is trying to establish a
|
|
||||||
* connection.
|
|
||||||
*/
|
|
||||||
function handleConnectMessage( data ) {
|
|
||||||
|
|
||||||
if( connected === false ) {
|
|
||||||
connected = true;
|
|
||||||
|
|
||||||
setupIframes( data );
|
|
||||||
setupKeyboard();
|
|
||||||
setupNotes();
|
|
||||||
setupTimer();
|
|
||||||
setupHeartbeat();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the main window sends an updated state.
|
|
||||||
*/
|
|
||||||
function handleStateMessage( data ) {
|
|
||||||
|
|
||||||
// Store the most recently set state to avoid circular loops
|
|
||||||
// applying the same state
|
|
||||||
currentState = JSON.stringify( data.state );
|
|
||||||
|
|
||||||
// No need for updating the notes in case of fragment changes
|
|
||||||
if ( data.notes ) {
|
|
||||||
notes.classList.remove( 'hidden' );
|
|
||||||
notesValue.style.whiteSpace = data.whitespace;
|
|
||||||
if( data.markdown ) {
|
|
||||||
notesValue.innerHTML = marked( data.notes );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
notesValue.innerHTML = data.notes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
notes.classList.add( 'hidden' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the note slides
|
|
||||||
currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
|
|
||||||
upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
|
|
||||||
upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit to max one state update per X ms
|
|
||||||
handleStateMessage = debounce( handleStateMessage, 200 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forward keyboard events to the current slide window.
|
|
||||||
* This enables keyboard events to work even if focus
|
|
||||||
* isn't set on the current slide iframe.
|
|
||||||
*
|
|
||||||
* Block F5 default handling, it reloads and disconnects
|
|
||||||
* the speaker notes window.
|
|
||||||
*/
|
|
||||||
function setupKeyboard() {
|
|
||||||
|
|
||||||
document.addEventListener( 'keydown', function( event ) {
|
|
||||||
if( event.keyCode === 116 || ( event.metaKey && event.keyCode === 82 ) ) {
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the preview iframes.
|
|
||||||
*/
|
|
||||||
function setupIframes( data ) {
|
|
||||||
|
|
||||||
var params = [
|
|
||||||
'receiver',
|
|
||||||
'progress=false',
|
|
||||||
'history=false',
|
|
||||||
'transition=none',
|
|
||||||
'autoSlide=0',
|
|
||||||
'backgroundTransition=none'
|
|
||||||
].join( '&' );
|
|
||||||
|
|
||||||
var urlSeparator = /\?/.test(data.url) ? '&' : '?';
|
|
||||||
var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
|
|
||||||
var currentURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&postMessageEvents=true' + hash;
|
|
||||||
var upcomingURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&controls=false' + hash;
|
|
||||||
|
|
||||||
currentSlide = document.createElement( 'iframe' );
|
|
||||||
currentSlide.setAttribute( 'width', 1280 );
|
|
||||||
currentSlide.setAttribute( 'height', 1024 );
|
|
||||||
currentSlide.setAttribute( 'src', currentURL );
|
|
||||||
document.querySelector( '#current-slide' ).appendChild( currentSlide );
|
|
||||||
|
|
||||||
upcomingSlide = document.createElement( 'iframe' );
|
|
||||||
upcomingSlide.setAttribute( 'width', 640 );
|
|
||||||
upcomingSlide.setAttribute( 'height', 512 );
|
|
||||||
upcomingSlide.setAttribute( 'src', upcomingURL );
|
|
||||||
document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the notes UI.
|
|
||||||
*/
|
|
||||||
function setupNotes() {
|
|
||||||
|
|
||||||
notes = document.querySelector( '.speaker-controls-notes' );
|
|
||||||
notesValue = document.querySelector( '.speaker-controls-notes .value' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We send out a heartbeat at all times to ensure we can
|
|
||||||
* reconnect with the main presentation window after reloads.
|
|
||||||
*/
|
|
||||||
function setupHeartbeat() {
|
|
||||||
|
|
||||||
setInterval( () => {
|
|
||||||
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
|
|
||||||
}, 1000 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimings( callback ) {
|
|
||||||
|
|
||||||
callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {
|
|
||||||
callRevealApi( 'getConfig', [], function ( config ) {
|
|
||||||
var totalTime = config.totalTime;
|
|
||||||
var minTimePerSlide = config.minimumTimePerSlide || 0;
|
|
||||||
var defaultTiming = config.defaultTiming;
|
|
||||||
if ((defaultTiming == null) && (totalTime == null)) {
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Setting totalTime overrides defaultTiming
|
|
||||||
if (totalTime) {
|
|
||||||
defaultTiming = 0;
|
|
||||||
}
|
|
||||||
var timings = [];
|
|
||||||
for ( var i in slideAttributes ) {
|
|
||||||
var slide = slideAttributes[ i ];
|
|
||||||
var timing = defaultTiming;
|
|
||||||
if( slide.hasOwnProperty( 'data-timing' )) {
|
|
||||||
var t = slide[ 'data-timing' ];
|
|
||||||
timing = parseInt(t);
|
|
||||||
if( isNaN(timing) ) {
|
|
||||||
console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming);
|
|
||||||
timing = defaultTiming;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timings.push(timing);
|
|
||||||
}
|
|
||||||
if ( totalTime ) {
|
|
||||||
// After we've allocated time to individual slides, we summarize it and
|
|
||||||
// subtract it from the total time
|
|
||||||
var remainingTime = totalTime - timings.reduce( function(a, b) { return a + b; }, 0 );
|
|
||||||
// The remaining time is divided by the number of slides that have 0 seconds
|
|
||||||
// allocated at the moment, giving the average time-per-slide on the remaining slides
|
|
||||||
var remainingSlides = (timings.filter( function(x) { return x == 0 }) ).length
|
|
||||||
var timePerSlide = Math.round( remainingTime / remainingSlides, 0 )
|
|
||||||
// And now we replace every zero-value timing with that average
|
|
||||||
timings = timings.map( function(x) { return (x==0 ? timePerSlide : x) } );
|
|
||||||
}
|
|
||||||
var slidesUnderMinimum = timings.filter( function(x) { return (x < minTimePerSlide) } ).length
|
|
||||||
if ( slidesUnderMinimum ) {
|
|
||||||
message = "The pacing time for " + slidesUnderMinimum + " slide(s) is under the configured minimum of " + minTimePerSlide + " seconds. Check the data-timing attribute on individual slides, or consider increasing the totalTime or minimumTimePerSlide configuration options (or removing some slides).";
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
callback( timings );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the number of seconds allocated for presenting
|
|
||||||
* all slides up to and including this one.
|
|
||||||
*/
|
|
||||||
function getTimeAllocated( timings, callback ) {
|
|
||||||
|
|
||||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
|
||||||
var allocated = 0;
|
|
||||||
for (var i in timings.slice(0, currentSlide + 1)) {
|
|
||||||
allocated += timings[i];
|
|
||||||
}
|
|
||||||
callback( allocated );
|
|
||||||
} );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the timer and clock and start updating them
|
|
||||||
* at an interval.
|
|
||||||
*/
|
|
||||||
function setupTimer() {
|
|
||||||
|
|
||||||
var start = new Date(),
|
|
||||||
timeEl = document.querySelector( '.speaker-controls-time' ),
|
|
||||||
clockEl = timeEl.querySelector( '.clock-value' ),
|
|
||||||
hoursEl = timeEl.querySelector( '.hours-value' ),
|
|
||||||
minutesEl = timeEl.querySelector( '.minutes-value' ),
|
|
||||||
secondsEl = timeEl.querySelector( '.seconds-value' ),
|
|
||||||
pacingTitleEl = timeEl.querySelector( '.pacing-title' ),
|
|
||||||
pacingEl = timeEl.querySelector( '.pacing' ),
|
|
||||||
pacingHoursEl = pacingEl.querySelector( '.hours-value' ),
|
|
||||||
pacingMinutesEl = pacingEl.querySelector( '.minutes-value' ),
|
|
||||||
pacingSecondsEl = pacingEl.querySelector( '.seconds-value' );
|
|
||||||
|
|
||||||
var timings = null;
|
|
||||||
getTimings( function ( _timings ) {
|
|
||||||
|
|
||||||
timings = _timings;
|
|
||||||
if (_timings !== null) {
|
|
||||||
pacingTitleEl.style.removeProperty('display');
|
|
||||||
pacingEl.style.removeProperty('display');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update once directly
|
|
||||||
_updateTimer();
|
|
||||||
|
|
||||||
// Then update every second
|
|
||||||
setInterval( _updateTimer, 1000 );
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
|
||||||
function _resetTimer() {
|
|
||||||
|
|
||||||
if (timings == null) {
|
|
||||||
start = new Date();
|
|
||||||
_updateTimer();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Reset timer to beginning of current slide
|
|
||||||
getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
|
|
||||||
var slideEndTiming = slideEndTimingSeconds * 1000;
|
|
||||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
|
||||||
var currentSlideTiming = timings[currentSlide] * 1000;
|
|
||||||
var previousSlidesTiming = slideEndTiming - currentSlideTiming;
|
|
||||||
var now = new Date();
|
|
||||||
start = new Date(now.getTime() - previousSlidesTiming);
|
|
||||||
_updateTimer();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
timeEl.addEventListener( 'click', function() {
|
|
||||||
_resetTimer();
|
|
||||||
return false;
|
|
||||||
} );
|
|
||||||
|
|
||||||
function _displayTime( hrEl, minEl, secEl, time) {
|
|
||||||
|
|
||||||
var sign = Math.sign(time) == -1 ? "-" : "";
|
|
||||||
time = Math.abs(Math.round(time / 1000));
|
|
||||||
var seconds = time % 60;
|
|
||||||
var minutes = Math.floor( time / 60 ) % 60 ;
|
|
||||||
var hours = Math.floor( time / ( 60 * 60 )) ;
|
|
||||||
hrEl.innerHTML = sign + zeroPadInteger( hours );
|
|
||||||
if (hours == 0) {
|
|
||||||
hrEl.classList.add( 'mute' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
hrEl.classList.remove( 'mute' );
|
|
||||||
}
|
|
||||||
minEl.innerHTML = ':' + zeroPadInteger( minutes );
|
|
||||||
if (hours == 0 && minutes == 0) {
|
|
||||||
minEl.classList.add( 'mute' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
minEl.classList.remove( 'mute' );
|
|
||||||
}
|
|
||||||
secEl.innerHTML = ':' + zeroPadInteger( seconds );
|
|
||||||
}
|
|
||||||
|
|
||||||
function _updateTimer() {
|
|
||||||
|
|
||||||
var diff, hours, minutes, seconds,
|
|
||||||
now = new Date();
|
|
||||||
|
|
||||||
diff = now.getTime() - start.getTime();
|
|
||||||
|
|
||||||
clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
|
|
||||||
_displayTime( hoursEl, minutesEl, secondsEl, diff );
|
|
||||||
if (timings !== null) {
|
|
||||||
_updatePacing(diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function _updatePacing(diff) {
|
|
||||||
|
|
||||||
getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
|
|
||||||
var slideEndTiming = slideEndTimingSeconds * 1000;
|
|
||||||
|
|
||||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
|
||||||
var currentSlideTiming = timings[currentSlide] * 1000;
|
|
||||||
var timeLeftCurrentSlide = slideEndTiming - diff;
|
|
||||||
if (timeLeftCurrentSlide < 0) {
|
|
||||||
pacingEl.className = 'pacing behind';
|
|
||||||
}
|
|
||||||
else if (timeLeftCurrentSlide < currentSlideTiming) {
|
|
||||||
pacingEl.className = 'pacing on-track';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
pacingEl.className = 'pacing ahead';
|
|
||||||
}
|
|
||||||
_displayTime( pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the speaker view layout and layout selector.
|
|
||||||
*/
|
|
||||||
function setupLayout() {
|
|
||||||
|
|
||||||
layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
|
|
||||||
layoutLabel = document.querySelector( '.speaker-layout-label' );
|
|
||||||
|
|
||||||
// Render the list of available layouts
|
|
||||||
for( var id in SPEAKER_LAYOUTS ) {
|
|
||||||
var option = document.createElement( 'option' );
|
|
||||||
option.setAttribute( 'value', id );
|
|
||||||
option.textContent = SPEAKER_LAYOUTS[ id ];
|
|
||||||
layoutDropdown.appendChild( option );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor the dropdown for changes
|
|
||||||
layoutDropdown.addEventListener( 'change', function( event ) {
|
|
||||||
|
|
||||||
setLayout( layoutDropdown.value );
|
|
||||||
|
|
||||||
}, false );
|
|
||||||
|
|
||||||
// Restore any currently persisted layout
|
|
||||||
setLayout( getLayout() );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a new speaker view layout. The layout is persisted
|
|
||||||
* in local storage.
|
|
||||||
*/
|
|
||||||
function setLayout( value ) {
|
|
||||||
|
|
||||||
var title = SPEAKER_LAYOUTS[ value ];
|
|
||||||
|
|
||||||
layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
|
|
||||||
layoutDropdown.value = value;
|
|
||||||
|
|
||||||
document.body.setAttribute( 'data-speaker-layout', value );
|
|
||||||
|
|
||||||
// Persist locally
|
|
||||||
if( supportsLocalStorage() ) {
|
|
||||||
window.localStorage.setItem( 'reveal-speaker-layout', value );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ID of the most recently set speaker layout
|
|
||||||
* or our default layout if none has been set.
|
|
||||||
*/
|
|
||||||
function getLayout() {
|
|
||||||
|
|
||||||
if( supportsLocalStorage() ) {
|
|
||||||
var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
|
|
||||||
if( layout ) {
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to the first record in the layouts hash
|
|
||||||
for( var id in SPEAKER_LAYOUTS ) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function supportsLocalStorage() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
localStorage.setItem('test', 'test');
|
|
||||||
localStorage.removeItem('test');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch( e ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function zeroPadInteger( num ) {
|
|
||||||
|
|
||||||
var str = '00' + parseInt( num );
|
|
||||||
return str.substring( str.length - 2 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limits the frequency at which a function can be called.
|
|
||||||
*/
|
|
||||||
function debounce( fn, ms ) {
|
|
||||||
|
|
||||||
var lastTime = 0,
|
|
||||||
timeout;
|
|
||||||
|
|
||||||
return function() {
|
|
||||||
|
|
||||||
var args = arguments;
|
|
||||||
var context = this;
|
|
||||||
|
|
||||||
clearTimeout( timeout );
|
|
||||||
|
|
||||||
var timeSinceLastCall = Date.now() - lastTime;
|
|
||||||
if( timeSinceLastCall > ms ) {
|
|
||||||
fn.apply( context, args );
|
|
||||||
lastTime = Date.now();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
timeout = setTimeout( function() {
|
|
||||||
fn.apply( context, args );
|
|
||||||
lastTime = Date.now();
|
|
||||||
}, ms - timeSinceLastCall );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,243 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
|
||||||
* by navigatating to that slide and highlighting it.
|
|
||||||
*
|
|
||||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Plugin = () => {
|
|
||||||
|
|
||||||
// The reveal.js instance this plugin is attached to
|
|
||||||
let deck;
|
|
||||||
|
|
||||||
let searchElement;
|
|
||||||
let searchButton;
|
|
||||||
let searchInput;
|
|
||||||
|
|
||||||
let matchedSlides;
|
|
||||||
let currentMatchedIndex;
|
|
||||||
let searchboxDirty;
|
|
||||||
let hilitor;
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
|
|
||||||
searchElement = document.createElement( 'div' );
|
|
||||||
searchElement.classList.add( 'searchbox' );
|
|
||||||
searchElement.style.position = 'absolute';
|
|
||||||
searchElement.style.top = '10px';
|
|
||||||
searchElement.style.right = '10px';
|
|
||||||
searchElement.style.zIndex = 10;
|
|
||||||
|
|
||||||
//embedded base64 search icon Designed by Sketchdock - http://www.sketchdock.com/:
|
|
||||||
searchElement.innerHTML = `<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>
|
|
||||||
</span>`;
|
|
||||||
|
|
||||||
searchInput = searchElement.querySelector( '.searchinput' );
|
|
||||||
searchInput.style.width = '240px';
|
|
||||||
searchInput.style.fontSize = '14px';
|
|
||||||
searchInput.style.padding = '4px 6px';
|
|
||||||
searchInput.style.color = '#000';
|
|
||||||
searchInput.style.background = '#fff';
|
|
||||||
searchInput.style.borderRadius = '2px';
|
|
||||||
searchInput.style.border = '0';
|
|
||||||
searchInput.style.outline = '0';
|
|
||||||
searchInput.style.boxShadow = '0 2px 18px rgba(0, 0, 0, 0.2)';
|
|
||||||
searchInput.style['-webkit-appearance'] = 'none';
|
|
||||||
|
|
||||||
deck.getRevealElement().appendChild( searchElement );
|
|
||||||
|
|
||||||
// searchButton.addEventListener( 'click', function(event) {
|
|
||||||
// doSearch();
|
|
||||||
// }, false );
|
|
||||||
|
|
||||||
searchInput.addEventListener( 'keyup', function( event ) {
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case 13:
|
|
||||||
event.preventDefault();
|
|
||||||
doSearch();
|
|
||||||
searchboxDirty = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
searchboxDirty = true;
|
|
||||||
}
|
|
||||||
}, false );
|
|
||||||
|
|
||||||
closeSearch();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSearch() {
|
|
||||||
if( !searchElement ) render();
|
|
||||||
|
|
||||||
searchElement.style.display = 'inline';
|
|
||||||
searchInput.focus();
|
|
||||||
searchInput.select();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSearch() {
|
|
||||||
if( !searchElement ) render();
|
|
||||||
|
|
||||||
searchElement.style.display = 'none';
|
|
||||||
if(hilitor) hilitor.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSearch() {
|
|
||||||
if( !searchElement ) render();
|
|
||||||
|
|
||||||
if (searchElement.style.display !== 'inline') {
|
|
||||||
openSearch();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
closeSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch() {
|
|
||||||
//if there's been a change in the search term, perform a new search:
|
|
||||||
if (searchboxDirty) {
|
|
||||||
var searchstring = searchInput.value;
|
|
||||||
|
|
||||||
if (searchstring === '') {
|
|
||||||
if(hilitor) hilitor.remove();
|
|
||||||
matchedSlides = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//find the keyword amongst the slides
|
|
||||||
hilitor = new Hilitor("slidecontent");
|
|
||||||
matchedSlides = hilitor.apply(searchstring);
|
|
||||||
currentMatchedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedSlides) {
|
|
||||||
//navigate to the next slide that has the keyword, wrapping to the first if necessary
|
|
||||||
if (matchedSlides.length && (matchedSlides.length <= currentMatchedIndex)) {
|
|
||||||
currentMatchedIndex = 0;
|
|
||||||
}
|
|
||||||
if (matchedSlides.length > currentMatchedIndex) {
|
|
||||||
deck.slide(matchedSlides[currentMatchedIndex].h, matchedSlides[currentMatchedIndex].v);
|
|
||||||
currentMatchedIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original JavaScript code by Chirp Internet: www.chirp.com.au
|
|
||||||
// Please acknowledge use of this code by including this header.
|
|
||||||
// 2/2013 jon: modified regex to display any match, not restricted to word boundaries.
|
|
||||||
function Hilitor(id, tag) {
|
|
||||||
|
|
||||||
var targetNode = document.getElementById(id) || document.body;
|
|
||||||
var hiliteTag = tag || "EM";
|
|
||||||
var skipTags = new RegExp("^(?:" + hiliteTag + "|SCRIPT|FORM)$");
|
|
||||||
var colors = ["#ff6", "#a0ffff", "#9f9", "#f99", "#f6f"];
|
|
||||||
var wordColor = [];
|
|
||||||
var colorIdx = 0;
|
|
||||||
var matchRegex = "";
|
|
||||||
var matchingSlides = [];
|
|
||||||
|
|
||||||
this.setRegex = function(input)
|
|
||||||
{
|
|
||||||
input = input.trim();
|
|
||||||
matchRegex = new RegExp("(" + input + ")","i");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getRegex = function()
|
|
||||||
{
|
|
||||||
return matchRegex.toString().replace(/^\/\\b\(|\)\\b\/i$/g, "").replace(/\|/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursively apply word highlighting
|
|
||||||
this.hiliteWords = function(node)
|
|
||||||
{
|
|
||||||
if(node == undefined || !node) return;
|
|
||||||
if(!matchRegex) return;
|
|
||||||
if(skipTags.test(node.nodeName)) return;
|
|
||||||
|
|
||||||
if(node.hasChildNodes()) {
|
|
||||||
for(var i=0; i < node.childNodes.length; i++)
|
|
||||||
this.hiliteWords(node.childNodes[i]);
|
|
||||||
}
|
|
||||||
if(node.nodeType == 3) { // NODE_TEXT
|
|
||||||
var nv, regs;
|
|
||||||
if((nv = node.nodeValue) && (regs = matchRegex.exec(nv))) {
|
|
||||||
//find the slide's section element and save it in our list of matching slides
|
|
||||||
var secnode = node;
|
|
||||||
while (secnode != null && secnode.nodeName != 'SECTION') {
|
|
||||||
secnode = secnode.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
var slideIndex = deck.getIndices(secnode);
|
|
||||||
var slidelen = matchingSlides.length;
|
|
||||||
var alreadyAdded = false;
|
|
||||||
for (var i=0; i < slidelen; i++) {
|
|
||||||
if ( (matchingSlides[i].h === slideIndex.h) && (matchingSlides[i].v === slideIndex.v) ) {
|
|
||||||
alreadyAdded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (! alreadyAdded) {
|
|
||||||
matchingSlides.push(slideIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!wordColor[regs[0].toLowerCase()]) {
|
|
||||||
wordColor[regs[0].toLowerCase()] = colors[colorIdx++ % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = document.createElement(hiliteTag);
|
|
||||||
match.appendChild(document.createTextNode(regs[0]));
|
|
||||||
match.style.backgroundColor = wordColor[regs[0].toLowerCase()];
|
|
||||||
match.style.fontStyle = "inherit";
|
|
||||||
match.style.color = "#000";
|
|
||||||
|
|
||||||
var after = node.splitText(regs.index);
|
|
||||||
after.nodeValue = after.nodeValue.substring(regs[0].length);
|
|
||||||
node.parentNode.insertBefore(match, after);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove highlighting
|
|
||||||
this.remove = function()
|
|
||||||
{
|
|
||||||
var arr = document.getElementsByTagName(hiliteTag);
|
|
||||||
var el;
|
|
||||||
while(arr.length && (el = arr[0])) {
|
|
||||||
el.parentNode.replaceChild(el.firstChild, el);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// start highlighting at target node
|
|
||||||
this.apply = function(input)
|
|
||||||
{
|
|
||||||
if(input == undefined || !input) return;
|
|
||||||
this.remove();
|
|
||||||
this.setRegex(input);
|
|
||||||
this.hiliteWords(targetNode);
|
|
||||||
return matchingSlides;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
id: 'search',
|
|
||||||
|
|
||||||
init: reveal => {
|
|
||||||
|
|
||||||
deck = reveal;
|
|
||||||
deck.registerKeyboardShortcut( 'CTRL + Shift + F', 'Search' );
|
|
||||||
|
|
||||||
document.addEventListener( 'keydown', function( event ) {
|
|
||||||
if( event.key == "F" && (event.ctrlKey || event.metaKey) ) { //Control+Shift+f
|
|
||||||
event.preventDefault();
|
|
||||||
toggleSearch();
|
|
||||||
}
|
|
||||||
}, false );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
open: openSearch
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Plugin;
|
|
@ -1,7 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
|
||||||
* by navigatating to that slide and highlighting it.
|
|
||||||
*
|
|
||||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
|
||||||
*/
|
|
||||||
const e=()=>{let e,t,n,l,i,o,r;function s(){t=document.createElement("div"),t.classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>\n\t\t</span>',n=t.querySelector(".searchinput"),n.style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){if(13===t.keyCode)t.preventDefault(),function(){if(o){var t=n.value;""===t?(r&&r.remove(),l=null):(r=new c("slidecontent"),l=r.apply(t),i=0)}l&&(l.length&&l.length<=i&&(i=0),l.length>i&&(e.slide(l[i].h,l[i].v),i++))}(),o=!1;else o=!0}),!1),d()}function a(){t||s(),t.style.display="inline",n.focus(),n.select()}function d(){t||s(),t.style.display="none",r&&r.remove()}function c(t,n){var l=document.getElementById(t)||document.body,i=n||"EM",o=new RegExp("^(?:"+i+"|SCRIPT|FORM)$"),r=["#ff6","#a0ffff","#9f9","#f99","#f6f"],s=[],a=0,d="",c=[];this.setRegex=function(e){e=e.trim(),d=new RegExp("("+e+")","i")},this.getRegex=function(){return d.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&d&&!o.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n<t.childNodes.length;n++)this.hiliteWords(t.childNodes[n]);var l,p;if(3==t.nodeType)if((l=t.nodeValue)&&(p=d.exec(l))){for(var u=t;null!=u&&"SECTION"!=u.nodeName;)u=u.parentNode;var h=e.getIndices(u),f=c.length,y=!1;for(n=0;n<f;n++)c[n].h===h.h&&c[n].v===h.v&&(y=!0);y||c.push(h),s[p[0].toLowerCase()]||(s[p[0].toLowerCase()]=r[a++%r.length]);var g=document.createElement(i);g.appendChild(document.createTextNode(p[0])),g.style.backgroundColor=s[p[0].toLowerCase()],g.style.fontStyle="inherit",g.style.color="#000";var v=t.splitText(p.index);v.nodeValue=v.nodeValue.substring(p[0].length),t.parentNode.insertBefore(g,v)}}},this.remove=function(){for(var e,t=document.getElementsByTagName(i);t.length&&(e=t[0]);)e.parentNode.replaceChild(e.firstChild,e)},this.apply=function(e){if(null!=e&&e)return this.remove(),this.setRegex(e),this.hiliteWords(l),c}}return{id:"search",init:n=>{e=n,e.registerKeyboardShortcut("CTRL + Shift + F","Search"),document.addEventListener("keydown",(function(e){"F"==e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),t||s(),"inline"!==t.style.display?a():d())}),!1)},open:a}};export{e as default};
|
|
@ -1,7 +0,0 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealSearch=t()}(this,(function(){"use strict";
|
|
||||||
/*!
|
|
||||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
|
||||||
* by navigatating to that slide and highlighting it.
|
|
||||||
*
|
|
||||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
|
||||||
*/return()=>{let e,t,n,i,o,l,r;function s(){t=document.createElement("div"),t.classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>\n\t\t</span>',n=t.querySelector(".searchinput"),n.style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){if(13===t.keyCode)t.preventDefault(),function(){if(l){var t=n.value;""===t?(r&&r.remove(),i=null):(r=new c("slidecontent"),i=r.apply(t),o=0)}i&&(i.length&&i.length<=o&&(o=0),i.length>o&&(e.slide(i[o].h,i[o].v),o++))}(),l=!1;else l=!0}),!1),d()}function a(){t||s(),t.style.display="inline",n.focus(),n.select()}function d(){t||s(),t.style.display="none",r&&r.remove()}function c(t,n){var i=document.getElementById(t)||document.body,o=n||"EM",l=new RegExp("^(?:"+o+"|SCRIPT|FORM)$"),r=["#ff6","#a0ffff","#9f9","#f99","#f6f"],s=[],a=0,d="",c=[];this.setRegex=function(e){e=e.trim(),d=new RegExp("("+e+")","i")},this.getRegex=function(){return d.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&d&&!l.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n<t.childNodes.length;n++)this.hiliteWords(t.childNodes[n]);var i,f;if(3==t.nodeType)if((i=t.nodeValue)&&(f=d.exec(i))){for(var u=t;null!=u&&"SECTION"!=u.nodeName;)u=u.parentNode;var p=e.getIndices(u),h=c.length,y=!1;for(n=0;n<h;n++)c[n].h===p.h&&c[n].v===p.v&&(y=!0);y||c.push(p),s[f[0].toLowerCase()]||(s[f[0].toLowerCase()]=r[a++%r.length]);var g=document.createElement(o);g.appendChild(document.createTextNode(f[0])),g.style.backgroundColor=s[f[0].toLowerCase()],g.style.fontStyle="inherit",g.style.color="#000";var v=t.splitText(f.index);v.nodeValue=v.nodeValue.substring(f[0].length),t.parentNode.insertBefore(g,v)}}},this.remove=function(){for(var e,t=document.getElementsByTagName(o);t.length&&(e=t[0]);)e.parentNode.replaceChild(e.firstChild,e)},this.apply=function(e){if(null!=e&&e)return this.remove(),this.setRegex(e),this.hiliteWords(i),c}}return{id:"search",init:n=>{e=n,e.registerKeyboardShortcut("CTRL + Shift + F","Search"),document.addEventListener("keydown",(function(e){"F"==e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),t||s(),"inline"!==t.style.display?a():d())}),!1)},open:a}}}));
|
|
@ -1,264 +0,0 @@
|
|||||||
/*!
|
|
||||||
* reveal.js Zoom plugin
|
|
||||||
*/
|
|
||||||
const Plugin = {
|
|
||||||
|
|
||||||
id: 'zoom',
|
|
||||||
|
|
||||||
init: function( reveal ) {
|
|
||||||
|
|
||||||
reveal.getRevealElement().addEventListener( 'mousedown', function( event ) {
|
|
||||||
var defaultModifier = /Linux/.test( window.navigator.platform ) ? 'ctrl' : 'alt';
|
|
||||||
|
|
||||||
var modifier = ( reveal.getConfig().zoomKey ? reveal.getConfig().zoomKey : defaultModifier ) + 'Key';
|
|
||||||
var zoomLevel = ( reveal.getConfig().zoomLevel ? reveal.getConfig().zoomLevel : 2 );
|
|
||||||
|
|
||||||
if( event[ modifier ] && !reveal.isOverview() ) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
zoom.to({
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
scale: zoomLevel,
|
|
||||||
pan: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: () => {
|
|
||||||
|
|
||||||
zoom.reset();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default () => Plugin;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* zoom.js 0.3 (modified for use with reveal.js)
|
|
||||||
* http://lab.hakim.se/zoom-js
|
|
||||||
* MIT licensed
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
|
||||||
*/
|
|
||||||
var zoom = (function(){
|
|
||||||
|
|
||||||
// The current zoom level (scale)
|
|
||||||
var level = 1;
|
|
||||||
|
|
||||||
// The current mouse position, used for panning
|
|
||||||
var mouseX = 0,
|
|
||||||
mouseY = 0;
|
|
||||||
|
|
||||||
// Timeout before pan is activated
|
|
||||||
var panEngageTimeout = -1,
|
|
||||||
panUpdateInterval = -1;
|
|
||||||
|
|
||||||
// Check for transform support so that we can fallback otherwise
|
|
||||||
var supportsTransforms = 'transform' in document.body.style;
|
|
||||||
|
|
||||||
if( supportsTransforms ) {
|
|
||||||
// The easing that will be applied when we zoom in/out
|
|
||||||
document.body.style.transition = 'transform 0.8s ease';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zoom out if the user hits escape
|
|
||||||
document.addEventListener( 'keyup', function( event ) {
|
|
||||||
if( level !== 1 && event.keyCode === 27 ) {
|
|
||||||
zoom.out();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Monitor mouse movement for panning
|
|
||||||
document.addEventListener( 'mousemove', function( event ) {
|
|
||||||
if( level !== 1 ) {
|
|
||||||
mouseX = event.clientX;
|
|
||||||
mouseY = event.clientY;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the CSS required to zoom in, prefers the use of CSS3
|
|
||||||
* transforms but falls back on zoom for IE.
|
|
||||||
*
|
|
||||||
* @param {Object} rect
|
|
||||||
* @param {Number} scale
|
|
||||||
*/
|
|
||||||
function magnify( rect, scale ) {
|
|
||||||
|
|
||||||
var scrollOffset = getScrollOffset();
|
|
||||||
|
|
||||||
// Ensure a width/height is set
|
|
||||||
rect.width = rect.width || 1;
|
|
||||||
rect.height = rect.height || 1;
|
|
||||||
|
|
||||||
// Center the rect within the zoomed viewport
|
|
||||||
rect.x -= ( window.innerWidth - ( rect.width * scale ) ) / 2;
|
|
||||||
rect.y -= ( window.innerHeight - ( rect.height * scale ) ) / 2;
|
|
||||||
|
|
||||||
if( supportsTransforms ) {
|
|
||||||
// Reset
|
|
||||||
if( scale === 1 ) {
|
|
||||||
document.body.style.transform = '';
|
|
||||||
}
|
|
||||||
// Scale
|
|
||||||
else {
|
|
||||||
var origin = scrollOffset.x +'px '+ scrollOffset.y +'px',
|
|
||||||
transform = 'translate('+ -rect.x +'px,'+ -rect.y +'px) scale('+ scale +')';
|
|
||||||
|
|
||||||
document.body.style.transformOrigin = origin;
|
|
||||||
document.body.style.transform = transform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Reset
|
|
||||||
if( scale === 1 ) {
|
|
||||||
document.body.style.position = '';
|
|
||||||
document.body.style.left = '';
|
|
||||||
document.body.style.top = '';
|
|
||||||
document.body.style.width = '';
|
|
||||||
document.body.style.height = '';
|
|
||||||
document.body.style.zoom = '';
|
|
||||||
}
|
|
||||||
// Scale
|
|
||||||
else {
|
|
||||||
document.body.style.position = 'relative';
|
|
||||||
document.body.style.left = ( - ( scrollOffset.x + rect.x ) / scale ) + 'px';
|
|
||||||
document.body.style.top = ( - ( scrollOffset.y + rect.y ) / scale ) + 'px';
|
|
||||||
document.body.style.width = ( scale * 100 ) + '%';
|
|
||||||
document.body.style.height = ( scale * 100 ) + '%';
|
|
||||||
document.body.style.zoom = scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
level = scale;
|
|
||||||
|
|
||||||
if( document.documentElement.classList ) {
|
|
||||||
if( level !== 1 ) {
|
|
||||||
document.documentElement.classList.add( 'zoomed' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.documentElement.classList.remove( 'zoomed' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pan the document when the mosue cursor approaches the edges
|
|
||||||
* of the window.
|
|
||||||
*/
|
|
||||||
function pan() {
|
|
||||||
var range = 0.12,
|
|
||||||
rangeX = window.innerWidth * range,
|
|
||||||
rangeY = window.innerHeight * range,
|
|
||||||
scrollOffset = getScrollOffset();
|
|
||||||
|
|
||||||
// Up
|
|
||||||
if( mouseY < rangeY ) {
|
|
||||||
window.scroll( scrollOffset.x, scrollOffset.y - ( 1 - ( mouseY / rangeY ) ) * ( 14 / level ) );
|
|
||||||
}
|
|
||||||
// Down
|
|
||||||
else if( mouseY > window.innerHeight - rangeY ) {
|
|
||||||
window.scroll( scrollOffset.x, scrollOffset.y + ( 1 - ( window.innerHeight - mouseY ) / rangeY ) * ( 14 / level ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left
|
|
||||||
if( mouseX < rangeX ) {
|
|
||||||
window.scroll( scrollOffset.x - ( 1 - ( mouseX / rangeX ) ) * ( 14 / level ), scrollOffset.y );
|
|
||||||
}
|
|
||||||
// Right
|
|
||||||
else if( mouseX > window.innerWidth - rangeX ) {
|
|
||||||
window.scroll( scrollOffset.x + ( 1 - ( window.innerWidth - mouseX ) / rangeX ) * ( 14 / level ), scrollOffset.y );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScrollOffset() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX !== undefined ? window.scrollX : window.pageXOffset,
|
|
||||||
y: window.scrollY !== undefined ? window.scrollY : window.pageYOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Zooms in on either a rectangle or HTML element.
|
|
||||||
*
|
|
||||||
* @param {Object} options
|
|
||||||
* - element: HTML element to zoom in on
|
|
||||||
* OR
|
|
||||||
* - x/y: coordinates in non-transformed space to zoom in on
|
|
||||||
* - width/height: the portion of the screen to zoom in on
|
|
||||||
* - scale: can be used instead of width/height to explicitly set scale
|
|
||||||
*/
|
|
||||||
to: function( options ) {
|
|
||||||
|
|
||||||
// Due to an implementation limitation we can't zoom in
|
|
||||||
// to another element without zooming out first
|
|
||||||
if( level !== 1 ) {
|
|
||||||
zoom.out();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
options.x = options.x || 0;
|
|
||||||
options.y = options.y || 0;
|
|
||||||
|
|
||||||
// If an element is set, that takes precedence
|
|
||||||
if( !!options.element ) {
|
|
||||||
// Space around the zoomed in element to leave on screen
|
|
||||||
var padding = 20;
|
|
||||||
var bounds = options.element.getBoundingClientRect();
|
|
||||||
|
|
||||||
options.x = bounds.left - padding;
|
|
||||||
options.y = bounds.top - padding;
|
|
||||||
options.width = bounds.width + ( padding * 2 );
|
|
||||||
options.height = bounds.height + ( padding * 2 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// If width/height values are set, calculate scale from those values
|
|
||||||
if( options.width !== undefined && options.height !== undefined ) {
|
|
||||||
options.scale = Math.max( Math.min( window.innerWidth / options.width, window.innerHeight / options.height ), 1 );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( options.scale > 1 ) {
|
|
||||||
options.x *= options.scale;
|
|
||||||
options.y *= options.scale;
|
|
||||||
|
|
||||||
magnify( options, options.scale );
|
|
||||||
|
|
||||||
if( options.pan !== false ) {
|
|
||||||
|
|
||||||
// Wait with engaging panning as it may conflict with the
|
|
||||||
// zoom transition
|
|
||||||
panEngageTimeout = setTimeout( function() {
|
|
||||||
panUpdateInterval = setInterval( pan, 1000 / 60 );
|
|
||||||
}, 800 );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the document zoom state to its default.
|
|
||||||
*/
|
|
||||||
out: function() {
|
|
||||||
clearTimeout( panEngageTimeout );
|
|
||||||
clearInterval( panUpdateInterval );
|
|
||||||
|
|
||||||
magnify( { x: 0, y: 0 }, 1 );
|
|
||||||
|
|
||||||
level = 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Alias
|
|
||||||
magnify: function( options ) { this.to( options ) },
|
|
||||||
reset: function() { this.out() },
|
|
||||||
|
|
||||||
zoomLevel: function() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
@ -1,11 +0,0 @@
|
|||||||
/*!
|
|
||||||
* reveal.js Zoom plugin
|
|
||||||
*/
|
|
||||||
const e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(t){var n=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:n)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;t[i]&&!e.isOverview()&&(t.preventDefault(),o.to({x:t.clientX,y:t.clientY,scale:d,pan:!1}))}))},destroy:()=>{o.reset()}};var t=()=>e,o=function(){var e=1,t=0,n=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,o){var n=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*o)/2,t.y-=(window.innerHeight-t.height*o)/2,l)if(1===o)document.body.style.transform="";else{var i=n.x+"px "+n.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+o+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===o?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(n.x+t.x)/o+"px",document.body.style.top=-(n.y+t.y)/o+"px",document.body.style.width=100*o+"%",document.body.style.height=100*o+"%",document.body.style.zoom=o);e=o,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var o=.12*window.innerWidth,i=.12*window.innerHeight,d=r();n<i?window.scroll(d.x,d.y-14/e*(1-n/i)):n>window.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-n)/i)*(14/e)),t<o?window.scroll(d.x-14/e*(1-t/o),d.y):t>window.innerWidth-o&&window.scroll(d.x+(1-(window.innerWidth-t)/o)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(t){1!==e&&27===t.keyCode&&o.out()})),document.addEventListener("mousemove",(function(o){1!==e&&(t=o.clientX,n=o.clientY)})),{to:function(t){if(1!==e)o.out();else{if(t.x=t.x||0,t.y=t.y||0,t.element){var n=t.element.getBoundingClientRect();t.x=n.left-20,t.y=n.top-20,t.width=n.width+40,t.height=n.height+40}void 0!==t.width&&void 0!==t.height&&(t.scale=Math.max(Math.min(window.innerWidth/t.width,window.innerHeight/t.height),1)),t.scale>1&&(t.x*=t.scale,t.y*=t.scale,s(t,t.scale),!1!==t.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();
|
|
||||||
/*!
|
|
||||||
* zoom.js 0.3 (modified for use with reveal.js)
|
|
||||||
* http://lab.hakim.se/zoom-js
|
|
||||||
* MIT licensed
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
|
||||||
*/export{t as default};
|
|
@ -1,11 +0,0 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealZoom=t()}(this,(function(){"use strict";
|
|
||||||
/*!
|
|
||||||
* reveal.js Zoom plugin
|
|
||||||
*/const e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(o){var n=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:n)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;o[i]&&!e.isOverview()&&(o.preventDefault(),t.to({x:o.clientX,y:o.clientY,scale:d,pan:!1}))}))},destroy:()=>{t.reset()}};var t=function(){var e=1,o=0,n=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,o){var n=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*o)/2,t.y-=(window.innerHeight-t.height*o)/2,l)if(1===o)document.body.style.transform="";else{var i=n.x+"px "+n.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+o+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===o?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(n.x+t.x)/o+"px",document.body.style.top=-(n.y+t.y)/o+"px",document.body.style.width=100*o+"%",document.body.style.height=100*o+"%",document.body.style.zoom=o);e=o,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var t=.12*window.innerWidth,i=.12*window.innerHeight,d=r();n<i?window.scroll(d.x,d.y-14/e*(1-n/i)):n>window.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-n)/i)*(14/e)),o<t?window.scroll(d.x-14/e*(1-o/t),d.y):o>window.innerWidth-t&&window.scroll(d.x+(1-(window.innerWidth-o)/t)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(o){1!==e&&27===o.keyCode&&t.out()})),document.addEventListener("mousemove",(function(t){1!==e&&(o=t.clientX,n=t.clientY)})),{to:function(o){if(1!==e)t.out();else{if(o.x=o.x||0,o.y=o.y||0,o.element){var n=o.element.getBoundingClientRect();o.x=n.left-20,o.y=n.top-20,o.width=n.width+40,o.height=n.height+40}void 0!==o.width&&void 0!==o.height&&(o.scale=Math.max(Math.min(window.innerWidth/o.width,window.innerHeight/o.height),1)),o.scale>1&&(o.x*=o.scale,o.y*=o.scale,s(o,o.scale),!1!==o.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();
|
|
||||||
/*!
|
|
||||||
* zoom.js 0.3 (modified for use with reveal.js)
|
|
||||||
* http://lab.hakim.se/zoom-js
|
|
||||||
* MIT licensed
|
|
||||||
*
|
|
||||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
|
||||||
*/return()=>e}));
|
|
7
slidev/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
index.html
|
||||||
|
.remote-assets
|
||||||
|
components.d.ts
|
3
slidev/.npmrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# for pnpm
|
||||||
|
shamefully-hoist=true
|
||||||
|
auto-install-peers=true
|
11
slidev/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Welcome to [Slidev](https://github.com/slidevjs/slidev)!
|
||||||
|
|
||||||
|
To start the slide show:
|
||||||
|
|
||||||
|
- `npm install`
|
||||||
|
- `npm run dev`
|
||||||
|
- visit http://localhost:3030
|
||||||
|
|
||||||
|
Edit the [slides.md](./slides.md) to see the changes.
|
||||||
|
|
||||||
|
Learn more about Slidev on [documentations](https://sli.dev/).
|
37
slidev/components/Counter.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
count: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const counter = ref(props.count)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div flex="~" w="min" border="~ main rounded-md">
|
||||||
|
<button
|
||||||
|
border="r main"
|
||||||
|
p="2"
|
||||||
|
font="mono"
|
||||||
|
outline="!none"
|
||||||
|
hover:bg="gray-400 opacity-20"
|
||||||
|
@click="counter -= 1"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span m="auto" p="2">{{ counter }}</span>
|
||||||
|
<button
|
||||||
|
border="l main"
|
||||||
|
p="2"
|
||||||
|
font="mono"
|
||||||
|
outline="!none"
|
||||||
|
hover:bg="gray-400 opacity-20"
|
||||||
|
@click="counter += 1"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
16
slidev/netlify.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[build]
|
||||||
|
publish = "dist"
|
||||||
|
command = "npm run build"
|
||||||
|
|
||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "18"
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/.well-known/*"
|
||||||
|
to = "/.well-known/:splat"
|
||||||
|
status = 200
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
14329
slidev/package-lock.json
generated
Normal file
16
slidev/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "east_africa_report",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "slidev build",
|
||||||
|
"dev": "slidev --open",
|
||||||
|
"export": "slidev export"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@slidev/cli": "^0.46.3",
|
||||||
|
"@slidev/theme-default": "latest",
|
||||||
|
"@slidev/theme-seriph": "latest",
|
||||||
|
"slidev-theme-zhozhoba": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
27
slidev/pages/multiple-entries.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Multiple Entries
|
||||||
|
|
||||||
|
You can split your slides.md into multiple files and organize them as you want using the `src` attribute.
|
||||||
|
|
||||||
|
#### `slides.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Page 1
|
||||||
|
|
||||||
|
Page 2 from main entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
src: ./subpage.md
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### `subpage.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Page 2
|
||||||
|
|
||||||
|
Page 2 from another file.
|
||||||
|
```
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/guide/syntax.html#multiple-entries)
|
444
slidev/slides.md
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
---
|
||||||
|
theme: seriph
|
||||||
|
background: https://source.unsplash.com/collection/94734566/1920x1080
|
||||||
|
class: text-center
|
||||||
|
highlighter: shikiji
|
||||||
|
lineNumbers: false
|
||||||
|
info: |
|
||||||
|
## Slidev Starter Template
|
||||||
|
Presentation slides for developers.
|
||||||
|
|
||||||
|
Learn more at [Sli.dev](https://sli.dev)
|
||||||
|
drawings:
|
||||||
|
persist: false
|
||||||
|
transition: slide-left
|
||||||
|
title: Welcome to Slidev
|
||||||
|
mdc: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Welcome to Slidev
|
||||||
|
|
||||||
|
Presentation slides for developers
|
||||||
|
|
||||||
|
<div class="pt-12">
|
||||||
|
<span @click="$slidev.nav.next" class="px-2 py-1 rounded cursor-pointer" hover="bg-white bg-opacity-10">
|
||||||
|
Press Space for next page <carbon:arrow-right class="inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="abs-br m-6 flex gap-2">
|
||||||
|
<button @click="$slidev.nav.openInEditor()" title="Open in Editor" class="text-xl slidev-icon-btn opacity-50 !border-none !hover:text-white">
|
||||||
|
<carbon:edit />
|
||||||
|
</button>
|
||||||
|
<a href="https://github.com/slidevjs/slidev" target="_blank" alt="GitHub" title="Open in GitHub"
|
||||||
|
class="text-xl slidev-icon-btn opacity-50 !border-none !hover:text-white">
|
||||||
|
<carbon-logo-github />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
transition: fade-out
|
||||||
|
---
|
||||||
|
|
||||||
|
# What is Slidev?
|
||||||
|
|
||||||
|
Slidev is a slides maker and presenter designed for developers, consist of the following features
|
||||||
|
|
||||||
|
- 📝 **Text-based** - focus on the content with Markdown, and then style them later
|
||||||
|
- 🎨 **Themable** - theme can be shared and used with npm packages
|
||||||
|
- 🧑💻 **Developer Friendly** - code highlighting, live coding with autocompletion
|
||||||
|
- 🤹 **Interactive** - embedding Vue components to enhance your expressions
|
||||||
|
- 🎥 **Recording** - built-in recording and camera view
|
||||||
|
- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
|
||||||
|
- 🛠 **Hackable** - anything possible on a webpage
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Read more about [Why Slidev?](https://sli.dev/guide/why)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
You can have `style` tag in markdown to override the style for the current page.
|
||||||
|
Learn more: https://sli.dev/guide/syntax#embedded-styles
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
background-color: #2B90B6;
|
||||||
|
background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%);
|
||||||
|
background-size: 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-moz-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
-moz-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Here is another comment.
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
|
# Table of contents
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Toc minDepth="1" maxDepth="1"></Toc>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Toc maxDepth="1"></Toc>
|
||||||
|
|
||||||
|
---
|
||||||
|
transition: slide-up
|
||||||
|
level: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
|
||||||
|
Hover on the bottom-left corner to see the navigation's controls panel, [learn more](https://sli.dev/guide/navigation.html)
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| <kbd>right</kbd> / <kbd>space</kbd>| next animation or slide |
|
||||||
|
| <kbd>left</kbd> / <kbd>shift</kbd><kbd>space</kbd> | previous animation or slide |
|
||||||
|
| <kbd>up</kbd> | previous slide |
|
||||||
|
| <kbd>down</kbd> | next slide |
|
||||||
|
|
||||||
|
<!-- https://sli.dev/guide/animations.html#click-animations -->
|
||||||
|
<img
|
||||||
|
v-click
|
||||||
|
class="absolute -bottom-9 -left-7 w-80 opacity-50"
|
||||||
|
src="https://sli.dev/assets/arrow-bottom-left.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<p v-after class="absolute bottom-23 left-45 opacity-30 transform -rotate-10">Here!</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: image-right
|
||||||
|
image: https://source.unsplash.com/collection/94734566/1920x1080
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code
|
||||||
|
|
||||||
|
Use code snippets and get the highlighting directly, and even types hover![^1]
|
||||||
|
|
||||||
|
```ts {all|5|1-6|9|all} twoslash
|
||||||
|
// TwoSlash enables TypeScript hover information and errors in markdown code blocks
|
||||||
|
// Learn more at https://www.typescriptlang.org/dev/twoslash/
|
||||||
|
function getUser(id: number): User {
|
||||||
|
return undefined as any
|
||||||
|
}
|
||||||
|
function saveUser(id: number, user: User) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
// ---cut---
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
role: string
|
||||||
|
// ^?
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUser(id: number, update: User) {
|
||||||
|
const user = getUser(id)
|
||||||
|
const newUser = { ...user, ...update }
|
||||||
|
saveUser(id, newUser)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<arrow v-click="[3, 4]" x1="400" y1="420" x2="230" y2="330" color="#564" width="3" arrowSize="1" />
|
||||||
|
|
||||||
|
[^1]: [Learn More](https://sli.dev/guide/syntax.html#line-highlighting)
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.footnotes-sep {
|
||||||
|
@apply mt-20 opacity-10;
|
||||||
|
}
|
||||||
|
.footnotes {
|
||||||
|
@apply text-sm opacity-75;
|
||||||
|
}
|
||||||
|
.footnote-backref {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
<div grid="~ cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
You can use Vue components directly inside your slides.
|
||||||
|
|
||||||
|
We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Counter :count="10" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- ./components/Counter.vue -->
|
||||||
|
<Counter :count="10" m="t-4" />
|
||||||
|
|
||||||
|
Check out [the guides](https://sli.dev/builtin/components.html) for more.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Tweet id="1390115482657726468" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tweet id="1390115482657726468" scale="0.65" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Presenter note with **bold**, *italic*, and ~~striked~~ text.
|
||||||
|
|
||||||
|
Also, HTML elements are valid:
|
||||||
|
<div class="flex w-full">
|
||||||
|
<span style="flex-grow: 1;">Left content</span>
|
||||||
|
<span>Right content</span>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
class: px-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Themes
|
||||||
|
|
||||||
|
Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switching between themes by just **one edit** in your frontmatter:
|
||||||
|
|
||||||
|
<div grid="~ cols-2 gap-2" m="t-2">
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
theme: default
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
theme: seriph
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-default/01.png?raw=true" alt="">
|
||||||
|
|
||||||
|
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-seriph/01.png?raw=true" alt="">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Read more about [How to use a theme](https://sli.dev/themes/use.html) and
|
||||||
|
check out the [Awesome Themes Gallery](https://sli.dev/themes/gallery.html).
|
||||||
|
|
||||||
|
---
|
||||||
|
preload: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Animations
|
||||||
|
|
||||||
|
Animations are powered by [@vueuse/motion](https://motion.vueuse.org/).
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: -80 }"
|
||||||
|
:enter="{ x: 0 }">
|
||||||
|
Slidev
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="w-60 relative mt-6">
|
||||||
|
<div class="relative w-40 h-40">
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
src="https://sli.dev/logo-square.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ y: 500, x: -100, scale: 2 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
src="https://sli.dev/logo-circle.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
src="https://sli.dev/logo-triangle.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-5xl absolute top-14 left-40 text-[#2B90B6] -z-1"
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: -80, opacity: 0}"
|
||||||
|
:enter="{ x: 0, opacity: 1, transition: { delay: 2000, duration: 1000 } }">
|
||||||
|
Slidev
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- vue script setup scripts can be directly used in markdown, and will only affects current page -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const final = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotate: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
type: 'spring',
|
||||||
|
damping: 10,
|
||||||
|
stiffness: 20,
|
||||||
|
mass: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-motion
|
||||||
|
:initial="{ x:35, y: 40, opacity: 0}"
|
||||||
|
:enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">
|
||||||
|
|
||||||
|
[Learn More](https://sli.dev/guide/animations.html#motion)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# LaTeX
|
||||||
|
|
||||||
|
LaTeX is supported out-of-box powered by [KaTeX](https://katex.org/).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Inline $\sqrt{3x-1}+(1+x)^2$
|
||||||
|
|
||||||
|
Block
|
||||||
|
$$ {1|3|all}
|
||||||
|
\begin{array}{c}
|
||||||
|
|
||||||
|
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
|
||||||
|
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
|
||||||
|
|
||||||
|
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
|
||||||
|
|
||||||
|
\nabla \cdot \vec{\mathbf{B}} & = 0
|
||||||
|
|
||||||
|
\end{array}
|
||||||
|
$$
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/guide/syntax#latex)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Diagrams
|
||||||
|
|
||||||
|
You can create diagrams / graphs from textual descriptions, directly in your Markdown.
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 gap-5 pt-4 -mb-6">
|
||||||
|
|
||||||
|
```mermaid {scale: 0.5, alt: 'A simple sequence diagram'}
|
||||||
|
sequenceDiagram
|
||||||
|
Alice->John: Hello John, how are you?
|
||||||
|
Note over Alice,John: A typical interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid {theme: 'neutral', scale: 0.8}
|
||||||
|
graph TD
|
||||||
|
B[Text] --> C{Decision}
|
||||||
|
C -->|One| D[Result 1]
|
||||||
|
C -->|Two| E[Result 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
Origins
|
||||||
|
Long history
|
||||||
|
::icon(fa fa-book)
|
||||||
|
Popularisation
|
||||||
|
British popular psychology author Tony Buzan
|
||||||
|
Research
|
||||||
|
On effectivness<br/>and features
|
||||||
|
On Automatic creation
|
||||||
|
Uses
|
||||||
|
Creative techniques
|
||||||
|
Strategic planning
|
||||||
|
Argument mapping
|
||||||
|
Tools
|
||||||
|
Pen and paper
|
||||||
|
Mermaid
|
||||||
|
```
|
||||||
|
|
||||||
|
```plantuml {scale: 0.7}
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
package "Some Group" {
|
||||||
|
HTTP - [First Component]
|
||||||
|
[Another Component]
|
||||||
|
}
|
||||||
|
|
||||||
|
node "Other Groups" {
|
||||||
|
FTP - [Second Component]
|
||||||
|
[First Component] --> FTP
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud {
|
||||||
|
[Example 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
database "MySql" {
|
||||||
|
folder "This is my folder" {
|
||||||
|
[Folder 3]
|
||||||
|
}
|
||||||
|
frame "Foo" {
|
||||||
|
[Frame 4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Another Component] --> [Example 1]
|
||||||
|
[Example 1] --> [Folder 3]
|
||||||
|
[Folder 3] --> [Frame 4]
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[Learn More](https://sli.dev/guide/syntax.html#diagrams)
|
||||||
|
|
||||||
|
---
|
||||||
|
src: ./pages/multiple-entries.md
|
||||||
|
hide: false
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: center
|
||||||
|
class: text-center
|
||||||
|
---
|
||||||
|
|
||||||
|
# Learn More
|
||||||
|
|
||||||
|
[Documentations](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/showcases.html)
|
7
slidev/vercel.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [
|
||||||
|
{ "source": "/(.*)", "destination": "/index.html" }
|
||||||
|
],
|
||||||
|
"buildCommand": "npm run build",
|
||||||
|
"outputDirectory": "dist"
|
||||||
|
}
|