After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 4.9 MiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 176 KiB |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 177 KiB |
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 203 KiB |
@ -0,0 +1,15 @@ |
|||||
|
module.exports = { |
||||
|
env: { |
||||
|
browser: true, |
||||
|
es2021: true |
||||
|
}, |
||||
|
extends: [ |
||||
|
'standard' |
||||
|
], |
||||
|
parserOptions: { |
||||
|
ecmaVersion: 12, |
||||
|
sourceType: 'module' |
||||
|
}, |
||||
|
rules: { |
||||
|
} |
||||
|
} |
@ -0,0 +1,150 @@ |
|||||
|
{ |
||||
|
"banana": { |
||||
|
"type": "fromPhysicsEditor", |
||||
|
"label": "banana", |
||||
|
"isStatic": false, |
||||
|
"density": 0.1, |
||||
|
"restitution": 0.1, |
||||
|
"friction": 0.1, |
||||
|
"frictionAir": 0.01, |
||||
|
"frictionStatic": 0.2, |
||||
|
"collisionFilter": { |
||||
|
"group": 0, |
||||
|
"category": 1, |
||||
|
"mask": 255 |
||||
|
}, |
||||
|
"fixtures": [ |
||||
|
{ |
||||
|
"label": "banana-fixture", |
||||
|
"isSensor": false, |
||||
|
"vertices": [ |
||||
|
[ { "x":107, "y":2 }, { "x":95.26110076904297, "y":0.6820602416992188 }, { "x":89, "y":6 }, { "x":94.2380142211914, "y":23.872116088867188 }, { "x":104, "y":22 } ], |
||||
|
[ { "x":4, "y":94 }, { "x":1, "y":105 }, { "x":23.985790252685547, "y":119.92895221710205 }, { "x":50, "y":128 }, { "x":27, "y":94 } ], |
||||
|
[ { "x":27, "y":94 }, { "x":50, "y":128 }, { "x":79, "y":123 }, { "x":102, "y":110 }, { "x":67, "y":83 } ], |
||||
|
[ { "x":67, "y":83 }, { "x":102, "y":110 }, { "x":118.56483459472656, "y":88.44049835205078 }, { "x":126, "y":66 }, { "x":123, "y":40 }, { "x":91, "y":48 } ], |
||||
|
[ { "x":123, "y":40 }, { "x":104, "y":22 }, { "x":94.2380142211914, "y":23.872116088867188 }, { "x":91, "y":48 } ] |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"cherries": { |
||||
|
"type": "fromPhysicsEditor", |
||||
|
"label": "cherries", |
||||
|
"isStatic": false, |
||||
|
"density": 0.10000000149011612, |
||||
|
"restitution": 0.1, |
||||
|
"friction": 0.10000000149011612, |
||||
|
"frictionAir": 0.009999999776482582, |
||||
|
"frictionStatic": 0.5, |
||||
|
"collisionFilter": { |
||||
|
"group": 0, |
||||
|
"category": 1, |
||||
|
"mask": 255 |
||||
|
}, |
||||
|
"fixtures": [ |
||||
|
{ |
||||
|
"label": "cherry-left", |
||||
|
"isSensor": false, |
||||
|
"circle": { |
||||
|
"x": 38.67625899280576, |
||||
|
"y": 100.92661870503598, |
||||
|
"radius": 25.16224146143038 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"label": "cherry-right", |
||||
|
"isSensor": false, |
||||
|
"circle": { |
||||
|
"x": 97.79568345323742, |
||||
|
"y": 102.95251798561152, |
||||
|
"radius": 25.16224146143038 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"label": "cherry-stem", |
||||
|
"isSensor": false, |
||||
|
"vertices": [ |
||||
|
[ { "x":7, "y":8 }, { "x":32, "y":35 }, { "x":61, "y":36 }, { "x":85.0877685546875, "y":32.59856414794922 }, { "x":77, "y":19 }, { "x":58, "y":5 } ], |
||||
|
[ { "x":85, "y":13 }, { "x":77, "y":19 }, { "x":85.0877685546875, "y":32.59856414794922 }, { "x":93, "y":22 } ], |
||||
|
[ { "x":85.0877685546875, "y":32.59856414794922 }, { "x":61, "y":36 }, { "x":43, "y":58 }, { "x":41, "y":75 }, { "x":84.16690826416016, "y":53.778419494628906 } ], |
||||
|
[ { "x":90, "y":78 }, { "x":84.16690826416016, "y":53.778419494628906 }, { "x":41, "y":75 } ] |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"crate": { |
||||
|
"type": "fromPhysicsEditor", |
||||
|
"label": "create", |
||||
|
"isStatic": false, |
||||
|
"density": 0.7999999999999999, |
||||
|
"restitution": 0.1, |
||||
|
"friction": 0.10000000149011612, |
||||
|
"frictionAir": 0.009999999776482582, |
||||
|
"frictionStatic": 0.5, |
||||
|
"collisionFilter": { |
||||
|
"group": 0, |
||||
|
"category": 1, |
||||
|
"mask": 255 |
||||
|
}, |
||||
|
"fixtures": [ |
||||
|
{ |
||||
|
"label": "crate-fixture", |
||||
|
"isSensor": false, |
||||
|
"vertices": [ |
||||
|
[ { "x":5, "y":9 }, { "x":5.115452766418457, "y":36.0355224609375 }, { "x":8.980462074279785, "y":36.376556396484375 }, { "x":120.61101531982422, "y":38.309059143066406 }, { "x":124, "y":36 }, { "x":124, "y":9 } ], |
||||
|
[ { "x":8, "y":128 }, { "x":123, "y":128 }, { "x":123, "y":105 }, { "x":120.15630340576172, "y":104.46891593933105 }, { "x":10.79928970336914, "y":103.67317962646484 }, { "x":7.73001766204834, "y":104.92362403869629 } ], |
||||
|
[ { "x":120.15630340576172, "y":104.46891593933105 }, { "x":120.61101531982422, "y":38.309059143066406 }, { "x":8.980462074279785, "y":36.376556396484375 }, { "x":10.79928970336914, "y":103.67317962646484 } ] |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"orange": { |
||||
|
"type": "fromPhysicsEditor", |
||||
|
"label": "orange", |
||||
|
"isStatic": false, |
||||
|
"density": 0.1, |
||||
|
"restitution": 0.1, |
||||
|
"friction": 0.10000000149011612, |
||||
|
"frictionAir": 0.009999999776482582, |
||||
|
"frictionStatic": 0.5, |
||||
|
"collisionFilter": { |
||||
|
"group": 0, |
||||
|
"category": 1, |
||||
|
"mask": 255 |
||||
|
}, |
||||
|
"fixtures": [ |
||||
|
{ |
||||
|
"label": "orange-fixture", |
||||
|
"isSensor": false, |
||||
|
"circle": { |
||||
|
"x": 64.46043165467627, |
||||
|
"y": 67.0388489208633, |
||||
|
"radius": 61.12368896735685 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"ground": { |
||||
|
"type": "fromPhysicsEditor", |
||||
|
"label": "ground", |
||||
|
"isStatic": true, |
||||
|
"density": 0, |
||||
|
"restitution": 0, |
||||
|
"friction": 0.10000000149011612, |
||||
|
"frictionAir": 0.009999999776482582, |
||||
|
"frictionStatic": 0.5, |
||||
|
"collisionFilter": { |
||||
|
"group": 0, |
||||
|
"category": 1, |
||||
|
"mask": 255 |
||||
|
}, |
||||
|
"fixtures": [ |
||||
|
{ |
||||
|
"label": "", |
||||
|
"isSensor": false, |
||||
|
"vertices": [ |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
@ -0,0 +1,146 @@ |
|||||
|
{ |
||||
|
"textures": [ |
||||
|
{ |
||||
|
"image": "fruit-sprites.png", |
||||
|
"format": "RGBA8888", |
||||
|
"size": { |
||||
|
"w": 1380, |
||||
|
"h": 1202 |
||||
|
}, |
||||
|
"scale": 1, |
||||
|
"frames": [ |
||||
|
{ |
||||
|
"filename": "background", |
||||
|
"rotated": true, |
||||
|
"trimmed": false, |
||||
|
"sourceSize": { |
||||
|
"w": 1200, |
||||
|
"h": 960 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 0, |
||||
|
"y": 0, |
||||
|
"w": 1200, |
||||
|
"h": 960 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 1, |
||||
|
"y": 1, |
||||
|
"w": 1200, |
||||
|
"h": 960 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"filename": "ground", |
||||
|
"rotated": true, |
||||
|
"trimmed": true, |
||||
|
"sourceSize": { |
||||
|
"w": 1200, |
||||
|
"h": 626 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 0, |
||||
|
"y": 339, |
||||
|
"w": 1200, |
||||
|
"h": 287 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 963, |
||||
|
"y": 1, |
||||
|
"w": 1200, |
||||
|
"h": 287 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"filename": "banana", |
||||
|
"rotated": false, |
||||
|
"trimmed": true, |
||||
|
"sourceSize": { |
||||
|
"w": 128, |
||||
|
"h": 128 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 0, |
||||
|
"y": 0, |
||||
|
"w": 127, |
||||
|
"h": 128 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 1252, |
||||
|
"y": 1, |
||||
|
"w": 127, |
||||
|
"h": 128 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"filename": "orange", |
||||
|
"rotated": false, |
||||
|
"trimmed": true, |
||||
|
"sourceSize": { |
||||
|
"w": 128, |
||||
|
"h": 128 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 1, |
||||
|
"y": 5, |
||||
|
"w": 127, |
||||
|
"h": 123 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 1252, |
||||
|
"y": 131, |
||||
|
"w": 127, |
||||
|
"h": 123 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"filename": "cherries", |
||||
|
"rotated": true, |
||||
|
"trimmed": true, |
||||
|
"sourceSize": { |
||||
|
"w": 128, |
||||
|
"h": 128 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 5, |
||||
|
"y": 3, |
||||
|
"w": 120, |
||||
|
"h": 125 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 1252, |
||||
|
"y": 256, |
||||
|
"w": 120, |
||||
|
"h": 125 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"filename": "crate", |
||||
|
"rotated": false, |
||||
|
"trimmed": true, |
||||
|
"sourceSize": { |
||||
|
"w": 128, |
||||
|
"h": 128 |
||||
|
}, |
||||
|
"spriteSourceSize": { |
||||
|
"x": 3, |
||||
|
"y": 7, |
||||
|
"w": 123, |
||||
|
"h": 121 |
||||
|
}, |
||||
|
"frame": { |
||||
|
"x": 1252, |
||||
|
"y": 378, |
||||
|
"w": 123, |
||||
|
"h": 121 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
], |
||||
|
"meta": { |
||||
|
"app": "https://www.codeandweb.com/texturepacker", |
||||
|
"version": "3.0", |
||||
|
"smartupdate": "$TexturePacker:SmartUpdate:0d0bce26c10ae31fb56fffa698e98974:1f8e7cbe61cf17088e7b8ba8bd089be7:25f385b2bb08c04649fd253213a50a44$" |
||||
|
} |
||||
|
} |
After Width: | Height: | Size: 75 KiB |
@ -0,0 +1,30 @@ |
|||||
|
/* http://meyerweb.com/eric/tools/css/reset/ |
||||
|
v4.0 | 20180602 |
||||
|
License: none (public domain) |
||||
|
*/ |
||||
|
|
||||
|
html, body, div, span, applet, object, iframe, |
||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre, |
||||
|
a, abbr, acronym, address, big, cite, code, |
||||
|
del, dfn, em, img, ins, kbd, q, s, samp, |
||||
|
small, strike, strong, sub, sup, tt, var, |
||||
|
b, u, i, center, |
||||
|
dl, dt, dd, ol, ul, li, |
||||
|
fieldset, form, label, legend, |
||||
|
table, caption, tbody, tfoot, thead, tr, th, td, |
||||
|
article, aside, canvas, details, embed, |
||||
|
figure, figcaption, footer, header, hgroup, |
||||
|
main, menu, nav, output, ruby, section, summary, |
||||
|
time, mark, audio, video { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
border: 0; |
||||
|
font-size: 100%; |
||||
|
font: inherit; |
||||
|
vertical-align: baseline; |
||||
|
} |
||||
|
/* HTML5 display-role reset for older browsers */ |
||||
|
article, aside, details, figcaption, figure, |
||||
|
footer, header, hgroup, main, menu, nav, section { |
||||
|
display: block; |
||||
|
} |
@ -0,0 +1,264 @@ |
|||||
|
/** |
||||
|
* 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 |
||||
|
*/ |
||||
|
@font-face { |
||||
|
font-family: 'NimbusSanL-ReguItal'; |
||||
|
src: url('fonts/nimbus-sans-l/NimbusSanL-ReguItal.ttf') format('truetype'); |
||||
|
font-weight: 400; |
||||
|
font-style: normal; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
section.has-dark-background, section.has-dark-background h1, section.has-dark-background h2, section.has-dark-background h3, section.has-dark-background h4, section.has-dark-background h5, section.has-dark-background h6 { |
||||
|
color: #fff; } |
||||
|
|
||||
|
/********************************************* |
||||
|
* GLOBAL STYLES |
||||
|
*********************************************/ |
||||
|
:root { |
||||
|
--background-color: #211aff; |
||||
|
--main-font: "NimbusSanL-ReguItal", sans-serif; |
||||
|
--main-font-size: 40px; |
||||
|
--main-color: #000; |
||||
|
--block-margin: 20px; |
||||
|
--heading-margin: 0 0 20px 0; |
||||
|
--heading-font: "NimbusSanL-ReguItal", Impact, sans-serif; |
||||
|
--heading-color: #000; |
||||
|
--heading-line-height: 1.2; |
||||
|
--heading-letter-spacing: normal; |
||||
|
--heading-text-transform: none; |
||||
|
--heading-text-shadow: none; |
||||
|
--heading-font-weight: normal; |
||||
|
--heading1-text-shadow: none; |
||||
|
--heading1-size: 3.77em; |
||||
|
--heading2-size: 2.11em; |
||||
|
--heading3-size: 1.55em; |
||||
|
--heading4-size: 1em; |
||||
|
--code-font: monospace; |
||||
|
--link-color: #00008B; |
||||
|
--link-color-hover: #0000f1; |
||||
|
--selection-background-color: rgba(0, 0, 0, 0.99); |
||||
|
--selection-color: #fff; } |
||||
|
|
||||
|
.reveal-viewport { |
||||
|
background: none; |
||||
|
background-color: transparent; } |
||||
|
|
||||
|
.reveal { |
||||
|
font-family: "NimbusSanL-ReguItal", sans-serif; |
||||
|
font-size: 40px; |
||||
|
font-weight: normal; |
||||
|
color: #fff; } |
||||
|
|
||||
|
.reveal ::selection { |
||||
|
color: #fff; |
||||
|
background: #0000f1; |
||||
|
text-shadow: none; } |
||||
|
|
||||
|
.reveal ::-moz-selection { |
||||
|
color: #fff; |
||||
|
background: #0000f1; |
||||
|
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: 0 0 20px 0; |
||||
|
color: #000; |
||||
|
font-family: "News Cycle", Impact, sans-serif; |
||||
|
font-weight: normal; |
||||
|
line-height: 1.2; |
||||
|
letter-spacing: normal; |
||||
|
text-transform: none; |
||||
|
text-shadow: none; |
||||
|
word-wrap: break-word; } |
||||
|
|
||||
|
.reveal h1 { |
||||
|
font-size: 3.77em; } |
||||
|
|
||||
|
.reveal h2 { |
||||
|
font-size: 2.11em; } |
||||
|
|
||||
|
.reveal h3 { |
||||
|
font-size: 1.55em; } |
||||
|
|
||||
|
.reveal h4 { |
||||
|
font-size: 1em; } |
||||
|
|
||||
|
.reveal h1 { |
||||
|
text-shadow: none; } |
||||
|
|
||||
|
/********************************************* |
||||
|
* OTHER |
||||
|
*********************************************/ |
||||
|
.reveal p { |
||||
|
margin: 20px 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; } |
||||
|
|
||||
|
/********************************************* |
||||
|
* LINKS |
||||
|
*********************************************/ |
||||
|
.reveal a { |
||||
|
color: #00008B; |
||||
|
text-decoration: none; |
||||
|
transition: color .15s ease; } |
||||
|
|
||||
|
.reveal a:hover { |
||||
|
color: #0000f1; |
||||
|
text-shadow: none; |
||||
|
border: none; } |
||||
|
|
||||
|
.reveal .roll span:after { |
||||
|
color: #fff; |
||||
|
background: #00003f; } |
||||
|
|
||||
|
/********************************************* |
||||
|
* Frame helper |
||||
|
*********************************************/ |
||||
|
.reveal .r-frame { |
||||
|
border: 4px solid #000; |
||||
|
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: #00008B; |
||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } |
||||
|
|
||||
|
/********************************************* |
||||
|
* NAVIGATION CONTROLS |
||||
|
*********************************************/ |
||||
|
.reveal .controls { |
||||
|
color: black; |
||||
|
} |
||||
|
.navigate-left, .navigate-right{ |
||||
|
background-color: rgba(255, 228, 4, 1) !important; |
||||
|
opacity: 0.9 !important; |
||||
|
} |
||||
|
|
||||
|
/********************************************* |
||||
|
* PROGRESS BAR |
||||
|
*********************************************/ |
||||
|
.reveal .progress { |
||||
|
background: rgba(0, 0, 0, 0.2); |
||||
|
color: rgba(255, 228, 4, 0.79);; |
||||
|
height: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
/********************************************* |
||||
|
* BITNIK SPECIFIC |
||||
|
*********************************************/ |
||||
|
|
||||
|
.typed-cursor { |
||||
|
color: black; |
||||
|
opacity: 1; |
||||
|
animation: blink 0.7s infinite; |
||||
|
padding-left: 0.5rem; |
||||
|
padding-right: 0.3rem; |
||||
|
} |
||||
|
|
||||
|
@keyframes blink { |
||||
|
0% { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
section { |
||||
|
width: 80%; |
||||
|
} |
||||
|
|
||||
|
.typed-content { |
||||
|
visibility: hidden; |
||||
|
} |
||||
|
|
||||
|
.typed-insert, .typed-cursor { |
||||
|
max-width: 80%; |
||||
|
font-size: 2rem; |
||||
|
color: black; |
||||
|
background-color: rgba(255, 228, 4, 0.79); |
||||
|
} |
||||
|
|
||||
|
canvas { |
||||
|
background: transparent; |
||||
|
width: 100%; |
||||
|
} |
@ -0,0 +1,114 @@ |
|||||
|
<!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>Follow the Rider ƪ(ړײ)ƪ</title> |
||||
|
|
||||
|
<link rel="stylesheet" href="dist/reset.css"> |
||||
|
<link rel="stylesheet" href="dist/reveal.css"> |
||||
|
<link rel="stylesheet" href="dist/theme/bitnik.css" id="theme"> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div class="reveal" id="game"> |
||||
|
<div class="slides"> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Follow the Rider ƪ(ړײ)ƪ</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>by 💻 !Mediengruppe Bitnik </p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Instructions for a Dérive ⚡💥</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"><p>You will experience 👠 the city through the movements of other e-scooter riders 💳🏴</p></div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"><p>Give up your autonomy by choosing to follow an other rider.🙅🏽<br> Do this in |
||||
|
stealth mode</p></div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Take the e-scooter 🛠</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Follow the next person you see on an e-scooter 💊 inconspicuously. Keep distance, but don't lose your lead</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Let the rider lead 📡 you through Berlin. Let them be your guide </p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>The tour ends when the rider stops and parks their e-scooter.<br>🕹 Follow a new rider if your ride was too short</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Document 📞 your journey with images, videos, voice and text messages 📸</p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Send them to the dedicated telegram group <a href="https://t.me/followtherider">@followtherider</a> </p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>Stay safe! Have fun. 💥 Start now! Keep us up2date. 🌐💄 </p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="typed-content"> |
||||
|
<p>🧠 Telegram <a href="https://t.me/followtherider">@followtherider</a></p> |
||||
|
</div> |
||||
|
<span class="typed-insert"></span> |
||||
|
</section> |
||||
|
|
||||
|
|
||||
|
</div> |
||||
|
<!--<button class="navigate-next">Next</button>--> |
||||
|
</div> |
||||
|
<script src="dist/reveal.js"></script> |
||||
|
<script src="dist/phaser.min.js"></script> |
||||
|
<script src="main.js"></script> |
||||
|
|
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -0,0 +1,165 @@ |
|||||
|
/** |
||||
|
* 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 ); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,293 @@ |
|||||
|
/** |
||||
|
* 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, |
||||
|
|
||||
|
// 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 'foucsed', we will only capture keyboard events
|
||||
|
// for embdedded 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 autolaying 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, |
||||
|
|
||||
|
// 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, |
||||
|
|
||||
|
// Script dependencies to load
|
||||
|
dependencies: [], |
||||
|
|
||||
|
// Plugin objects to register and use for this presentation
|
||||
|
plugins: [] |
||||
|
|
||||
|
} |
@ -0,0 +1,619 @@ |
|||||
|
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(); |
||||
|
|
||||
|
// Ensure that both slides are auto-animate targets
|
||||
|
if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' ) ) { |
||||
|
|
||||
|
// 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
|
||||
|
let allSlides = this.Reveal.getSlides(); |
||||
|
animationOptions.slideDirection = allSlides.indexOf( toSlide ) > allSlides.indexOf( fromSlide ) ? 'forward' : 'backward'; |
||||
|
|
||||
|
// 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++ ); |
||||
|
} ); |
||||
|
|
||||
|
// 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 { |
||||
|
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'; |
||||
|
|
||||
|
// Eplicit 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-line[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 pimaryIndex = 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][ pimaryIndex ] ) { |
||||
|
fromElement = fromMatches[key][ pimaryIndex ]; |
||||
|
fromMatches[key][ pimaryIndex ] = 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 considerd |
||||
|
* 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; |
||||
|
|
||||
|
}, [] ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,397 @@ |
|||||
|
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() { |
||||
|
|
||||
|
let printMode = this.Reveal.isPrintingPDF(); |
||||
|
|
||||
|
// 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 ) { |
||||
|
|
||||
|
let element = slide.slideBackgroundElement, |
||||
|
contentElement = slide.slideBackgroundContentElement; |
||||
|
|
||||
|
// 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 = ''; |
||||
|
|
||||
|
let 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' ), |
||||
|
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), |
||||
|
backgroundPosition: slide.getAttribute( 'data-background-position' ), |
||||
|
backgroundTransition: slide.getAttribute( 'data-background-transition' ), |
||||
|
backgroundOpacity: slide.getAttribute( 'data-background-opacity' ) |
||||
|
}; |
||||
|
|
||||
|
if( data.background ) { |
||||
|
// Auto-wrap image urls in url(...)
|
||||
|
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\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.backgroundImage || data.backgroundVideo || data.backgroundIframe ) { |
||||
|
element.setAttribute( 'data-background-hash', data.background + |
||||
|
data.backgroundSize + |
||||
|
data.backgroundImage + |
||||
|
data.backgroundVideo + |
||||
|
data.backgroundIframe + |
||||
|
data.backgroundColor + |
||||
|
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.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); |
||||
|
|
||||
|
if( slide.hasAttribute( 'data-preload' ) ) 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; |
||||
|
|
||||
|
// 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 = data.backgroundColor; |
||||
|
|
||||
|
// If no bg color was found, check the computed background
|
||||
|
if( !contrastColor ) { |
||||
|
let computedBackgroundStyle = window.getComputedStyle( element ); |
||||
|
if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { |
||||
|
contrastColor = computedBackgroundStyle.backgroundColor; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if( contrastColor ) { |
||||
|
let 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 ) { |
||||
|
slide.classList.add( 'has-dark-background' ); |
||||
|
} |
||||
|
else { |
||||
|
slide.classList.add( 'has-light-background' ); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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' ); |
||||
|
|
||||
|
if( v < indices.v ) { |
||||
|
backgroundv.classList.add( 'past' ); |
||||
|
} |
||||
|
else if ( v > indices.v ) { |
||||
|
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 ) { |
||||
|
[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => { |
||||
|
if( currentSlide.classList.contains( classToBubble ) ) { |
||||
|
this.Reveal.getRevealElement().classList.add( classToBubble ); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.getRevealElement().classList.remove( classToBubble ); |
||||
|
} |
||||
|
}, this ); |
||||
|
} |
||||
|
|
||||
|
// 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'; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,259 @@ |
|||||
|
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' ); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
@ -0,0 +1,97 @@ |
|||||
|
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; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
onRevealPointerDown( event ) { |
||||
|
|
||||
|
this.focus(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
onDocumentPointerDown( event ) { |
||||
|
|
||||
|
let revealElement = closest( event.target, '.reveal' ); |
||||
|
if( !revealElement || revealElement !== this.Reveal.getRevealElement() ) { |
||||
|
this.blur(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,375 @@ |
|||||
|
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 ) { |
||||
|
|
||||
|
let changedFragments = { |
||||
|
shown: [], |
||||
|
hidden: [] |
||||
|
}; |
||||
|
|
||||
|
let currentSlide = this.Reveal.getCurrentSlide(); |
||||
|
if( currentSlide && this.Reveal.getConfig().fragments ) { |
||||
|
|
||||
|
fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) ); |
||||
|
|
||||
|
if( fragments.length ) { |
||||
|
|
||||
|
let maxIndex = 0; |
||||
|
|
||||
|
if( typeof index !== 'number' ) { |
||||
|
let currentFragment = this.sort( currentSlide.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 ) { |
||||
|
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 ); |
||||
|
currentSlide.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 ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,388 @@ |
|||||
|
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 ); |
||||
|
this.onDocumentKeyPress = this.onDocumentKeyPress.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'] = 'Previous slide'; |
||||
|
this.shortcuts['← , H'] = 'Navigate left'; |
||||
|
this.shortcuts['→ , L'] = 'Navigate right'; |
||||
|
this.shortcuts['↑ , K'] = 'Navigate up'; |
||||
|
this.shortcuts['↓ , J'] = 'Navigate down'; |
||||
|
} |
||||
|
|
||||
|
this.shortcuts['Home , Shift ←'] = 'First slide'; |
||||
|
this.shortcuts['End , Shift →'] = 'Last slide'; |
||||
|
this.shortcuts['B , .'] = 'Pause'; |
||||
|
this.shortcuts['F'] = 'Fullscreen'; |
||||
|
this.shortcuts['ESC, O'] = 'Slide overview'; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Starts listening for keyboard events. |
||||
|
*/ |
||||
|
bind() { |
||||
|
|
||||
|
document.addEventListener( 'keydown', this.onDocumentKeyDown, false ); |
||||
|
document.addEventListener( 'keypress', this.onDocumentKeyPress, false ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Stops listening for keyboard events. |
||||
|
*/ |
||||
|
unbind() { |
||||
|
|
||||
|
document.removeEventListener( 'keydown', this.onDocumentKeyDown, false ); |
||||
|
document.removeEventListener( 'keypress', this.onDocumentKeyPress, 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 'keypress' event. |
||||
|
* |
||||
|
* @param {object} event |
||||
|
*/ |
||||
|
onDocumentKeyPress( event ) { |
||||
|
|
||||
|
// Check if the pressed key is question mark
|
||||
|
if( event.shiftKey && event.charCode === 63 ) { |
||||
|
this.Reveal.toggleHelp(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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 specific modified + keycode combinations
|
||||
|
let prevSlideShortcut = event.shiftKey && event.keyCode === 32; |
||||
|
let firstSlideShortcut = event.shiftKey && keyCode === 37; |
||||
|
let lastSlideShortcut = event.shiftKey && keyCode === 39; |
||||
|
|
||||
|
// Prevent all other events when a modifier is pressed
|
||||
|
let unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut && |
||||
|
( 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(); |
||||
|
} |
||||
|
// N, PAGE DOWN
|
||||
|
else if( keyCode === 78 || keyCode === 34 ) { |
||||
|
this.Reveal.next(); |
||||
|
} |
||||
|
// H, LEFT
|
||||
|
else if( keyCode === 72 || keyCode === 37 ) { |
||||
|
if( firstSlideShortcut ) { |
||||
|
this.Reveal.slide( 0 ); |
||||
|
} |
||||
|
else if( !this.Reveal.overview.isActive() && useLinearMode ) { |
||||
|
this.Reveal.prev(); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.left(); |
||||
|
} |
||||
|
} |
||||
|
// L, RIGHT
|
||||
|
else if( keyCode === 76 || keyCode === 39 ) { |
||||
|
if( lastSlideShortcut ) { |
||||
|
this.Reveal.slide( Number.MAX_VALUE ); |
||||
|
} |
||||
|
else if( !this.Reveal.overview.isActive() && useLinearMode ) { |
||||
|
this.Reveal.next(); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.right(); |
||||
|
} |
||||
|
} |
||||
|
// K, UP
|
||||
|
else if( keyCode === 75 || keyCode === 38 ) { |
||||
|
if( !this.Reveal.overview.isActive() && useLinearMode ) { |
||||
|
this.Reveal.prev(); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.up(); |
||||
|
} |
||||
|
} |
||||
|
// J, DOWN
|
||||
|
else if( keyCode === 74 || keyCode === 40 ) { |
||||
|
if( !this.Reveal.overview.isActive() && useLinearMode ) { |
||||
|
this.Reveal.next(); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.down(); |
||||
|
} |
||||
|
} |
||||
|
// HOME
|
||||
|
else if( keyCode === 36 ) { |
||||
|
this.Reveal.slide( 0 ); |
||||
|
} |
||||
|
// END
|
||||
|
else if( keyCode === 35 ) { |
||||
|
this.Reveal.slide( Number.MAX_VALUE ); |
||||
|
} |
||||
|
// SPACE
|
||||
|
else if( keyCode === 32 ) { |
||||
|
if( this.Reveal.overview.isActive() ) { |
||||
|
this.Reveal.overview.deactivate(); |
||||
|
} |
||||
|
if( event.shiftKey ) { |
||||
|
this.Reveal.prev(); |
||||
|
} |
||||
|
else { |
||||
|
this.Reveal.next(); |
||||
|
} |
||||
|
} |
||||
|
// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
|
||||
|
else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) { |
||||
|
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 ); |
||||
|
} |
||||
|
} |
||||
|
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(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,209 @@ |
|||||
|
/** |
||||
|
* Reads and writes the URL based on reveal.js' current state. |
||||
|
*/ |
||||
|
export default class Location { |
||||
|
|
||||
|
constructor( Reveal ) { |
||||
|
|
||||
|
this.Reveal = Reveal; |
||||
|
|
||||
|
// Delays updates to the URL due to a Chrome thumbnailer bug
|
||||
|
this.writeURLTimeout = 0; |
||||
|
|
||||
|
this.onWindowHashChange = this.onWindowHashChange.bind( this ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
bind() { |
||||
|
|
||||
|
window.addEventListener( 'hashchange', this.onWindowHashChange, false ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
unbind() { |
||||
|
|
||||
|
window.removeEventListener( 'hashchange', this.onWindowHashChange, false ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reads the current URL (hash) and navigates accordingly. |
||||
|
*/ |
||||
|
readURL() { |
||||
|
|
||||
|
let config = this.Reveal.getConfig(); |
||||
|
let indices = this.Reveal.getIndices(); |
||||
|
let currentSlide = this.Reveal.getCurrentSlide(); |
||||
|
|
||||
|
let hash = window.location.hash; |
||||
|
|
||||
|
// Attempt to parse the hash as either an index or name
|
||||
|
let bits = hash.slice( 2 ).split( '/' ), |
||||
|
name = hash.replace( /#\/?/gi, '' ); |
||||
|
|
||||
|
// 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 element; |
||||
|
|
||||
|
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 { |
||||
|
element = document.getElementById( decodeURIComponent( name ) ); |
||||
|
} |
||||
|
catch ( error ) { } |
||||
|
|
||||
|
// Ensure that we're not already on a slide with the same name
|
||||
|
let isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false; |
||||
|
|
||||
|
if( element ) { |
||||
|
// If the slide exists and is not the current slide...
|
||||
|
if ( !isSameNameAsCurrentSlide || typeof f !== 'undefined' ) { |
||||
|
// ...find the position of the named slide and navigate to it
|
||||
|
let slideIndices = this.Reveal.getIndices( element ); |
||||
|
this.Reveal.slide( slideIndices.h, slideIndices.v, f ); |
||||
|
} |
||||
|
} |
||||
|
// If the slide doesn't exist, navigate to the current slide
|
||||
|
else { |
||||
|
this.Reveal.slide( indices.h || 0, indices.v || 0 ); |
||||
|
} |
||||
|
} |
||||
|
else { |
||||
|
let hashIndexBase = config.hashOneBasedIndex ? 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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if( h !== indices.h || v !== indices.v || f !== undefined ) { |
||||
|
this.Reveal.slide( h, v, f ); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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 avaialble.
|
||||
|
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 === '/' ) { |
||||
|
window.history.replaceState( null, null, window.location.pathname + window.location.search ); |
||||
|
} |
||||
|
else { |
||||
|
window.history.replaceState( null, null, '#' + 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 );
|
||||
|
// }
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,114 @@ |
|||||
|
/** |
||||
|
* Handles the showing and |
||||
|
*/ |
||||
|
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.print.isPrintingPDF() ) { |
||||
|
|
||||
|
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.print.isPrintingPDF() ) { |
||||
|
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. As an <aside class="notes"> inside of 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 an <aside class="notes"> element
|
||||
|
let notesElement = slide.querySelector( 'aside.notes' ); |
||||
|
if( notesElement ) { |
||||
|
return notesElement.innerHTML; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,255 @@ |
|||||
|
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.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 ); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,241 @@ |
|||||
|
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 registed 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 regisered 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; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,118 @@ |
|||||
|
/** |
||||
|
* 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( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
|
document.addEventListener( 'mousewheel', this.onDocumentMouseScroll, false ); |
||||
|
} |
||||
|
else { |
||||
|
document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
|
document.removeEventListener( 'mousewheel', 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'; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,195 @@ |
|||||
|
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 Print { |
||||
|
|
||||
|
constructor( Reveal ) { |
||||
|
|
||||
|
this.Reveal = Reveal; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Configures the presentation for printing to a static |
||||
|
* PDF. |
||||
|
*/ |
||||
|
setupPDF() { |
||||
|
|
||||
|
let config = this.Reveal.getConfig(); |
||||
|
|
||||
|
let slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight ); |
||||
|
|
||||
|
// Dimensions of the PDF pages
|
||||
|
let pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ), |
||||
|
pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) ); |
||||
|
|
||||
|
// Dimensions of slides within the pages
|
||||
|
let slideWidth = slideSize.width, |
||||
|
slideHeight = slideSize.height; |
||||
|
|
||||
|
// 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( 'print-pdf' ); |
||||
|
document.body.style.width = pageWidth + 'px'; |
||||
|
document.body.style.height = pageHeight + 'px'; |
||||
|
|
||||
|
// Make sure stretch elements fit on slide
|
||||
|
this.Reveal.layoutSlideContents( slideWidth, slideHeight ); |
||||
|
|
||||
|
// Compute slide numbers now, before we start duplicating slides
|
||||
|
let doingSlideNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber ); |
||||
|
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( function( slide ) { |
||||
|
slide.setAttribute( 'data-slide-number', this.Reveal.slideNumber.getSlideNumber( slide ) ); |
||||
|
}, this ); |
||||
|
|
||||
|
// Slide and slide background layout
|
||||
|
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( function( slide ) { |
||||
|
|
||||
|
// 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, |
||||
|
top = ( pageHeight - slideHeight ) / 2; |
||||
|
|
||||
|
let contentHeight = slide.scrollHeight; |
||||
|
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
|
||||
|
let page = document.createElement( 'div' ); |
||||
|
page.className = 'pdf-page'; |
||||
|
page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px'; |
||||
|
slide.parentNode.insertBefore( page, slide ); |
||||
|
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'; |
||||
|
|
||||
|
if( slide.slideBackgroundElement ) { |
||||
|
page.insertBefore( slide.slideBackgroundElement, slide ); |
||||
|
} |
||||
|
|
||||
|
// Inject notes if `showNotes` is enabled
|
||||
|
if( config.showNotes ) { |
||||
|
|
||||
|
// Are there notes for this slide?
|
||||
|
let notes = this.Reveal.getSlideNotes( slide ); |
||||
|
if( notes ) { |
||||
|
|
||||
|
let notesSpacing = 8; |
||||
|
let notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline'; |
||||
|
let 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' ) { |
||||
|
page.parentNode.insertBefore( notesElement, page.nextSibling ); |
||||
|
} |
||||
|
else { |
||||
|
notesElement.style.left = notesSpacing + 'px'; |
||||
|
notesElement.style.bottom = notesSpacing + 'px'; |
||||
|
notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px'; |
||||
|
page.appendChild( notesElement ); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// Inject slide numbers if `slideNumbers` are enabled
|
||||
|
if( doingSlideNumbers ) { |
||||
|
let numberElement = document.createElement( 'div' ); |
||||
|
numberElement.classList.add( 'slide-number' ); |
||||
|
numberElement.classList.add( 'slide-number-pdf' ); |
||||
|
numberElement.innerHTML = slide.getAttribute( 'data-slide-number' ); |
||||
|
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.
|
||||
|
let fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true ); |
||||
|
|
||||
|
let previousFragmentStep; |
||||
|
let previousPage; |
||||
|
|
||||
|
fragmentGroups.forEach( function( fragments ) { |
||||
|
|
||||
|
// 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
|
||||
|
let clonedPage = page.cloneNode( true ); |
||||
|
page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling ); |
||||
|
|
||||
|
previousFragmentStep = fragments; |
||||
|
previousPage = clonedPage; |
||||
|
|
||||
|
}, 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 ); |
||||
|
|
||||
|
// Notify subscribers that the PDF layout is good to go
|
||||
|
this.Reveal.dispatchEvent({ type: 'pdf-ready' }); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Checks if this instance is being used to print a PDF. |
||||
|
*/ |
||||
|
isPrintingPDF() { |
||||
|
|
||||
|
return ( /print-pdf/gi ).test( window.location.search ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,103 @@ |
|||||
|
/** |
||||
|
* 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 slidesTotal = this.Reveal.getHorizontalSlides().length; |
||||
|
let slideIndex = Math.floor( ( event.clientX / this.getMaxWidth() ) * slidesTotal ); |
||||
|
|
||||
|
if( this.Reveal.getConfig().rtl ) { |
||||
|
slideIndex = slidesTotal - slideIndex; |
||||
|
} |
||||
|
|
||||
|
this.Reveal.slide( slideIndex ); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
@ -0,0 +1,456 @@ |
|||||
|
import { HORIZONTAL_SLIDES_SELECTOR, VERTICAL_SLIDES_SELECTOR } from '../utils/constants.js' |
||||
|
import { extend, queryAll, closest } 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 ) { |
||||
|
|
||||
|
// 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 ) { |
||||
|
backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')'; |
||||
|
} |
||||
|
// 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 => { |
||||
|
video.innerHTML += '<source src="'+ source +'">'; |
||||
|
} ); |
||||
|
|
||||
|
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 ); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// 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( slide.querySelectorAll( '.r-fit-text:not([data-fitted])' ) ).forEach( element => { |
||||
|
element.dataset.fitted = ''; |
||||
|
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' ); |
||||
|
} ); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,126 @@ |
|||||
|
/** |
||||
|
* 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.isPrintingPDF() ) { |
||||
|
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 = 'h.v'; |
||||
|
|
||||
|
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 = 'c'; |
||||
|
} |
||||
|
|
||||
|
// 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 'c': |
||||
|
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset ); |
||||
|
break; |
||||
|
case 'c/t': |
||||
|
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 === 'h/v' ? '/' : '.'; |
||||
|
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>`; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,259 @@ |
|||||
|
import { isAndroid } from '../utils/device.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 ) { |
||||
|
|
||||
|
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 ); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,58 @@ |
|||||
|
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; |
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* 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.substr( 0, 2 ), 16 ), |
||||
|
g: parseInt( hex6.substr( 2, 2 ), 16 ), |
||||
|
b: parseInt( hex6.substr( 4, 2 ), 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; |
||||
|
|
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
|
||||
|
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/; |
||||
|
|
||||
|
// 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/; |
@ -0,0 +1,15 @@ |
|||||
|
const UA = navigator.userAgent; |
||||
|
const testElement = document.createElement( 'div' ); |
||||
|
|
||||
|
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 ); |
||||
|
|
||||
|
// Flags if we should use zoom instead of transform to scale
|
||||
|
// up slides. Zoom produces crisper results but has a lot of
|
||||
|
// xbrowser quirks so we only use it in whitelisted browsers.
|
||||
|
export const supportsZoom = 'zoom' in testElement.style && !isMobile && |
||||
|
( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) ); |
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* 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 ); |
||||
|
|
||||
|
} |
@ -0,0 +1,282 @@ |
|||||
|
/** |
||||
|
* 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; |
||||
|
|
||||
|
} |
@ -0,0 +1,220 @@ |
|||||
|
// REVEAL AND TYPE
|
||||
|
|
||||
|
Reveal.initialize({ |
||||
|
width: '80%', |
||||
|
height: '60%', |
||||
|
hash: true, |
||||
|
controls: true, |
||||
|
// Learn about plugins: https://revealjs.com/plugins/
|
||||
|
// plugins: [RevealMarkdown, RevealHighlight, RevealNotes],
|
||||
|
|
||||
|
dependencies: [{ |
||||
|
src: 'plugin/typed/typed.js', |
||||
|
async: false, |
||||
|
callback: function () { |
||||
|
callTypedJs() |
||||
|
} |
||||
|
}] |
||||
|
}) |
||||
|
|
||||
|
function init_type (event) { |
||||
|
console.log('init_type()') |
||||
|
|
||||
|
// console.log(event);
|
||||
|
// console.log(event.currentSlide);
|
||||
|
// console.log(event.indexh);
|
||||
|
|
||||
|
index = event.indexh |
||||
|
div_typed_content = event.currentSlide.getElementsByClassName( |
||||
|
'typed-content' |
||||
|
)[0] |
||||
|
|
||||
|
if (div_typed_content == undefined) { |
||||
|
console.log('Nothing to do. No type div found.') |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
div_typed_content.id = 'typed-content-' + index |
||||
|
div_typed_insert = event.currentSlide.getElementsByClassName( |
||||
|
'typed-insert' |
||||
|
)[0] |
||||
|
div_typed_insert.id = 'typed-insert-' + index |
||||
|
|
||||
|
if (typeof window['typed' + index] === 'undefined') { |
||||
|
window['typed' + index] = new Typed('#' + div_typed_insert.id, { |
||||
|
stringsElement: '#' + div_typed_content.id, |
||||
|
loop: false, |
||||
|
startDelay: 1500, |
||||
|
typeSpeed: 50, |
||||
|
backSpeed: 0, |
||||
|
showCursor: true, |
||||
|
cursorChar: '| ', |
||||
|
autoInsertCss: true, |
||||
|
onComplete: function (self) { |
||||
|
// console.log(Reveal.isLastSlide())
|
||||
|
// console.log(Reveal.getSlides())
|
||||
|
console.log('------') |
||||
|
console.log('End Typing!') |
||||
|
if (Reveal.isLastSlide()) { |
||||
|
console.log('LAAAAAAAAAAAAAST SLIDE') |
||||
|
aside_controls = document.getElementsByClassName('controls')[0] |
||||
|
aside_controls.style.display = 'block' |
||||
|
} else { |
||||
|
setTimeout(() => { |
||||
|
Reveal.right() |
||||
|
}, 3000) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var count_sliding = 0 |
||||
|
|
||||
|
Reveal.on('slidechanged', (event) => { |
||||
|
count_sliding = count_sliding + 1 |
||||
|
console.log('Slide Change: ' + count_sliding) |
||||
|
init_type(event) |
||||
|
}) |
||||
|
|
||||
|
// Hit on first slide
|
||||
|
Reveal.on('ready', (event) => { |
||||
|
console.log('Reveal Ready') |
||||
|
|
||||
|
var aside_controls = document.getElementsByClassName('controls')[0] |
||||
|
aside_controls.style.display = 'none' |
||||
|
|
||||
|
if (Reveal.isFirstSlide()) { |
||||
|
console.log('Reveal is First Slide') |
||||
|
setTimeout(() => { |
||||
|
init_type(event) |
||||
|
}, 5000) |
||||
|
} else { |
||||
|
console.log('Reveal not first slide!!') |
||||
|
window.location.assign('/') |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
/* |
||||
|
* This example show how to load complex shapes created with PhysicsEditor (https://www.codeandweb.com/physicseditor)
|
||||
|
* |
||||
|
* Scaling via: https://www.joshmorony.com/how-to-scale-a-game-for-all-device-sizes-in-phaser/
|
||||
|
*/ |
||||
|
|
||||
|
var config = { |
||||
|
type: Phaser.AUTO, |
||||
|
scale: { |
||||
|
mode: Phaser.Scale.FIT, |
||||
|
parent: 'game', |
||||
|
autoCenter: Phaser.Scale.CENTER_BOTH, |
||||
|
width: window.innerWidth * window.devicePixelRatio, |
||||
|
height: window.innerHeight * window.devicePixelRatio |
||||
|
}, |
||||
|
transparent: true, |
||||
|
backgroundColor: 0xffffff, |
||||
|
scene: { |
||||
|
preload: preload, |
||||
|
create: create |
||||
|
}, |
||||
|
physics: { |
||||
|
default: 'matter', |
||||
|
matter: { |
||||
|
// debug: true
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var game = new Phaser.Game(config) |
||||
|
|
||||
|
function preload () { |
||||
|
// Load sprite sheet generated with TexturePacker
|
||||
|
this.load.atlas( |
||||
|
'sheet', |
||||
|
'assets/fruit-sprites.png', |
||||
|
'assets/fruit-sprites.json' |
||||
|
) |
||||
|
|
||||
|
// Load body shapes from JSON file generated using PhysicsEditor
|
||||
|
this.load.json('shapes', 'assets/fruit-shapes.json') |
||||
|
// this.load.json('shapes', 'assets/scooter.json');
|
||||
|
} |
||||
|
|
||||
|
function create () { |
||||
|
var shapes = this.cache.json.get('shapes') |
||||
|
|
||||
|
this.matter.world.setBounds(0, 0, game.config.width, game.config.height) |
||||
|
// this.add.image(0, 0, 'sheet', 'background').setOrigin(0, 0)
|
||||
|
|
||||
|
// sprites are positioned at their center of mass
|
||||
|
var ground = this.matter.add.sprite(0, 0, 'sheet', 'ground', { |
||||
|
shape: shapes.ground |
||||
|
}) |
||||
|
ground.setPosition(0 + ground.centerOfMass.x, 1000 + ground.centerOfMass.y) // corrected position: (0,280)
|
||||
|
this.matter.add.sprite(200, 50, 'sheet', 'crate', { |
||||
|
shape: shapes.crate |
||||
|
}) |
||||
|
this.matter.add.sprite(250, 250, 'sheet', 'banana', { |
||||
|
shape: shapes.banana |
||||
|
}) |
||||
|
this.matter.add.sprite(360, 50, 'sheet', 'orange', { |
||||
|
shape: shapes.orange |
||||
|
}) |
||||
|
this.matter.add.sprite(400, 250, 'sheet', 'cherries', { |
||||
|
shape: shapes.cherries |
||||
|
}) |
||||
|
|
||||
|
var sprites = ['crate', 'banana', 'orange', 'cherries'] // array to choose random sprite from
|
||||
|
|
||||
|
for (let i = 0; i < 50; ++i) { |
||||
|
this.matter.add.sprite( |
||||
|
getRandomIntMin(30, 100), |
||||
|
getRandomIntMin(30, game.config.height / 2), |
||||
|
'sheet', |
||||
|
sprites[getRandomInt(4)], { |
||||
|
shape: shapes.banana |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
this.input.on( |
||||
|
'pointerdown', |
||||
|
function (pointer) { |
||||
|
// var touchX = pointer.x;
|
||||
|
// var touchY = pointer.y;
|
||||
|
this.matter.add.sprite( |
||||
|
pointer.x, |
||||
|
pointer.y, |
||||
|
'sheet', |
||||
|
sprites[getRandomInt(4)], { |
||||
|
shape: shapes.banana |
||||
|
} |
||||
|
) |
||||
|
}, |
||||
|
this |
||||
|
) |
||||
|
|
||||
|
setInterval(() => { |
||||
|
console.log('get random sprite') |
||||
|
for (let i = 0; i < getRandomInt(3); ++i) { |
||||
|
this.matter.add.sprite( |
||||
|
getRandomIntMin(30, 100), |
||||
|
getRandomIntMin(30, game.config.height / 2), |
||||
|
'sheet', |
||||
|
sprites[getRandomInt(4)], { |
||||
|
shape: shapes.banana |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
}, 10000) |
||||
|
} |
||||
|
|
||||
|
function getRandomInt (max) { |
||||
|
return Math.floor(Math.random() * Math.floor(max)) |
||||
|
} |
||||
|
|
||||
|
function getRandomIntMin (min, max) { |
||||
|
min = Math.ceil(min) |
||||
|
max = Math.floor(max) |
||||
|
return Math.floor(Math.random() * (max - min) + min) // The maximum is exclusive and the minimum is inclusive
|
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
#!/bin/bash |
||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" |
||||
|
|
||||
|
rm -rf /tmp/followtherider* |
||||
|
cp -rfapv ../followtherider /tmp |
||||
|
cd /tmp/followtherider |
||||
|
rm -rf .git |
||||
|
rm -rf _publish |
||||
|
cd /tmp/ |
||||
|
zip followtherider.zip -r followtherider |
||||
|
cd $DIR |
||||
|
mv /tmp/followtherider.zip _publish |
||||
|
|
||||
|
|
@ -0,0 +1,23 @@ |
|||||
|
{ |
||||
|
"name": "followthescooter", |
||||
|
"version": "1.0.0", |
||||
|
"description": "", |
||||
|
"main": "main.js", |
||||
|
"scripts": { |
||||
|
"test": "eslint" |
||||
|
}, |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "gitea@git.bitnik.org:anton/followthescooter.git" |
||||
|
}, |
||||
|
"author": "", |
||||
|
"license": "ISC", |
||||
|
"devDependencies": { |
||||
|
"eslint": "^7.10.0", |
||||
|
"eslint-config-standard": "^14.1.1", |
||||
|
"eslint-plugin-import": "^2.22.1", |
||||
|
"eslint-plugin-node": "^11.1.0", |
||||
|
"eslint-plugin-promise": "^4.2.1", |
||||
|
"eslint-plugin-standard": "^4.0.1" |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2017 Matt Boldt |
||||
|
|
||||
|
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. |
@ -0,0 +1,318 @@ |
|||||
|
[![Build Status](https://travis-ci.org/mattboldt/typed.js.svg?branch=typed-2.0)](https://travis-ci.org/mattboldt/typed.js) |
||||
|
[![Code Climate](https://codeclimate.com/github/mattboldt/typed.js/badges/gpa.svg)](https://codeclimate.com/github/mattboldt/typed.js) |
||||
|
[![GitHub release](https://img.shields.io/github/release/mattboldt/typed.js.svg)]() |
||||
|
[![npm](https://img.shields.io/npm/dt/typed.js.svg)](https://img.shields.io/npm/dt/typed.js.svg) |
||||
|
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) |
||||
|
[![Gratipay User](https://img.shields.io/gratipay/user/mattboldt.svg)](https://gratipay.com/Typed.js/) |
||||
|
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/mattboldt/typed.js/master/LICENSE.txt) |
||||
|
|
||||
|
<img src="https://raw.githubusercontent.com/mattboldt/typed.js/master/logo-cropped.png" width="450px" title="Typed.js" /> |
||||
|
|
||||
|
### [View the Live Demo](http://www.mattboldt.com/demos/typed-js/) | [View All Demos](http://www.mattboldt.com/typed.js) | [View Full Docs](http://www.mattboldt.com/typed.js/docs) | [mattboldt.com](http://www.mattboldt.com) |
||||
|
|
||||
|
Typed.js is a library that types. Enter in any string, and watch it type at the speed you've set, backspace what it's typed, and begin a new sentence for however many strings you've set. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Installation |
||||
|
------------ |
||||
|
|
||||
|
#### NPM |
||||
|
|
||||
|
~~~ |
||||
|
npm install typed.js |
||||
|
~~~ |
||||
|
|
||||
|
#### Yarn |
||||
|
|
||||
|
~~~ |
||||
|
yarn add typed.js |
||||
|
~~~ |
||||
|
|
||||
|
#### Bower |
||||
|
|
||||
|
~~~ |
||||
|
bower install typed.js |
||||
|
~~~ |
||||
|
|
||||
|
#### Setup |
||||
|
|
||||
|
This is really all you need to get going. |
||||
|
|
||||
|
~~~ javascript |
||||
|
// Can also be included with a regular script tag |
||||
|
import Typed from 'typed.js'; |
||||
|
|
||||
|
var options = { |
||||
|
strings: ["<i>First</i> sentence.", "& a second sentence."], |
||||
|
typeSpeed: 40 |
||||
|
} |
||||
|
|
||||
|
var typed = new Typed(".element", options); |
||||
|
~~~ |
||||
|
|
||||
|
Wonderful sites using Typed.js |
||||
|
--- |
||||
|
https://slack.com/ |
||||
|
|
||||
|
https://envato.com/ |
||||
|
|
||||
|
https://productmap.co/ |
||||
|
|
||||
|
https://www.typed.com/ |
||||
|
|
||||
|
https://apeiron.io |
||||
|
|
||||
|
https://git.market/ |
||||
|
|
||||
|
http://allison.house/404 |
||||
|
|
||||
|
http://www.maxcdn.com/ |
||||
|
|
||||
|
https://commando.io/ |
||||
|
|
||||
|
http://testdouble.com/agency.html |
||||
|
|
||||
|
http://www.stephanemartinw.com/ |
||||
|
|
||||
|
http://www.trelab.fi/en/ |
||||
|
|
||||
|
http://jessejohnson.github.io/ |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Strings from static HTML (SEO Friendly) |
||||
|
Rather than using the `strings` array to insert strings, you can place an HTML `div` on the page and read from it. |
||||
|
This allows bots and search engines, as well as users with JavaScript disabled, to see your text on the page. |
||||
|
|
||||
|
~~~ javascript |
||||
|
<script> |
||||
|
var typed = new Typed('.element', { |
||||
|
stringsElement: '#typed-strings' |
||||
|
}); |
||||
|
</script> |
||||
|
~~~ |
||||
|
|
||||
|
~~~ html |
||||
|
<div id="typed-strings"> |
||||
|
<p>Typed.js is a <strong>JavaScript</strong> library.</p> |
||||
|
<p>It <em>types</em> out sentences.</p> |
||||
|
</div> |
||||
|
<span id="typed"></span> |
||||
|
~~~ |
||||
|
|
||||
|
### Type Pausing |
||||
|
|
||||
|
You can pause in the middle of a string for a given amount of time by including an escape character. |
||||
|
|
||||
|
~~~ javascript |
||||
|
var typed = new Typed(".element", { |
||||
|
// Waits 1000ms after typing "First" |
||||
|
strings: ["First ^1000 sentence.", "Second sentence."] |
||||
|
}); |
||||
|
~~~ |
||||
|
|
||||
|
### Smart Backspacing |
||||
|
|
||||
|
In the following example, this would only backspace the words after "This is a" |
||||
|
|
||||
|
~~~ javascript |
||||
|
var typed = new Typed(".element", { |
||||
|
strings: ["This is a JavaScript library", "This is an ES6 module"], |
||||
|
smartBackspace: true // Default value |
||||
|
}); |
||||
|
~~~ |
||||
|
|
||||
|
### Bulk Typing |
||||
|
|
||||
|
The following example would emulate how a terminal acts when typing a command and seeing its result. |
||||
|
|
||||
|
~~~ javascript |
||||
|
var typed = new Typed(".element", { |
||||
|
strings: [ |
||||
|
"git push --force ^1000\n `pushed to origin with option force`" |
||||
|
] |
||||
|
}); |
||||
|
~~~ |
||||
|
|
||||
|
### CSS |
||||
|
|
||||
|
CSS animations are build upon initialzation in JavaScript. But, you can customize them at your will! These classes are: |
||||
|
```css |
||||
|
/* Cursor */ |
||||
|
.typed-cursor {} |
||||
|
|
||||
|
/* If fade out option is set */ |
||||
|
.typed-fade-out {} |
||||
|
``` |
||||
|
|
||||
|
### Use with ReactJS |
||||
|
|
||||
|
Check out this example React app using Typed.js in a component: https://jsfiddle.net/mattboldt/ovat9jmp/ |
||||
|
|
||||
|
|
||||
|
Customization |
||||
|
---- |
||||
|
|
||||
|
~~~ javascript |
||||
|
var typed = new Typed(".element", { |
||||
|
/** |
||||
|
* @property {array} strings strings to be typed |
||||
|
* @property {string} stringsElement ID of element containing string children |
||||
|
*/ |
||||
|
strings: ['These are the default values...', 'You know what you should do?', 'Use your own!', 'Have a great day!'], |
||||
|
stringsElement: null, |
||||
|
|
||||
|
/** |
||||
|
* @property {number} typeSpeed type speed in milliseconds |
||||
|
*/ |
||||
|
typeSpeed: 0, |
||||
|
|
||||
|
/** |
||||
|
* @property {number} startDelay time before typing starts in milliseconds |
||||
|
*/ |
||||
|
startDelay: 0, |
||||
|
|
||||
|
/** |
||||
|
* @property {number} backSpeed backspacing speed in milliseconds |
||||
|
*/ |
||||
|
backSpeed: 0, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} smartBackspace only backspace what doesn't match the previous string |
||||
|
*/ |
||||
|
smartBackspace: true, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} shuffle shuffle the strings |
||||
|
*/ |
||||
|
shuffle: false, |
||||
|
|
||||
|
/** |
||||
|
* @property {number} backDelay time before backspacing in milliseconds |
||||
|
*/ |
||||
|
backDelay: 700, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} fadeOut Fade out instead of backspace |
||||
|
* @property {string} fadeOutClass css class for fade animation |
||||
|
* @property {boolean} fadeOutDelay Fade out delay in milliseconds |
||||
|
*/ |
||||
|
fadeOut: false, |
||||
|
fadeOutClass: 'typed-fade-out', |
||||
|
fadeOutDelay: 500, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} loop loop strings |
||||
|
* @property {number} loopCount amount of loops |
||||
|
*/ |
||||
|
loop: false, |
||||
|
loopCount: Infinity, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} showCursor show cursor |
||||
|
* @property {string} cursorChar character for cursor |
||||
|
* @property {boolean} autoInsertCss insert CSS for cursor and fadeOut into HTML <head> |
||||
|
*/ |
||||
|
showCursor: true, |
||||
|
cursorChar: '|', |
||||
|
autoInsertCss: true, |
||||
|
|
||||
|
/** |
||||
|
* @property {string} attr attribute for typing |
||||
|
* Ex: input placeholder, value, or just HTML text |
||||
|
*/ |
||||
|
attr: null, |
||||
|
|
||||
|
/** |
||||
|
* @property {boolean} bindInputFocusEvents bind to focus and blur if el is text input |
||||
|
*/ |
||||
|
bindInputFocusEvents: false, |
||||
|
|
||||
|
/** |
||||
|
* @property {string} contentType 'html' or 'null' for plaintext |
||||
|
*/ |
||||
|
contentType: 'html', |
||||
|
|
||||
|
/** |
||||
|
* All typing is complete |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onComplete: (self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* Before each string is typed |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
preStringTyped: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* After each string is typed |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onStringTyped: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* During looping, after last string is typed |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onLastStringBackspaced: (self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* Typing has been stopped |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onTypingPaused: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* Typing has been started after being stopped |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onTypingResumed: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* After reset |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onReset: (self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* After stop |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onStop: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* After start |
||||
|
* @param {number} arrayPos |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onStart: (arrayPos, self) => {}, |
||||
|
|
||||
|
/** |
||||
|
* After destroy |
||||
|
* @param {Typed} self |
||||
|
*/ |
||||
|
onDestroy: (self) => {} |
||||
|
}); |
||||
|
~~~ |
||||
|
|
||||
|
|
||||
|
## Contributing |
||||
|
|
||||
|
### [View Contribution Guidelines](./.github/CONTRIBUTING.md) |
||||
|
|
||||
|
end |
||||
|
--- |
||||
|
|
||||
|
Thanks for checking this out. If you have any questions, I'll be on [Twitter](http://www.twitter.com/atmattb). |
||||
|
|
||||
|
If you're using this, let me know! I'd love to see it. |
||||
|
|
||||
|
It would also be great if you mentioned me or my website somewhere. [www.mattboldt.com](http://www.mattboldt.com) |
@ -0,0 +1,3 @@ |
|||||
|
#!/usr/bin/bash |
||||
|
# sudo npm install -g browser-sync |
||||
|
browser-sync start --server --files "css/*.css" "*.html" "*.js" "*.css" |
After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,62 @@ |
|||||
|
body, html { |
||||
|
margin: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
text-align: center; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
transition: 1s; |
||||
|
} |
||||
|
|
||||
|
body.bw { |
||||
|
background: #000; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.fixed { |
||||
|
text-transform: capitalize; |
||||
|
position: fixed; |
||||
|
font-size: 2rem; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 8rem; |
||||
|
text-transform: capitalize; |
||||
|
} |
||||
|
|
||||
|
#left { |
||||
|
padding: 0; |
||||
|
padding-right: 4px; |
||||
|
border-right: 2px solid #747474; |
||||
|
animation: blink 0.75s infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes blink { |
||||
|
from { |
||||
|
border-color: #fff; |
||||
|
} |
||||
|
to { |
||||
|
border-color: #747474; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* .blue { |
||||
|
color: #4480ff; |
||||
|
} |
||||
|
.green { |
||||
|
color: #4caf50; |
||||
|
} |
||||
|
.yellow { |
||||
|
color: #f4b400; |
||||
|
} |
||||
|
.red { |
||||
|
color: #f44336; |
||||
|
}*/ |
||||
|
/*h1 span:first-child(), h1 span span:first-child() { |
||||
|
} |
||||
|
*/ |
@ -0,0 +1,13 @@ |
|||||
|
<html><head> |
||||
|
<meta http-equiv="content-type" content="text/html; charset=windows-1252"> |
||||
|
<link rel="stylesheet" href="capitalism-Dateien/styles-type.css"> |
||||
|
</head> |
||||
|
<body class="bw"> |
||||
|
<h1><span id="left">educ</span> <span id="right">Capitalism</span></h1> |
||||
|
|
||||
|
<script src="capitalism-Dateien/jquery.js"></script> |
||||
|
<script src="capitalism-Dateien/script_type-retype.js"></script> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</body></html> |
After Width: | Height: | Size: 109 KiB |