From 31cb7db0808cc065c50612b84883057a62f84a21 Mon Sep 17 00:00:00 2001 From: Francesco Mecca Date: Tue, 16 Jun 2020 19:25:06 +0200 Subject: [PATCH] progmobile --- anno3/progmobile/apollon/app/.gitignore | 1 + anno3/progmobile/apollon/app/build.gradle | 38 + .../progmobile/apollon/app/proguard-rules.pro | 21 + .../apollon/app/src/main/AndroidManifest.xml | 34 + .../apollon/app/src/main/ic_launcher-web.png | Bin 0 -> 38617 bytes .../src/main/java/com/apollon/MainActivity.kt | 128 + .../main/java/com/apollon/PlayerService.kt | 539 ++ .../src/main/java/com/apollon/ServerBridge.kt | 613 ++ .../src/main/java/com/apollon/TaskListener.kt | 5 + .../com/apollon/adapters/PlaylistAdapter.kt | 196 + .../java/com/apollon/adapters/SongAdapter.kt | 179 + .../java/com/apollon/classes/Credentials.kt | 42 + .../main/java/com/apollon/classes/Playlist.kt | 25 + .../src/main/java/com/apollon/classes/Song.kt | 21 + .../java/com/apollon/classes/StreamingSong.kt | 7 + .../com/apollon/fragments/LoginFragment.kt | 90 + .../apollon/fragments/PlayListsFragment.kt | 185 + .../com/apollon/fragments/PlayerFragment.kt | 456 ++ .../com/apollon/fragments/SongsFragment.kt | 117 + .../main/res/drawable-v24/default_song.png | Bin 0 -> 32528 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 + .../app/src/main/res/drawable-v24/logo.png | Bin 0 -> 41030 bytes .../res/drawable-v24/mq_button_selector.xml | 5 + .../mq_button_selector_inverted.xml | 5 + .../src/main/res/drawable-v24/playlist.png | Bin 0 -> 18046 bytes .../app/src/main/res/drawable/album.png | Bin 0 -> 31006 bytes .../apollon/app/src/main/res/drawable/all.png | Bin 0 -> 43316 bytes .../app/src/main/res/drawable/artist.png | Bin 0 -> 39751 bytes .../app/src/main/res/drawable/back.png | Bin 0 -> 21950 bytes .../res/drawable/back_button_selector.xml | 5 + .../app/src/main/res/drawable/back_noti.png | Bin 0 -> 638 bytes .../src/main/res/drawable/back_pressed.png | Bin 0 -> 29979 bytes .../app/src/main/res/drawable/delete.png | Bin 0 -> 32034 bytes .../res/drawable/delete_button_selector.xml | 5 + .../src/main/res/drawable/delete_pressed.png | Bin 0 -> 24194 bytes .../app/src/main/res/drawable/dots.png | Bin 0 -> 2793 bytes .../app/src/main/res/drawable/edit.png | Bin 0 -> 24836 bytes .../res/drawable/edit_button_selector.xml | 5 + .../src/main/res/drawable/edit_pressed.png | Bin 0 -> 16460 bytes .../app/src/main/res/drawable/favourite.png | Bin 0 -> 36154 bytes .../drawable/favourite_button_selector.xml | 5 + .../favourite_button_selector_inverted.xml | 5 + .../src/main/res/drawable/favourite_not.png | Bin 0 -> 33004 bytes .../favourite_not_button_selector.xml | 5 + ...favourite_not_button_selector_inverted.xml | 5 + .../res/drawable/favourite_not_pressed.png | Bin 0 -> 22064 bytes .../main/res/drawable/favourite_pressed.png | Bin 0 -> 24652 bytes .../app/src/main/res/drawable/favourites.png | Bin 0 -> 44068 bytes .../app/src/main/res/drawable/forward.png | Bin 0 -> 21868 bytes .../res/drawable/forward_button_selector.xml | 5 + .../src/main/res/drawable/forward_noti.png | Bin 0 -> 525 bytes .../src/main/res/drawable/forward_pressed.png | Bin 0 -> 29948 bytes .../app/src/main/res/drawable/genre.png | Bin 0 -> 29994 bytes .../app/src/main/res/drawable/gradient.xml | 10 + .../apollon/app/src/main/res/drawable/hq.png | Bin 0 -> 21277 bytes .../main/res/drawable/hq_button_selector.xml | 5 + .../drawable/hq_button_selector_inverted.xml | 5 + .../app/src/main/res/drawable/hq_pressed.png | Bin 0 -> 21837 bytes .../res/drawable/ic_launcher_background.xml | 74 + .../app/src/main/res/drawable/icon.png | Bin 0 -> 28304 bytes .../apollon/app/src/main/res/drawable/lq.png | Bin 0 -> 21017 bytes .../main/res/drawable/lq_button_selector.xml | 5 + .../drawable/lq_button_selector_inverted.xml | 5 + .../app/src/main/res/drawable/lq_pressed.png | Bin 0 -> 21627 bytes .../app/src/main/res/drawable/lyrics.png | Bin 0 -> 22603 bytes .../res/drawable/lyrics_button_selector.xml | 5 + .../lyrics_button_selector_inverted.xml | 5 + .../src/main/res/drawable/lyrics_pressed.png | Bin 0 -> 23452 bytes .../apollon/app/src/main/res/drawable/mq.png | Bin 0 -> 24726 bytes .../app/src/main/res/drawable/mq_pressed.png | Bin 0 -> 25302 bytes .../app/src/main/res/drawable/pause.png | Bin 0 -> 16153 bytes .../res/drawable/pause_button_selector.xml | 5 + .../app/src/main/res/drawable/pause_noti.png | Bin 0 -> 193 bytes .../src/main/res/drawable/pause_pressed.png | Bin 0 -> 24364 bytes .../app/src/main/res/drawable/play.png | Bin 0 -> 23701 bytes .../res/drawable/play_button_selector.xml | 5 + .../app/src/main/res/drawable/play_noti.png | Bin 0 -> 405 bytes .../src/main/res/drawable/play_pressed.png | Bin 0 -> 31780 bytes .../app/src/main/res/drawable/plus.png | Bin 0 -> 24741 bytes .../res/drawable/plus_button_selector.xml | 5 + .../src/main/res/drawable/plus_pressed.png | Bin 0 -> 16546 bytes .../app/src/main/res/drawable/repeat_all.png | Bin 0 -> 32496 bytes .../drawable/repeat_all_button_selector.xml | 5 + .../main/res/drawable/repeat_all_pressed.png | Bin 0 -> 40265 bytes .../app/src/main/res/drawable/repeat_not.png | Bin 0 -> 39813 bytes .../drawable/repeat_not_button_selector.xml | 5 + .../main/res/drawable/repeat_not_pressed.png | Bin 0 -> 47468 bytes .../app/src/main/res/drawable/repeat_this.png | Bin 0 -> 33790 bytes .../drawable/repeat_this_button_selector.xml | 5 + .../main/res/drawable/repeat_this_pressed.png | Bin 0 -> 41654 bytes .../app/src/main/res/drawable/share.png | Bin 0 -> 37811 bytes .../res/drawable/share_button_selector.xml | 5 + .../share_button_selector_inverted.xml | 5 + .../src/main/res/drawable/share_pressed.png | Bin 0 -> 29911 bytes .../app/src/main/res/drawable/shuffle.png | Bin 0 -> 26329 bytes .../res/drawable/shuffle_button_selector.xml | 5 + .../app/src/main/res/drawable/shuffle_not.png | Bin 0 -> 30475 bytes .../drawable/shuffle_not_button_selector.xml | 5 + .../main/res/drawable/shuffle_not_pressed.png | Bin 0 -> 37512 bytes .../src/main/res/drawable/shuffle_pressed.png | Bin 0 -> 33738 bytes .../app/src/main/res/layout-h600dp/player.xml | 156 + .../app/src/main/res/layout/activity_main.xml | 77 + .../apollon/app/src/main/res/layout/login.xml | 103 + .../app/src/main/res/layout/modify.xml | 12 + .../app/src/main/res/layout/notification.xml | 63 + .../app/src/main/res/layout/player.xml | 159 + .../app/src/main/res/layout/playlist_card.xml | 84 + .../app/src/main/res/layout/playlists.xml | 40 + .../app/src/main/res/layout/song_card.xml | 63 + .../apollon/app/src/main/res/layout/songs.xml | 76 + .../app/src/main/res/menu/quality_menu.xml | 18 + .../app/src/main/res/menu/song_menu.xml | 18 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3610 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2878 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3610 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2280 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1802 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2280 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5153 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4036 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5153 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8232 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 6672 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8232 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11733 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 9818 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 11733 bytes .../app/src/main/res/values-fr/strings.xml | 52 + .../app/src/main/res/values-it/strings.xml | 52 + .../app/src/main/res/values-sw/strings.xml | 52 + .../app/src/main/res/values/arrays.xml | 7 + .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/dimens.xml | 50 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 55 + .../app/src/main/res/values/styles.xml | 51 + anno3/progmobile/apollon/build.gradle | 27 + anno3/progmobile/apollon/gradle.properties | 1 + anno3/progmobile/apollon/settings.gradle | 1 + .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../buildOutputCleanup/outputFiles.bin | Bin 0 -> 18839 bytes .../mozapp.server/.gradle/vcs-1/gc.properties | 0 .../backend/mozapp.server/.idea/.name | 1 + .../.idea/codeStyles/Project.xml | 10 + .../.idea/codeStyles/codeStyleConfig.xml | 5 + .../backend/mozapp.server/.idea/gradle.xml | 18 + .../backend/mozapp.server/.idea/misc.xml | 7 + .../backend/mozapp.server/.idea/vcs.xml | 6 + .../backend/mozapp.server/.idea/workspace.xml | 631 ++ .../backend/mozapp.server/README.md | 607 ++ .../backend/mozapp.server/build.gradle | 38 + .../mozapp.server/mpd_dir/artists.json | 1 + .../backend/mozapp.server/mpd_dir/covers.json | 1 + .../backend/mozapp.server/mpd_dir/db2.json | 284 + .../backend/mozapp.server/mpd_dir/example.py | 81 + .../backend/mozapp.server/mpd_dir/lyrics.py | 4 + .../backend/mozapp.server/mpd_dir/mm.py | 83 + .../mozapp.server/mpd_dir/produce_imgs.py | 13 + .../backend/mozapp.server/playlists.json | 1 + .../backend/mozapp.server/settings.gradle | 2 + .../src/main/kotlin/Playlists.kt | 206 + .../mozapp.server/src/main/kotlin/database.kt | 255 + .../mozapp.server/src/main/kotlin/main.kt | 341 + .../mozapp.server/src/main/kotlin/request.kt | 171 + .../mozapp.server/src/main/kotlin/response.kt | 214 + .../src/main/kotlin/streaming.kt | 175 + .../src/main/kotlin/thirdparties.kt | 105 + .../backend/mozapp.server/users.json | 6 + anno3/progmobile/presentazione/.gitignore | 14 + .../presentazione/css/print/paper.css | 203 + .../presentazione/css/print/pdf.css | 164 + anno3/progmobile/presentazione/css/reset.css | 30 + anno3/progmobile/presentazione/css/reveal.css | 1606 +++++ .../progmobile/presentazione/css/reveal.scss | 1777 +++++ .../presentazione/css/theme/Apollon.css | 297 + .../presentazione/css/theme/README.md | 21 + .../presentazione/css/theme/beige.css | 277 + .../presentazione/css/theme/black.css | 273 + .../presentazione/css/theme/blood.css | 296 + .../presentazione/css/theme/league.css | 279 + .../presentazione/css/theme/moon.css | 277 + .../presentazione/css/theme/night.css | 271 + .../presentazione/css/theme/serif.css | 273 + .../presentazione/css/theme/simple.css | 276 + .../presentazione/css/theme/sky.css | 280 + .../presentazione/css/theme/solarized.css | 277 + .../presentazione/css/theme/source/beige.scss | 39 + .../presentazione/css/theme/source/black.scss | 49 + .../presentazione/css/theme/source/blood.scss | 78 + .../css/theme/source/league.scss | 34 + .../presentazione/css/theme/source/moon.scss | 57 + .../presentazione/css/theme/source/night.scss | 34 + .../presentazione/css/theme/source/serif.scss | 35 + .../css/theme/source/simple.scss | 43 + .../presentazione/css/theme/source/sky.scss | 46 + .../css/theme/source/solarized.scss | 63 + .../presentazione/css/theme/source/white.scss | 49 + .../css/theme/template/mixins.scss | 29 + .../css/theme/template/settings.scss | 45 + .../css/theme/template/theme.scss | 325 + .../presentazione/css/theme/white.css | 273 + anno3/progmobile/presentazione/favicon.ico | Bin 0 -> 43506 bytes .../presentazione/gifs/favourite.gif | Bin 0 -> 2566962 bytes anno3/progmobile/presentazione/gifs/swipe.gif | Bin 0 -> 2211445 bytes .../img/450px-Apollon_opera_Garnier_n3.png | Bin 0 -> 314253 bytes anno3/progmobile/presentazione/img/AM.png | Bin 0 -> 87819 bytes .../presentazione/img/ApolloCensored.png | Bin 0 -> 327713 bytes .../progmobile/presentazione/img/Apollon.png | Bin 0 -> 43506 bytes anno3/progmobile/presentazione/img/Spot.png | Bin 0 -> 60181 bytes anno3/progmobile/presentazione/img/albums.png | Bin 0 -> 2716042 bytes anno3/progmobile/presentazione/img/am2.jpg | Bin 0 -> 27365 bytes anno3/progmobile/presentazione/img/loghi.kra | Bin 0 -> 903542 bytes .../presentazione/img/login_framed.png | Bin 0 -> 1329412 bytes anno3/progmobile/presentazione/img/logo.png | Bin 0 -> 50195 bytes anno3/progmobile/presentazione/img/lyrics.png | Bin 0 -> 1599228 bytes .../presentazione/img/miniplayer.png | Bin 0 -> 1407564 bytes .../presentazione/img/notification.png | Bin 0 -> 1981434 bytes anno3/progmobile/presentazione/img/player.png | Bin 0 -> 1725094 bytes .../presentazione/img/playerArch.png | Bin 0 -> 2975 bytes .../presentazione/img/player_long.png | Bin 0 -> 2304206 bytes anno3/progmobile/presentazione/img/prova.png | Bin 0 -> 551462 bytes anno3/progmobile/presentazione/img/s_arch.png | Bin 0 -> 9371 bytes anno3/progmobile/presentazione/img/songs.png | Bin 0 -> 1734311 bytes anno3/progmobile/presentazione/index.html | 335 + anno3/progmobile/presentazione/js/reveal.js | 6191 +++++++++++++++++ .../presentazione/lib/css/monokai.css | 71 + .../presentazione/lib/css/zenburn.css | 80 + .../lib/font/league-gothic/LICENSE | 2 + .../lib/font/league-gothic/league-gothic.css | 10 + .../lib/font/league-gothic/league-gothic.eot | Bin 0 -> 25696 bytes .../lib/font/league-gothic/league-gothic.ttf | Bin 0 -> 64256 bytes .../lib/font/league-gothic/league-gothic.woff | Bin 0 -> 30764 bytes .../lib/font/source-sans-pro/LICENSE | 45 + .../source-sans-pro-italic.eot | Bin 0 -> 75720 bytes .../source-sans-pro-italic.ttf | Bin 0 -> 238084 bytes .../source-sans-pro-italic.woff | Bin 0 -> 98556 bytes .../source-sans-pro-regular.eot | Bin 0 -> 88070 bytes .../source-sans-pro-regular.ttf | Bin 0 -> 288008 bytes .../source-sans-pro-regular.woff | Bin 0 -> 114324 bytes .../source-sans-pro-semibold.eot | Bin 0 -> 89897 bytes .../source-sans-pro-semibold.ttf | Bin 0 -> 284640 bytes .../source-sans-pro-semibold.woff | Bin 0 -> 115648 bytes .../source-sans-pro-semibolditalic.eot | Bin 0 -> 75706 bytes .../source-sans-pro-semibolditalic.ttf | Bin 0 -> 240944 bytes .../source-sans-pro-semibolditalic.woff | Bin 0 -> 98816 bytes .../font/source-sans-pro/source-sans-pro.css | 39 + .../presentazione/lib/js/html5shiv.js | 7 + .../presentazione/lib/js/promise.js | 2 + .../plugin/highlight/highlight.js | 303 + .../plugin/markdown/example.html | 134 + .../presentazione/plugin/markdown/example.md | 36 + .../presentazione/plugin/markdown/markdown.js | 446 ++ .../presentazione/plugin/markdown/marked.js | 6 + .../presentazione/plugin/math/math.js | 92 + .../presentazione/plugin/multiplex/client.js | 13 + .../presentazione/plugin/multiplex/index.js | 64 + .../presentazione/plugin/multiplex/master.js | 34 + .../plugin/multiplex/package.json | 19 + .../plugin/notes-server/client.js | 65 + .../plugin/notes-server/index.js | 69 + .../plugin/notes-server/notes.html | 585 ++ .../presentazione/plugin/notes/notes.html | 854 +++ .../presentazione/plugin/notes/notes.js | 178 + .../plugin/print-pdf/print-pdf.js | 67 + .../presentazione/plugin/search/search.js | 206 + .../presentazione/plugin/zoom-js/zoom.js | 277 + anno3/progmobile/presentazione/relazione.md | 165 + anno3/progmobile/presentazione/table.html | 66 + 271 files changed, 26001 insertions(+) create mode 100644 anno3/progmobile/apollon/app/.gitignore create mode 100644 anno3/progmobile/apollon/app/build.gradle create mode 100644 anno3/progmobile/apollon/app/proguard-rules.pro create mode 100644 anno3/progmobile/apollon/app/src/main/AndroidManifest.xml create mode 100644 anno3/progmobile/apollon/app/src/main/ic_launcher-web.png create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/MainActivity.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/PlayerService.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/ServerBridge.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/TaskListener.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/adapters/PlaylistAdapter.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/adapters/SongAdapter.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/classes/Credentials.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/classes/Playlist.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/classes/Song.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/classes/StreamingSong.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/fragments/LoginFragment.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/fragments/PlayListsFragment.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/fragments/PlayerFragment.kt create mode 100644 anno3/progmobile/apollon/app/src/main/java/com/apollon/fragments/SongsFragment.kt create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/default_song.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/logo.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/mq_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/mq_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable-v24/playlist.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/album.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/all.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/artist.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/back.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/back_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/back_noti.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/back_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/delete.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/delete_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/delete_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/dots.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/edit.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/edit_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/edit_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_not.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_not_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_not_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_not_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourite_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/favourites.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/forward.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/forward_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/forward_noti.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/forward_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/genre.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/gradient.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/hq.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/hq_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/hq_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/hq_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/icon.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lq.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lq_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lq_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lq_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lyrics.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lyrics_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lyrics_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/lyrics_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/mq.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/mq_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/pause.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/pause_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/pause_noti.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/pause_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/play.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/play_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/play_noti.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/play_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/plus.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/plus_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/plus_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_all.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_all_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_all_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_not.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_not_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_not_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_this.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_this_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/repeat_this_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/share.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/share_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/share_button_selector_inverted.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/share_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle_not.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle_not_button_selector.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle_not_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/drawable/shuffle_pressed.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout-h600dp/player.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/activity_main.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/login.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/modify.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/notification.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/player.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/playlist_card.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/playlists.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/song_card.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/layout/songs.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/menu/quality_menu.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/menu/song_menu.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 anno3/progmobile/apollon/app/src/main/res/values-fr/strings.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values-it/strings.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values-sw/strings.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/arrays.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/colors.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/dimens.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/ic_launcher_background.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/strings.xml create mode 100644 anno3/progmobile/apollon/app/src/main/res/values/styles.xml create mode 100644 anno3/progmobile/apollon/build.gradle create mode 100644 anno3/progmobile/apollon/gradle.properties create mode 100644 anno3/progmobile/apollon/settings.gradle create mode 100644 anno3/progmobile/backend/mozapp.server/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 anno3/progmobile/backend/mozapp.server/.gradle/buildOutputCleanup/cache.properties create mode 100644 anno3/progmobile/backend/mozapp.server/.gradle/buildOutputCleanup/outputFiles.bin create mode 100644 anno3/progmobile/backend/mozapp.server/.gradle/vcs-1/gc.properties create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/.name create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/codeStyles/Project.xml create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/codeStyles/codeStyleConfig.xml create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/gradle.xml create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/misc.xml create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/vcs.xml create mode 100644 anno3/progmobile/backend/mozapp.server/.idea/workspace.xml create mode 100644 anno3/progmobile/backend/mozapp.server/README.md create mode 100644 anno3/progmobile/backend/mozapp.server/build.gradle create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/artists.json create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/covers.json create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/db2.json create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/example.py create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/lyrics.py create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/mm.py create mode 100644 anno3/progmobile/backend/mozapp.server/mpd_dir/produce_imgs.py create mode 100644 anno3/progmobile/backend/mozapp.server/playlists.json create mode 100644 anno3/progmobile/backend/mozapp.server/settings.gradle create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/Playlists.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/database.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/main.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/request.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/response.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/streaming.kt create mode 100644 anno3/progmobile/backend/mozapp.server/src/main/kotlin/thirdparties.kt create mode 100644 anno3/progmobile/backend/mozapp.server/users.json create mode 100644 anno3/progmobile/presentazione/.gitignore create mode 100644 anno3/progmobile/presentazione/css/print/paper.css create mode 100644 anno3/progmobile/presentazione/css/print/pdf.css create mode 100644 anno3/progmobile/presentazione/css/reset.css create mode 100644 anno3/progmobile/presentazione/css/reveal.css create mode 100644 anno3/progmobile/presentazione/css/reveal.scss create mode 100644 anno3/progmobile/presentazione/css/theme/Apollon.css create mode 100644 anno3/progmobile/presentazione/css/theme/README.md create mode 100644 anno3/progmobile/presentazione/css/theme/beige.css create mode 100644 anno3/progmobile/presentazione/css/theme/black.css create mode 100644 anno3/progmobile/presentazione/css/theme/blood.css create mode 100644 anno3/progmobile/presentazione/css/theme/league.css create mode 100644 anno3/progmobile/presentazione/css/theme/moon.css create mode 100644 anno3/progmobile/presentazione/css/theme/night.css create mode 100644 anno3/progmobile/presentazione/css/theme/serif.css create mode 100644 anno3/progmobile/presentazione/css/theme/simple.css create mode 100644 anno3/progmobile/presentazione/css/theme/sky.css create mode 100644 anno3/progmobile/presentazione/css/theme/solarized.css create mode 100644 anno3/progmobile/presentazione/css/theme/source/beige.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/black.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/blood.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/league.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/moon.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/night.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/serif.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/simple.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/sky.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/solarized.scss create mode 100644 anno3/progmobile/presentazione/css/theme/source/white.scss create mode 100644 anno3/progmobile/presentazione/css/theme/template/mixins.scss create mode 100644 anno3/progmobile/presentazione/css/theme/template/settings.scss create mode 100644 anno3/progmobile/presentazione/css/theme/template/theme.scss create mode 100644 anno3/progmobile/presentazione/css/theme/white.css create mode 100644 anno3/progmobile/presentazione/favicon.ico create mode 100644 anno3/progmobile/presentazione/gifs/favourite.gif create mode 100644 anno3/progmobile/presentazione/gifs/swipe.gif create mode 100644 anno3/progmobile/presentazione/img/450px-Apollon_opera_Garnier_n3.png create mode 100644 anno3/progmobile/presentazione/img/AM.png create mode 100644 anno3/progmobile/presentazione/img/ApolloCensored.png create mode 100644 anno3/progmobile/presentazione/img/Apollon.png create mode 100644 anno3/progmobile/presentazione/img/Spot.png create mode 100644 anno3/progmobile/presentazione/img/albums.png create mode 100644 anno3/progmobile/presentazione/img/am2.jpg create mode 100644 anno3/progmobile/presentazione/img/loghi.kra create mode 100644 anno3/progmobile/presentazione/img/login_framed.png create mode 100644 anno3/progmobile/presentazione/img/logo.png create mode 100644 anno3/progmobile/presentazione/img/lyrics.png create mode 100644 anno3/progmobile/presentazione/img/miniplayer.png create mode 100644 anno3/progmobile/presentazione/img/notification.png create mode 100644 anno3/progmobile/presentazione/img/player.png create mode 100644 anno3/progmobile/presentazione/img/playerArch.png create mode 100644 anno3/progmobile/presentazione/img/player_long.png create mode 100644 anno3/progmobile/presentazione/img/prova.png create mode 100644 anno3/progmobile/presentazione/img/s_arch.png create mode 100644 anno3/progmobile/presentazione/img/songs.png create mode 100644 anno3/progmobile/presentazione/index.html create mode 100644 anno3/progmobile/presentazione/js/reveal.js create mode 100644 anno3/progmobile/presentazione/lib/css/monokai.css create mode 100644 anno3/progmobile/presentazione/lib/css/zenburn.css create mode 100644 anno3/progmobile/presentazione/lib/font/league-gothic/LICENSE create mode 100644 anno3/progmobile/presentazione/lib/font/league-gothic/league-gothic.css create mode 100644 anno3/progmobile/presentazione/lib/font/league-gothic/league-gothic.eot create mode 100644 anno3/progmobile/presentazione/lib/font/league-gothic/league-gothic.ttf create mode 100644 anno3/progmobile/presentazione/lib/font/league-gothic/league-gothic.woff create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/LICENSE create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-italic.eot create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-italic.ttf create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-italic.woff create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-regular.eot create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-regular.ttf create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-regular.woff create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibold.eot create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibold.ttf create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibold.woff create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibolditalic.eot create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibolditalic.ttf create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro-semibolditalic.woff create mode 100644 anno3/progmobile/presentazione/lib/font/source-sans-pro/source-sans-pro.css create mode 100644 anno3/progmobile/presentazione/lib/js/html5shiv.js create mode 100644 anno3/progmobile/presentazione/lib/js/promise.js create mode 100644 anno3/progmobile/presentazione/plugin/highlight/highlight.js create mode 100644 anno3/progmobile/presentazione/plugin/markdown/example.html create mode 100644 anno3/progmobile/presentazione/plugin/markdown/example.md create mode 100644 anno3/progmobile/presentazione/plugin/markdown/markdown.js create mode 100644 anno3/progmobile/presentazione/plugin/markdown/marked.js create mode 100644 anno3/progmobile/presentazione/plugin/math/math.js create mode 100644 anno3/progmobile/presentazione/plugin/multiplex/client.js create mode 100644 anno3/progmobile/presentazione/plugin/multiplex/index.js create mode 100644 anno3/progmobile/presentazione/plugin/multiplex/master.js create mode 100644 anno3/progmobile/presentazione/plugin/multiplex/package.json create mode 100644 anno3/progmobile/presentazione/plugin/notes-server/client.js create mode 100644 anno3/progmobile/presentazione/plugin/notes-server/index.js create mode 100644 anno3/progmobile/presentazione/plugin/notes-server/notes.html create mode 100644 anno3/progmobile/presentazione/plugin/notes/notes.html create mode 100644 anno3/progmobile/presentazione/plugin/notes/notes.js create mode 100644 anno3/progmobile/presentazione/plugin/print-pdf/print-pdf.js create mode 100644 anno3/progmobile/presentazione/plugin/search/search.js create mode 100644 anno3/progmobile/presentazione/plugin/zoom-js/zoom.js create mode 100644 anno3/progmobile/presentazione/relazione.md create mode 100644 anno3/progmobile/presentazione/table.html diff --git a/anno3/progmobile/apollon/app/.gitignore b/anno3/progmobile/apollon/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/anno3/progmobile/apollon/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/anno3/progmobile/apollon/app/build.gradle b/anno3/progmobile/apollon/app/build.gradle new file mode 100644 index 0000000..7d45f0e --- /dev/null +++ b/anno3/progmobile/apollon/app/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.apollon" + minSdkVersion 26 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.media:media:1.0.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation("com.squareup.okhttp3:okhttp:4.1.0") + implementation 'com.squareup.picasso:picasso:2.71828' +} diff --git a/anno3/progmobile/apollon/app/proguard-rules.pro b/anno3/progmobile/apollon/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/anno3/progmobile/apollon/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/anno3/progmobile/apollon/app/src/main/AndroidManifest.xml b/anno3/progmobile/apollon/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fd0df7a --- /dev/null +++ b/anno3/progmobile/apollon/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/anno3/progmobile/apollon/app/src/main/ic_launcher-web.png b/anno3/progmobile/apollon/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..4a00e669ebd9f3bde008fd9597298b78ae1927b2 GIT binary patch literal 38617 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.id.button_previous).setOnClickListener(this) + findViewById' + + '' + + '' + + '' ); + + // Slide number + dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' ); + + // Element containing notes that are visible to the audience + dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null ); + dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' ); + dom.speakerNotes.setAttribute( 'tabindex', '0' ); + + // Overlay graphic which is displayed during the paused mode + dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '' : null ); + + dom.wrapper.setAttribute( 'role', 'application' ); + + // There can be multiple instances of controls throughout the page + dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) ); + dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) ); + dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) ); + dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) ); + dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) ); + dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) ); + + // The right and down arrows in the standard reveal.js controls + dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' ); + dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' ); + + dom.statusDiv = createStatusDiv(); + } + + /** + * Creates a hidden div with role aria-live to announce the + * current slide content. Hide the div off-screen to make it + * available only to Assistive Technologies. + * + * @return {HTMLElement} + */ + function createStatusDiv() { + + var statusDiv = document.getElementById( 'aria-status-div' ); + if( !statusDiv ) { + statusDiv = document.createElement( 'div' ); + statusDiv.style.position = 'absolute'; + statusDiv.style.height = '1px'; + statusDiv.style.width = '1px'; + statusDiv.style.overflow = 'hidden'; + statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )'; + statusDiv.setAttribute( 'id', 'aria-status-div' ); + statusDiv.setAttribute( 'aria-live', 'polite' ); + statusDiv.setAttribute( 'aria-atomic','true' ); + dom.wrapper.appendChild( statusDiv ); + } + return statusDiv; + + } + + /** + * Converts the given HTML element into a string of text + * that can be announced to a screen reader. Hidden + * elements are excluded. + */ + function getStatusText( node ) { + + var text = ''; + + // Text node + if( node.nodeType === 3 ) { + text += node.textContent; + } + // Element node + else if( node.nodeType === 1 ) { + + var isAriaHidden = node.getAttribute( 'aria-hidden' ); + var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none'; + if( isAriaHidden !== 'true' && !isDisplayHidden ) { + + toArray( node.childNodes ).forEach( function( child ) { + text += getStatusText( child ); + } ); + + } + + } + + return text; + + } + + /** + * Configures the presentation for printing to a static + * PDF. + */ + function setupPDF() { + + var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight ); + + // Dimensions of the PDF pages + var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ), + pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) ); + + // Dimensions of slides within the pages + var slideWidth = slideSize.width, + slideHeight = slideSize.height; + + // Let the browser know what page size we want to print + injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' ); + + // Limit the size of certain elements to the dimensions of the slide + injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' ); + + document.body.classList.add( 'print-pdf' ); + document.body.style.width = pageWidth + 'px'; + document.body.style.height = pageHeight + 'px'; + + // Make sure stretch elements fit on slide + layoutSlideContents( slideWidth, slideHeight ); + + // Compute slide numbers now, before we start duplicating slides + var doingSlideNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber ); + toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { + slide.setAttribute( 'data-slide-number', getSlideNumber( slide ) ); + } ); + + // Slide and slide background layout + toArray( dom.wrapper.querySelectorAll( 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 + var left = ( pageWidth - slideWidth ) / 2, + top = ( pageHeight - slideHeight ) / 2; + + var contentHeight = slide.scrollHeight; + var 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 + var 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? + var notes = getSlideNotes( slide ); + if( notes ) { + + var notesSpacing = 8; + var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline'; + var 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 ) { + var 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. + var fragmentGroups = sortFragments( page.querySelectorAll( '.fragment' ), true ); + + var previousFragmentStep; + var 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' ); + } ); + + // Create a separate page for the current fragment state + var clonedPage = page.cloneNode( true ); + page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling ); + + previousFragmentStep = fragments; + previousPage = clonedPage; + + } ); + + // 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 { + toArray( page.querySelectorAll( '.fragment:not(.fade-out)' ) ).forEach( function( fragment ) { + fragment.classList.add( 'visible' ); + } ); + } + + } + + } ); + + // Notify subscribers that the PDF layout is good to go + dispatchEvent( 'pdf-ready' ); + + } + + /** + * This is an unfortunate necessity. Some actions – such as + * an input field being focused in an iframe or using the + * keyboard to expand text selection beyond the bounds of + * a slide – can trigger our content to be pushed out of view. + * This scrolling can not be prevented by hiding overflow in + * CSS (we already do) so we have to resort to repeatedly + * checking if the slides have been offset :( + */ + function setupScrollPrevention() { + + setInterval( function() { + if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) { + dom.wrapper.scrollTop = 0; + dom.wrapper.scrollLeft = 0; + } + }, 1000 ); + + } + + /** + * 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} + */ + function createSingletonNode( container, tagname, classname, innerHTML ) { + + // Find all nodes matching the description + var nodes = container.querySelectorAll( '.' + classname ); + + // Check all matches to find one which is a direct child of + // the specified container + for( var i = 0; i < nodes.length; i++ ) { + var testNode = nodes[i]; + if( testNode.parentNode === container ) { + return testNode; + } + } + + // If no node was found, create it now + var node = document.createElement( tagname ); + node.className = classname; + if( typeof innerHTML === 'string' ) { + node.innerHTML = innerHTML; + } + container.appendChild( node ); + + return node; + + } + + /** + * 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. + */ + function createBackgrounds() { + + var printMode = isPrintingPDF(); + + // Clear prior backgrounds + dom.background.innerHTML = ''; + dom.background.classList.add( 'no-transition' ); + + // Iterate over all horizontal slides + toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) { + + var backgroundStack = createBackground( slideh, dom.background ); + + // Iterate over all vertical slides + toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) { + + createBackground( slidev, backgroundStack ); + + backgroundStack.classList.add( 'stack' ); + + } ); + + } ); + + // Add parallax background if specified + if( config.parallaxBackgroundImage ) { + + dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")'; + dom.background.style.backgroundSize = config.parallaxBackgroundSize; + dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat; + dom.background.style.backgroundPosition = config.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( function() { + dom.wrapper.classList.add( 'has-parallax-background' ); + }, 1 ); + + } + else { + + dom.background.style.backgroundImage = ''; + dom.wrapper.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 + */ + function createBackground( slide, container ) { + + + // Main slide background element + var element = document.createElement( 'div' ); + element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); + + // Inner background element that wraps images/videos/iframes + var 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 + syncBackground( slide ); + + return element; + + } + + /** + * Renders all of the visual properties of a slide background + * based on the various background attributes. + * + * @param {HTMLElement} slide + */ + function syncBackground( slide ) { + + var 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 = ''; + + var 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 + var contrastColor = data.backgroundColor; + + // If no bg color was found, check the computed background + if( !contrastColor ) { + var computedBackgroundStyle = window.getComputedStyle( element ); + if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { + contrastColor = computedBackgroundStyle.backgroundColor; + } + } + + if( contrastColor ) { + var 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' ); + } + } + } + + } + + /** + * Registers a listener to postMessage events, this makes it + * possible to call all reveal.js API methods from another + * window. For example: + * + * revealWindow.postMessage( JSON.stringify({ + * method: 'slide', + * args: [ 2 ] + * }), '*' ); + */ + function setupPostMessage() { + + if( config.postMessage ) { + window.addEventListener( 'message', function ( event ) { + var data = event.data; + + // Make sure we're dealing with JSON + if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) { + data = JSON.parse( data ); + + // Check if the requested method can be found + if( data.method && typeof Reveal[data.method] === 'function' ) { + + if( POST_MESSAGE_METHOD_BLACKLIST.test( data.method ) === false ) { + + var result = Reveal[data.method].apply( Reveal, data.args ); + + // Dispatch a postMessage event with the returned value from + // our method invocation for getter functions + dispatchPostMessage( 'callback', { method: data.method, result: result } ); + + } + else { + console.warn( 'reveal.js: "'+ data.method +'" is is blacklisted from the postMessage API' ); + } + + } + } + }, false ); + } + + } + + /** + * Applies the configuration settings from the config + * object. May be called multiple times. + * + * @param {object} options + */ + function configure( options ) { + + var oldTransition = config.transition; + + // New config options may be passed when this method + // is invoked through the API after initialization + if( typeof options === 'object' ) extend( config, options ); + + // Abort if reveal.js hasn't finished loading, config + // changes will be applied automatically once loading + // finishes + if( loaded === false ) return; + + var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length; + + // Remove the previously configured transition class + dom.wrapper.classList.remove( oldTransition ); + + // Force linear transition based on browser capabilities + if( features.transforms3d === false ) config.transition = 'linear'; + + dom.wrapper.classList.add( config.transition ); + + dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed ); + dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition ); + + dom.controls.style.display = config.controls ? 'block' : 'none'; + dom.progress.style.display = config.progress ? 'block' : 'none'; + + dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout ); + dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows ); + + if( config.shuffle ) { + shuffle(); + } + + if( config.rtl ) { + dom.wrapper.classList.add( 'rtl' ); + } + else { + dom.wrapper.classList.remove( 'rtl' ); + } + + if( config.center ) { + dom.wrapper.classList.add( 'center' ); + } + else { + dom.wrapper.classList.remove( 'center' ); + } + + // Exit the paused mode if it was configured off + if( config.pause === false ) { + resume(); + } + + if( config.showNotes ) { + dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' ); + } + + if( config.mouseWheel ) { + document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF + document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); + } + else { + document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF + document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false ); + } + + // Rolling 3D links + if( config.rollingLinks ) { + enableRollingLinks(); + } + else { + disableRollingLinks(); + } + + // Auto-hide the mouse pointer when its inactive + if( config.hideInactiveCursor ) { + document.addEventListener( 'mousemove', onDocumentCursorActive, false ); + document.addEventListener( 'mousedown', onDocumentCursorActive, false ); + } + else { + showCursor(); + + document.removeEventListener( 'mousemove', onDocumentCursorActive, false ); + document.removeEventListener( 'mousedown', onDocumentCursorActive, false ); + } + + // Iframe link previews + if( config.previewLinks ) { + enablePreviewLinks(); + disablePreviewLinks( '[data-preview-link=false]' ); + } + else { + disablePreviewLinks(); + enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' ); + } + + // Remove existing auto-slide controls + if( autoSlidePlayer ) { + autoSlidePlayer.destroy(); + autoSlidePlayer = null; + } + + // Generate auto-slide controls if needed + if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) { + autoSlidePlayer = new Playback( dom.wrapper, function() { + return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 ); + } ); + + autoSlidePlayer.on( 'click', onAutoSlidePlayerClick ); + autoSlidePaused = false; + } + + // When fragments are turned off they should be visible + if( config.fragments === false ) { + toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) { + element.classList.add( 'visible' ); + element.classList.remove( 'current-fragment' ); + } ); + } + + // Slide numbers + var slideNumberDisplay = 'none'; + if( config.slideNumber && !isPrintingPDF() ) { + if( config.showSlideNumber === 'all' ) { + slideNumberDisplay = 'block'; + } + else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) { + slideNumberDisplay = 'block'; + } + } + + dom.slideNumber.style.display = slideNumberDisplay; + + // Add the navigation mode to the DOM so we can adjust styling + if( config.navigationMode !== 'default' ) { + dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode ); + } + else { + dom.wrapper.removeAttribute( 'data-navigation-mode' ); + } + + // Define our contextual list of keyboard shortcuts + if( config.navigationMode === 'linear' ) { + keyboardShortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide'; + keyboardShortcuts['← , ↑ , P , H , K'] = 'Previous slide'; + } + else { + keyboardShortcuts['N , SPACE'] = 'Next slide'; + keyboardShortcuts['P'] = 'Previous slide'; + keyboardShortcuts['← , H'] = 'Navigate left'; + keyboardShortcuts['→ , L'] = 'Navigate right'; + keyboardShortcuts['↑ , K'] = 'Navigate up'; + keyboardShortcuts['↓ , J'] = 'Navigate down'; + } + + keyboardShortcuts['Home , Shift ←'] = 'First slide'; + keyboardShortcuts['End , Shift →'] = 'Last slide'; + keyboardShortcuts['B , .'] = 'Pause'; + keyboardShortcuts['F'] = 'Fullscreen'; + keyboardShortcuts['ESC, O'] = 'Slide overview'; + + sync(); + + } + + /** + * Binds all event listeners. + */ + function addEventListeners() { + + eventsAreBound = true; + + window.addEventListener( 'hashchange', onWindowHashChange, false ); + window.addEventListener( 'resize', onWindowResize, false ); + + if( config.touch ) { + if( 'onpointerdown' in window ) { + // Use W3C pointer events + dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false ); + dom.wrapper.addEventListener( 'pointermove', onPointerMove, false ); + dom.wrapper.addEventListener( 'pointerup', onPointerUp, false ); + } + else if( window.navigator.msPointerEnabled ) { + // IE 10 uses prefixed version of pointer events + dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false ); + dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); + dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); + } + else { + // Fall back to touch events + dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); + dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); + dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); + } + } + + if( config.keyboard ) { + document.addEventListener( 'keydown', onDocumentKeyDown, false ); + document.addEventListener( 'keypress', onDocumentKeyPress, false ); + } + + if( config.progress && dom.progress ) { + dom.progress.addEventListener( 'click', onProgressClicked, false ); + } + + dom.pauseOverlay.addEventListener( 'click', resume, false ); + + if( config.focusBodyOnPageVisibilityChange ) { + var visibilityChange; + + if( 'hidden' in document ) { + visibilityChange = 'visibilitychange'; + } + else if( 'msHidden' in document ) { + visibilityChange = 'msvisibilitychange'; + } + else if( 'webkitHidden' in document ) { + visibilityChange = 'webkitvisibilitychange'; + } + + if( visibilityChange ) { + document.addEventListener( visibilityChange, onPageVisibilityChange, false ); + } + } + + // Listen to both touch and click events, in case the device + // supports both + var pointerEvents = [ 'touchstart', 'click' ]; + + // Only support touch for Android, fixes double navigations in + // stock browser + if( UA.match( /android/gi ) ) { + pointerEvents = [ 'touchstart' ]; + } + + pointerEvents.forEach( function( eventName ) { + dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } ); + dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } ); + dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } ); + } ); + + } + + /** + * Unbinds all event listeners. + */ + function removeEventListeners() { + + eventsAreBound = false; + + document.removeEventListener( 'keydown', onDocumentKeyDown, false ); + document.removeEventListener( 'keypress', onDocumentKeyPress, false ); + window.removeEventListener( 'hashchange', onWindowHashChange, false ); + window.removeEventListener( 'resize', onWindowResize, false ); + + dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false ); + dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false ); + dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false ); + + dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); + dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); + dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); + + dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false ); + dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); + dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); + + dom.pauseOverlay.removeEventListener( 'click', resume, false ); + + if ( config.progress && dom.progress ) { + dom.progress.removeEventListener( 'click', onProgressClicked, false ); + } + + [ 'touchstart', 'click' ].forEach( function( eventName ) { + dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } ); + dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } ); + dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } ); + } ); + + } + + /** + * 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()`. + */ + function registerPlugin( id, plugin ) { + + if( plugins[id] === undefined ) { + plugins[id] = plugin; + + // If a plugin is registered after reveal.js is loaded, + // initialize it right away + if( loaded && typeof plugin.init === 'function' ) { + plugin.init(); + } + } + 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 + */ + function hasPlugin( id ) { + + return !!plugins[id]; + + } + + /** + * Returns the specific plugin instance, if a plugin + * with the given ID has been registered. + * + * @param {String} id Unique plugin identifier + */ + function getPlugin( id ) { + + return plugins[id]; + + } + + /** + * Add a custom key binding with optional description to + * be added to the help screen. + */ + function addKeyBinding( binding, callback ) { + + if( typeof binding === 'object' && binding.keyCode ) { + registeredKeyBindings[binding.keyCode] = { + callback: callback, + key: binding.key, + description: binding.description + }; + } + else { + registeredKeyBindings[binding] = { + callback: callback, + key: null, + description: null + }; + } + + } + + /** + * Removes the specified custom key binding. + */ + function removeKeyBinding( keyCode ) { + + delete registeredKeyBindings[keyCode]; + + } + + /** + * Extend object a with the properties of object b. + * If there's a conflict, object b takes precedence. + * + * @param {object} a + * @param {object} b + */ + function extend( a, b ) { + + for( var i in b ) { + a[ i ] = b[ i ]; + } + + return a; + + } + + /** + * Converts the target object to an array. + * + * @param {object} o + * @return {object[]} + */ + function toArray( o ) { + + return Array.prototype.slice.call( o ); + + } + + /** + * Utility for deserializing a value. + * + * @param {*} value + * @return {*} + */ + function 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} + */ + function distanceBetween( a, b ) { + + var 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 + */ + function transformElement( element, transform ) { + + element.style.WebkitTransform = transform; + element.style.MozTransform = transform; + element.style.msTransform = transform; + element.style.transform = transform; + + } + + /** + * Applies CSS transforms to the slides container. The container + * is transformed from two separate sources: layout and the overview + * mode. + * + * @param {object} transforms + */ + function transformSlides( transforms ) { + + // Pick up new transforms from arguments + if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout; + if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview; + + // Apply the transforms to the slides container + if( slidesTransform.layout ) { + transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview ); + } + else { + transformElement( dom.slides, slidesTransform.overview ); + } + + } + + /** + * Injects the given CSS styles into the DOM. + * + * @param {string} value + */ + function injectStyleSheet( value ) { + + var tag = document.createElement( 'style' ); + tag.type = 'text/css'; + if( tag.styleSheet ) { + tag.styleSheet.cssText = value; + } + else { + tag.appendChild( document.createTextNode( value ) ); + } + document.getElementsByTagName( 'head' )[0].appendChild( tag ); + + } + + /** + * 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 + */ + function closestParent( target, selector ) { + + var parent = target.parentNode; + + while( parent ) { + + // There's some overhead doing this each time, we don't + // want to rewrite the element prototype but should still + // be enough to feature detect once at startup... + var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector; + + // If we find a match, we're all set + if( matchesMethod && matchesMethod.call( parent, selector ) ) { + return parent; + } + + // Keep searching + parent = parent.parentNode; + + } + + return null; + + } + + /** + * 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} + */ + function colorToRgb( color ) { + + var 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 + }; + } + + var 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 ) + }; + } + + var 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 ) + }; + } + + var 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} + */ + function colorBrightness( color ) { + + if( typeof color === 'string' ) color = colorToRgb( color ); + + if( color ) { + return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000; + } + + return null; + + } + + /** + * 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] + */ + function getRemainingHeight( element, height ) { + + height = height || 0; + + if( element ) { + var 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; + + } + + /** + * Checks if this instance is being used to print a PDF. + */ + function isPrintingPDF() { + + return ( /print-pdf/gi ).test( window.location.search ); + + } + + /** + * Hides the address bar if we're on a mobile device. + */ + function hideAddressBar() { + + if( config.hideAddressBar && isMobileDevice ) { + // Events that should trigger the address bar to hide + window.addEventListener( 'load', removeAddressBar, false ); + window.addEventListener( 'orientationchange', removeAddressBar, false ); + } + + } + + /** + * Causes the address bar to hide on mobile devices, + * more vertical space ftw. + */ + function removeAddressBar() { + + setTimeout( function() { + window.scrollTo( 0, 1 ); + }, 10 ); + + } + + /** + * Dispatches an event of the specified type from the + * reveal DOM element. + */ + function dispatchEvent( type, args ) { + + var event = document.createEvent( 'HTMLEvents', 1, 2 ); + event.initEvent( type, true, true ); + extend( event, args ); + dom.wrapper.dispatchEvent( event ); + + // If we're in an iframe, post each reveal.js event to the + // parent window. Used by the notes plugin + dispatchPostMessage( type ); + + } + + /** + * Dispatched a postMessage of the given type from our window. + */ + function dispatchPostMessage( type, data ) { + + if( config.postMessageEvents && window.parent !== window.self ) { + var message = { + namespace: 'reveal', + eventName: type, + state: getState() + }; + + extend( message, data ); + + window.parent.postMessage( JSON.stringify( message ), '*' ); + } + + } + + /** + * Wrap all links in 3D goodness. + */ + function enableRollingLinks() { + + if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) { + var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' ); + + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; + + if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) { + var span = document.createElement('span'); + span.setAttribute('data-title', anchor.text); + span.innerHTML = anchor.innerHTML; + + anchor.classList.add( 'roll' ); + anchor.innerHTML = ''; + anchor.appendChild(span); + } + } + } + + } + + /** + * Unwrap all 3D links. + */ + function disableRollingLinks() { + + var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' ); + + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; + var span = anchor.querySelector( 'span' ); + + if( span ) { + anchor.classList.remove( 'roll' ); + anchor.innerHTML = span.innerHTML; + } + } + + } + + /** + * Bind preview frame links. + * + * @param {string} [selector=a] - selector for anchors + */ + function enablePreviewLinks( selector ) { + + var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) ); + + anchors.forEach( function( element ) { + if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { + element.addEventListener( 'click', onPreviewLinkClicked, false ); + } + } ); + + } + + /** + * Unbind preview frame links. + */ + function disablePreviewLinks( selector ) { + + var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) ); + + anchors.forEach( function( element ) { + if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { + element.removeEventListener( 'click', onPreviewLinkClicked, false ); + } + } ); + + } + + /** + * Opens a preview window for the target URL. + * + * @param {string} url - url for preview iframe src + */ + function showPreview( url ) { + + closeOverlay(); + + dom.overlay = document.createElement( 'div' ); + dom.overlay.classList.add( 'overlay' ); + dom.overlay.classList.add( 'overlay-preview' ); + dom.wrapper.appendChild( dom.overlay ); + + dom.overlay.innerHTML = [ + '
', + '', + '', + '
', + '
', + '
', + '', + '', + 'Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).', + '', + '
' + ].join(''); + + dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) { + dom.overlay.classList.add( 'loaded' ); + }, false ); + + dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) { + closeOverlay(); + event.preventDefault(); + }, false ); + + dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) { + closeOverlay(); + }, false ); + + setTimeout( function() { + dom.overlay.classList.add( 'visible' ); + }, 1 ); + + } + + /** + * Open or close help overlay window. + * + * @param {Boolean} [override] Flag which overrides the + * toggle logic and forcibly sets the desired state. True means + * help is open, false means it's closed. + */ + function toggleHelp( override ){ + + if( typeof override === 'boolean' ) { + override ? showHelp() : closeOverlay(); + } + else { + if( dom.overlay ) { + closeOverlay(); + } + else { + showHelp(); + } + } + } + + /** + * Opens an overlay window with help material. + */ + function showHelp() { + + if( config.help ) { + + closeOverlay(); + + dom.overlay = document.createElement( 'div' ); + dom.overlay.classList.add( 'overlay' ); + dom.overlay.classList.add( 'overlay-help' ); + dom.wrapper.appendChild( dom.overlay ); + + var html = '

Keyboard Shortcuts


'; + + html += ''; + for( var key in keyboardShortcuts ) { + html += ''; + } + + // Add custom key bindings that have associated descriptions + for( var binding in registeredKeyBindings ) { + if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) { + html += ''; + } + } + + html += '
KEYACTION
' + key + '' + keyboardShortcuts[ key ] + '
' + registeredKeyBindings[binding].key + '' + registeredKeyBindings[binding].description + '
'; + + dom.overlay.innerHTML = [ + '
', + '', + '
', + '
', + '
'+ html +'
', + '
' + ].join(''); + + dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) { + closeOverlay(); + event.preventDefault(); + }, false ); + + setTimeout( function() { + dom.overlay.classList.add( 'visible' ); + }, 1 ); + + } + + } + + /** + * Closes any currently open overlay. + */ + function closeOverlay() { + + if( dom.overlay ) { + dom.overlay.parentNode.removeChild( dom.overlay ); + dom.overlay = null; + } + + } + + /** + * Applies JavaScript-controlled layout rules to the + * presentation. + */ + function layout() { + + if( dom.wrapper && !isPrintingPDF() ) { + + if( !config.disableLayout ) { + + // On some mobile devices '100vh' is taller than the visible + // viewport which leads to part of the presentation being + // cut off. To work around this we define our own '--vh' custom + // property where 100x adds up to the correct height. + // + // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + if( isMobileDevice ) { + document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' ); + } + + var size = getComputedSlideSize(); + + var oldScale = scale; + + // Layout the contents of the slides + layoutSlideContents( config.width, config.height ); + + dom.slides.style.width = size.width + 'px'; + dom.slides.style.height = size.height + 'px'; + + // Determine scale of content to fit within available space + scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height ); + + // Respect max/min scale settings + scale = Math.max( scale, config.minScale ); + scale = Math.min( scale, config.maxScale ); + + // Don't apply any scaling styles if scale is 1 + if( scale === 1 ) { + dom.slides.style.zoom = ''; + dom.slides.style.left = ''; + dom.slides.style.top = ''; + dom.slides.style.bottom = ''; + dom.slides.style.right = ''; + transformSlides( { layout: '' } ); + } + else { + // Zoom Scaling + // Content remains crisp no matter how much we scale. Side + // effects are minor differences in text layout and iframe + // viewports changing size. A 200x200 iframe viewport in a + // 2x zoomed presentation ends up having a 400x400 viewport. + if( scale > 1 && features.zoom && window.devicePixelRatio < 2 ) { + dom.slides.style.zoom = scale; + dom.slides.style.left = ''; + dom.slides.style.top = ''; + dom.slides.style.bottom = ''; + dom.slides.style.right = ''; + transformSlides( { layout: '' } ); + } + // Transform Scaling + // Content layout remains the exact same when scaled up. + // Side effect is content becoming blurred, especially with + // high scale values on ldpi screens. + else { + dom.slides.style.zoom = ''; + dom.slides.style.left = '50%'; + dom.slides.style.top = '50%'; + dom.slides.style.bottom = 'auto'; + dom.slides.style.right = 'auto'; + transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } ); + } + } + + // Select all slides, vertical and horizontal + var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ); + + for( var i = 0, len = slides.length; i < len; i++ ) { + var slide = slides[ i ]; + + // Don't bother updating invisible slides + if( slide.style.display === 'none' ) { + continue; + } + + if( config.center || slide.classList.contains( 'center' ) ) { + // Vertical stacks are not centred since their section + // children will be + if( slide.classList.contains( 'stack' ) ) { + slide.style.top = 0; + } + else { + slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px'; + } + } + else { + slide.style.top = ''; + } + + } + + if( oldScale !== scale ) { + dispatchEvent( 'resize', { + 'oldScale': oldScale, + 'scale': scale, + 'size': size + } ); + } + } + + updateProgress(); + updateParallax(); + + if( isOverview() ) { + updateOverview(); + } + + } + + } + + /** + * Applies layout logic to the contents of all slides in + * the presentation. + * + * @param {string|number} width + * @param {string|number} height + */ + function layoutSlideContents( width, height ) { + + // Handle sizing of elements with the 'stretch' class + toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) { + + // Determine how much vertical space we can use + var remainingHeight = getRemainingHeight( element, height ); + + // Consider the aspect ratio of media elements + if( /(img|video)/gi.test( element.nodeName ) ) { + var nw = element.naturalWidth || element.videoWidth, + nh = element.naturalHeight || element.videoHeight; + + var es = Math.min( width / nw, remainingHeight / nh ); + + element.style.width = ( nw * es ) + 'px'; + element.style.height = ( nh * es ) + 'px'; + + } + else { + element.style.width = width + 'px'; + element.style.height = remainingHeight + 'px'; + } + + } ); + + } + + /** + * Calculates the computed pixel size of our slides. These + * values are based on the width and height configuration + * options. + * + * @param {number} [presentationWidth=dom.wrapper.offsetWidth] + * @param {number} [presentationHeight=dom.wrapper.offsetHeight] + */ + function getComputedSlideSize( presentationWidth, presentationHeight ) { + + var size = { + // Slide size + width: config.width, + height: config.height, + + // Presentation size + presentationWidth: presentationWidth || dom.wrapper.offsetWidth, + presentationHeight: presentationHeight || dom.wrapper.offsetHeight + }; + + // Reduce available space by margin + size.presentationWidth -= ( size.presentationWidth * config.margin ); + size.presentationHeight -= ( size.presentationHeight * config.margin ); + + // Slide width may be a percentage of available width + if( typeof size.width === 'string' && /%$/.test( size.width ) ) { + size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth; + } + + // Slide height may be a percentage of available height + if( typeof size.height === 'string' && /%$/.test( size.height ) ) { + size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight; + } + + return size; + + } + + /** + * Stores the vertical index of a stack so that the same + * vertical slide can be selected when navigating to and + * from the stack. + * + * @param {HTMLElement} stack The vertical stack element + * @param {string|number} [v=0] Index to memorize + */ + function setPreviousVerticalIndex( stack, v ) { + + if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) { + stack.setAttribute( 'data-previous-indexv', v || 0 ); + } + + } + + /** + * Retrieves the vertical index which was stored using + * #setPreviousVerticalIndex() or 0 if no previous index + * exists. + * + * @param {HTMLElement} stack The vertical stack element + */ + function getPreviousVerticalIndex( stack ) { + + if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) { + // Prefer manually defined start-indexv + var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv'; + + return parseInt( stack.getAttribute( attributeName ) || 0, 10 ); + } + + return 0; + + } + + /** + * Displays the overview of slides (quick nav) by scaling + * down and arranging all slide elements. + */ + function activateOverview() { + + // Only proceed if enabled in config + if( config.overview && !isOverview() ) { + + overview = true; + + dom.wrapper.classList.add( 'overview' ); + dom.wrapper.classList.remove( 'overview-deactivating' ); + + if( features.overviewTransitions ) { + setTimeout( function() { + dom.wrapper.classList.add( 'overview-animated' ); + }, 1 ); + } + + // Don't auto-slide while in overview mode + cancelAutoSlide(); + + // Move the backgrounds element into the slide container to + // that the same scaling is applied + dom.slides.appendChild( dom.background ); + + // Clicking on an overview slide navigates to it + toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { + if( !slide.classList.contains( 'stack' ) ) { + slide.addEventListener( 'click', onOverviewSlideClicked, true ); + } + } ); + + // Calculate slide sizes + var margin = 70; + var slideSize = getComputedSlideSize(); + overviewSlideWidth = slideSize.width + margin; + overviewSlideHeight = slideSize.height + margin; + + // Reverse in RTL mode + if( config.rtl ) { + overviewSlideWidth = -overviewSlideWidth; + } + + updateSlidesVisibility(); + layoutOverview(); + updateOverview(); + + layout(); + + // Notify observers of the overview showing + dispatchEvent( 'overviewshown', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + + } + + } + + /** + * Uses CSS transforms to position all slides in a grid for + * display inside of the overview mode. + */ + function layoutOverview() { + + // Layout slides + toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) { + hslide.setAttribute( 'data-index-h', h ); + transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' ); + + if( hslide.classList.contains( 'stack' ) ) { + + toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) { + vslide.setAttribute( 'data-index-h', h ); + vslide.setAttribute( 'data-index-v', v ); + + transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' ); + } ); + + } + } ); + + // Layout slide backgrounds + toArray( dom.background.childNodes ).forEach( function( hbackground, h ) { + transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' ); + + toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) { + transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' ); + } ); + } ); + + } + + /** + * Moves the overview viewport to the current slides. + * Called each time the current slide changes. + */ + function updateOverview() { + + var vmin = Math.min( window.innerWidth, window.innerHeight ); + var scale = Math.max( vmin / 5, 150 ) / vmin; + + transformSlides( { + overview: [ + 'scale('+ scale +')', + 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)', + 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)' + ].join( ' ' ) + } ); + + } + + /** + * Exits the slide overview and enters the currently + * active slide. + */ + function deactivateOverview() { + + // Only proceed if enabled in config + if( config.overview ) { + + overview = false; + + dom.wrapper.classList.remove( 'overview' ); + dom.wrapper.classList.remove( 'overview-animated' ); + + // 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 + dom.wrapper.classList.add( 'overview-deactivating' ); + + setTimeout( function () { + dom.wrapper.classList.remove( 'overview-deactivating' ); + }, 1 ); + + // Move the background element back out + dom.wrapper.appendChild( dom.background ); + + // Clean up changes made to slides + toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { + transformElement( slide, '' ); + + slide.removeEventListener( 'click', onOverviewSlideClicked, true ); + } ); + + // Clean up changes made to backgrounds + toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) { + transformElement( background, '' ); + } ); + + transformSlides( { overview: '' } ); + + slide( indexh, indexv ); + + layout(); + + cueAutoSlide(); + + // Notify observers of the overview hiding + dispatchEvent( 'overviewhidden', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + + } + } + + /** + * 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. + */ + function toggleOverview( override ) { + + if( typeof override === 'boolean' ) { + override ? activateOverview() : deactivateOverview(); + } + else { + isOverview() ? deactivateOverview() : activateOverview(); + } + + } + + /** + * Checks if the overview is currently active. + * + * @return {Boolean} true if the overview is active, + * false otherwise + */ + function isOverview() { + + return overview; + + } + + /** + * Return a hash URL that will resolve to the given slide location. + * + * @param {HTMLElement} [slide=currentSlide] The slide to link to + */ + function locationHash( slide ) { + + var url = '/'; + + // Attempt to create a named link based on the slide's ID + var s = slide || currentSlide; + var id = s ? s.getAttribute( 'id' ) : null; + if( id ) { + id = encodeURIComponent( id ); + } + + var index = getIndices( slide ); + if( !config.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 && index.f === undefined ) { + url = '/' + id; + } + // Otherwise use the /h/v index + else { + var hashIndexBase = config.hashOneBasedIndex ? 1 : 0; + if( index.h > 0 || index.v > 0 || index.f !== undefined ) url += index.h + hashIndexBase; + if( index.v > 0 || index.f !== undefined ) url += '/' + (index.v + hashIndexBase ); + if( index.f !== undefined ) url += '/' + index.f; + } + + return url; + + } + + /** + * Checks if the current or specified slide is vertical + * (nested within another slide). + * + * @param {HTMLElement} [slide=currentSlide] The slide to check + * orientation of + * @return {Boolean} + */ + function isVerticalSlide( slide ) { + + // Prefer slide argument, otherwise use current slide + slide = slide ? slide : currentSlide; + + return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i ); + + } + + /** + * 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 + */ + function enterFullscreen() { + + var element = document.documentElement; + + // Check which implementation is available + var requestMethod = element.requestFullscreen || + element.webkitRequestFullscreen || + element.webkitRequestFullScreen || + element.mozRequestFullScreen || + element.msRequestFullscreen; + + if( requestMethod ) { + requestMethod.apply( element ); + } + + } + + /** + * Shows the mouse pointer after it has been hidden with + * #hideCursor. + */ + function showCursor() { + + if( cursorHidden ) { + cursorHidden = false; + dom.wrapper.style.cursor = ''; + } + + } + + /** + * Hides the mouse pointer when it's on top of the .reveal + * container. + */ + function hideCursor() { + + if( cursorHidden === false ) { + cursorHidden = true; + dom.wrapper.style.cursor = 'none'; + } + + } + + /** + * Enters the paused mode which fades everything on screen to + * black. + */ + function pause() { + + if( config.pause ) { + var wasPaused = dom.wrapper.classList.contains( 'paused' ); + + cancelAutoSlide(); + dom.wrapper.classList.add( 'paused' ); + + if( wasPaused === false ) { + dispatchEvent( 'paused' ); + } + } + + } + + /** + * Exits from the paused mode. + */ + function resume() { + + var wasPaused = dom.wrapper.classList.contains( 'paused' ); + dom.wrapper.classList.remove( 'paused' ); + + cueAutoSlide(); + + if( wasPaused ) { + dispatchEvent( 'resumed' ); + } + + } + + /** + * Toggles the paused mode on and off. + */ + function togglePause( override ) { + + if( typeof override === 'boolean' ) { + override ? pause() : resume(); + } + else { + isPaused() ? resume() : pause(); + } + + } + + /** + * Checks if we are currently in the paused mode. + * + * @return {Boolean} + */ + function isPaused() { + + return dom.wrapper.classList.contains( 'paused' ); + + } + + /** + * Toggles the auto slide mode on and off. + * + * @param {Boolean} [override] Flag which sets the desired state. + * True means autoplay starts, false means it stops. + */ + + function toggleAutoSlide( override ) { + + if( typeof override === 'boolean' ) { + override ? resumeAutoSlide() : pauseAutoSlide(); + } + + else { + autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide(); + } + + } + + /** + * Checks if the auto slide mode is currently on. + * + * @return {Boolean} + */ + function isAutoSliding() { + + return !!( autoSlide && !autoSlidePaused ); + + } + + /** + * Steps from the current point in the presentation to the + * slide which matches the specified horizontal and vertical + * indices. + * + * @param {number} [h=indexh] Horizontal index of the target slide + * @param {number} [v=indexv] Vertical index of the target slide + * @param {number} [f] Index of a fragment within the + * target slide to activate + * @param {number} [o] Origin for use in multimaster environments + */ + function slide( h, v, f, o ) { + + // Remember where we were at before + previousSlide = currentSlide; + + // Query all horizontal slides in the deck + var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); + + // Abort if there are no slides + if( horizontalSlides.length === 0 ) return; + + // If no vertical index is specified and the upcoming slide is a + // stack, resume at its previous vertical index + if( v === undefined && !isOverview() ) { + v = getPreviousVerticalIndex( horizontalSlides[ h ] ); + } + + // If we were on a vertical stack, remember what vertical index + // it was on so we can resume at the same position when returning + if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) { + setPreviousVerticalIndex( previousSlide.parentNode, indexv ); + } + + // Remember the state before this slide + var stateBefore = state.concat(); + + // Reset the state array + state.length = 0; + + var indexhBefore = indexh || 0, + indexvBefore = indexv || 0; + + // Activate and transition to the new slide + indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); + indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); + + // Update the visibility of slides now that the indices have changed + updateSlidesVisibility(); + + layout(); + + // Update the overview if it's currently active + if( isOverview() ) { + updateOverview(); + } + + // Find the current horizontal slide and any possible vertical slides + // within it + var currentHorizontalSlide = horizontalSlides[ indexh ], + currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); + + // Store references to the previous and current slides + currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; + + // Show fragment, if specified + if( typeof f !== 'undefined' ) { + navigateFragment( f ); + } + + // Dispatch an event if the slide changed + var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); + if (!slideChanged) { + // Ensure that the previous slide is never the same as the current + previousSlide = null; + } + + // Solves an edge case where the previous slide maintains the + // 'present' class when navigating between adjacent vertical + // stacks + if( previousSlide && previousSlide !== currentSlide ) { + previousSlide.classList.remove( 'present' ); + previousSlide.setAttribute( 'aria-hidden', 'true' ); + + // Reset all slides upon navigate to home + // Issue: #285 + if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) { + // Launch async task + setTimeout( function () { + var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i; + for( i in slides ) { + if( slides[i] ) { + // Reset stack + setPreviousVerticalIndex( slides[i], 0 ); + } + } + }, 0 ); + } + } + + // Apply the new state + stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { + // Check if this state existed on the previous slide. If it + // did, we will avoid adding it repeatedly + for( var j = 0; j < stateBefore.length; j++ ) { + if( stateBefore[j] === state[i] ) { + stateBefore.splice( j, 1 ); + continue stateLoop; + } + } + + document.documentElement.classList.add( state[i] ); + + // Dispatch custom event matching the state's name + dispatchEvent( state[i] ); + } + + // Clean up the remains of the previous state + while( stateBefore.length ) { + document.documentElement.classList.remove( stateBefore.pop() ); + } + + if( slideChanged ) { + dispatchEvent( 'slidechanged', { + 'indexh': indexh, + 'indexv': indexv, + 'previousSlide': previousSlide, + 'currentSlide': currentSlide, + 'origin': o + } ); + } + + // Handle embedded content + if( slideChanged || !previousSlide ) { + stopEmbeddedContent( previousSlide ); + startEmbeddedContent( currentSlide ); + } + + // Announce the current slide contents, for screen readers + dom.statusDiv.textContent = getStatusText( currentSlide ); + + updateControls(); + updateProgress(); + updateBackground(); + updateParallax(); + updateSlideNumber(); + updateNotes(); + updateFragments(); + + // Update the URL hash + writeURL(); + + cueAutoSlide(); + + } + + /** + * Syncs the presentation with the current DOM. Useful + * when new slides or control elements are added or when + * the configuration has changed. + */ + function sync() { + + // Subscribe to input + removeEventListeners(); + addEventListeners(); + + // Force a layout to make sure the current config is accounted for + layout(); + + // Reflect the current autoSlide value + autoSlide = config.autoSlide; + + // Start auto-sliding if it's enabled + cueAutoSlide(); + + // Re-create the slide backgrounds + createBackgrounds(); + + // Write the current hash to the URL + writeURL(); + + sortAllFragments(); + + updateControls(); + updateProgress(); + updateSlideNumber(); + updateSlidesVisibility(); + updateBackground( true ); + updateNotesVisibility(); + updateNotes(); + + formatEmbeddedContent(); + + // Start or stop embedded content depending on global config + if( config.autoPlayMedia === false ) { + stopEmbeddedContent( currentSlide, { unloadIframes: false } ); + } + else { + startEmbeddedContent( currentSlide ); + } + + if( isOverview() ) { + layoutOverview(); + } + + } + + /** + * Updates reveal.js to keep in sync with new slide attributes. For + * example, if you add a new `data-background-image` you can call + * this to have reveal.js render the new background image. + * + * Similar to #sync() but more efficient when you only need to + * refresh a specific slide. + * + * @param {HTMLElement} slide + */ + function syncSlide( slide ) { + + // Default to the current slide + slide = slide || currentSlide; + + syncBackground( slide ); + syncFragments( slide ); + + loadSlide( slide ); + + updateBackground(); + updateNotes(); + + } + + /** + * 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 + */ + function syncFragments( slide ) { + + // Default to the current slide + slide = slide || currentSlide; + + return sortFragments( slide.querySelectorAll( '.fragment' ) ); + + } + + /** + * Resets all vertical slides so that only the first + * is visible. + */ + function resetVerticalSlides() { + + var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + horizontalSlides.forEach( function( horizontalSlide ) { + + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + verticalSlides.forEach( function( verticalSlide, y ) { + + if( y > 0 ) { + verticalSlide.classList.remove( 'present' ); + verticalSlide.classList.remove( 'past' ); + verticalSlide.classList.add( 'future' ); + verticalSlide.setAttribute( 'aria-hidden', 'true' ); + } + + } ); + + } ); + + } + + /** + * Sorts and formats all of fragments in the + * presentation. + */ + function sortAllFragments() { + + var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + horizontalSlides.forEach( function( horizontalSlide ) { + + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + verticalSlides.forEach( function( verticalSlide, y ) { + + sortFragments( verticalSlide.querySelectorAll( '.fragment' ) ); + + } ); + + if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) ); + + } ); + + } + + /** + * Randomly shuffles all slides in the deck. + */ + function shuffle() { + + var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + slides.forEach( function( slide ) { + + // Insert this slide next to another random slide. This may + // cause the slide to insert before itself but that's fine. + dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] ); + + } ); + + } + + /** + * Updates one dimension of slides by showing the slide + * with the specified index. + * + * @param {string} selector A CSS selector that will fetch + * the group of slides we are working with + * @param {number} index The index of the slide that should be + * shown + * + * @return {number} The index of the slide that is now shown, + * might differ from the passed in index if it was out of + * bounds. + */ + function updateSlides( selector, index ) { + + // Select all slides and convert the NodeList result to + // an array + var slides = toArray( dom.wrapper.querySelectorAll( selector ) ), + slidesLength = slides.length; + + var printMode = isPrintingPDF(); + + if( slidesLength ) { + + // Should the index loop? + if( config.loop ) { + index %= slidesLength; + + if( index < 0 ) { + index = slidesLength + index; + } + } + + // Enforce max and minimum index bounds + index = Math.max( Math.min( index, slidesLength - 1 ), 0 ); + + for( var i = 0; i < slidesLength; i++ ) { + var element = slides[i]; + + var reverse = config.rtl && !isVerticalSlide( element ); + + element.classList.remove( 'past' ); + element.classList.remove( 'present' ); + element.classList.remove( 'future' ); + + // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute + element.setAttribute( 'hidden', '' ); + element.setAttribute( 'aria-hidden', 'true' ); + + // If this element contains vertical slides + if( element.querySelector( 'section' ) ) { + element.classList.add( 'stack' ); + } + + // If we're printing static slides, all slides are "present" + if( printMode ) { + element.classList.add( 'present' ); + continue; + } + + if( i < index ) { + // Any element previous to index is given the 'past' class + element.classList.add( reverse ? 'future' : 'past' ); + + if( config.fragments ) { + // Show all fragments in prior slides + toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) { + fragment.classList.add( 'visible' ); + fragment.classList.remove( 'current-fragment' ); + } ); + } + } + else if( i > index ) { + // Any element subsequent to index is given the 'future' class + element.classList.add( reverse ? 'past' : 'future' ); + + if( config.fragments ) { + // Hide all fragments in future slides + toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) { + fragment.classList.remove( 'visible' ); + fragment.classList.remove( 'current-fragment' ); + } ); + } + } + } + + // Mark the current slide as present + slides[index].classList.add( 'present' ); + slides[index].removeAttribute( 'hidden' ); + slides[index].removeAttribute( 'aria-hidden' ); + + // If this slide has a state associated with it, add it + // onto the current state of the deck + var slideState = slides[index].getAttribute( 'data-state' ); + if( slideState ) { + state = state.concat( slideState.split( ' ' ) ); + } + + } + else { + // Since there are no slides we can't be anywhere beyond the + // zeroth index + index = 0; + } + + return index; + + } + + /** + * Optimization method; hide all slides that are far away + * from the present slide. + */ + function updateSlidesVisibility() { + + // Select all slides and convert the NodeList result to + // an array + var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ), + horizontalSlidesLength = horizontalSlides.length, + distanceX, + distanceY; + + if( horizontalSlidesLength && typeof indexh !== 'undefined' ) { + + // The number of steps away from the present slide that will + // be visible + var viewDistance = isOverview() ? 10 : config.viewDistance; + + // Shorten the view distance on devices that typically have + // less resources + if( isMobileDevice ) { + viewDistance = isOverview() ? 6 : config.mobileViewDistance; + } + + // All slides need to be visible when exporting to PDF + if( isPrintingPDF() ) { + viewDistance = Number.MAX_VALUE; + } + + for( var x = 0; x < horizontalSlidesLength; x++ ) { + var horizontalSlide = horizontalSlides[x]; + + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ), + verticalSlidesLength = verticalSlides.length; + + // Determine how far away this slide is from the present + distanceX = Math.abs( ( indexh || 0 ) - x ) || 0; + + // If the presentation is looped, distance should measure + // 1 between the first and last slides + if( config.loop ) { + distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0; + } + + // Show the horizontal slide if it's within the view distance + if( distanceX < viewDistance ) { + loadSlide( horizontalSlide ); + } + else { + unloadSlide( horizontalSlide ); + } + + if( verticalSlidesLength ) { + + var oy = getPreviousVerticalIndex( horizontalSlide ); + + for( var y = 0; y < verticalSlidesLength; y++ ) { + var verticalSlide = verticalSlides[y]; + + distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy ); + + if( distanceX + distanceY < viewDistance ) { + loadSlide( verticalSlide ); + } + else { + unloadSlide( verticalSlide ); + } + } + + } + } + + // Flag if there are ANY vertical slides, anywhere in the deck + if( hasVerticalSlides() ) { + dom.wrapper.classList.add( 'has-vertical-slides' ); + } + else { + dom.wrapper.classList.remove( 'has-vertical-slides' ); + } + + // Flag if there are ANY horizontal slides, anywhere in the deck + if( hasHorizontalSlides() ) { + dom.wrapper.classList.add( 'has-horizontal-slides' ); + } + else { + dom.wrapper.classList.remove( 'has-horizontal-slides' ); + } + + } + + } + + /** + * Pick up notes from the current slide and display them + * to the viewer. + * + * @see {@link config.showNotes} + */ + function updateNotes() { + + if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) { + + dom.speakerNotes.innerHTML = getSlideNotes() || 'No notes on this slide.'; + + } + + } + + /** + * 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. + */ + function updateNotesVisibility() { + + if( config.showNotes && hasNotes() ) { + dom.wrapper.classList.add( 'show-notes' ); + } + else { + dom.wrapper.classList.remove( 'show-notes' ); + } + + } + + /** + * Checks if there are speaker notes for ANY slide in the + * presentation. + */ + function hasNotes() { + + return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0; + + } + + /** + * Updates the progress bar to reflect the current slide. + */ + function updateProgress() { + + // Update progress if enabled + if( config.progress && dom.progressbar ) { + + dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px'; + + } + + } + + + /** + * Updates the slide number to match the current slide. + */ + function updateSlideNumber() { + + // Update slide number if enabled + if( config.slideNumber && dom.slideNumber ) { + dom.slideNumber.innerHTML = getSlideNumber(); + } + + } + + /** + * Returns the HTML string corresponding to the current slide number, + * including formatting. + */ + function getSlideNumber( slide ) { + + var value; + var format = 'h.v'; + if( slide === undefined ) { + slide = currentSlide; + } + + 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 ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) { + format = 'c'; + } + + value = []; + switch( format ) { + case 'c': + value.push( getSlidePastCount( slide ) + 1 ); + break; + case 'c/t': + value.push( getSlidePastCount( slide ) + 1, '/', getTotalSlides() ); + break; + default: + var indices = getIndices( slide ); + value.push( indices.h + 1 ); + var sep = format === 'h/v' ? '/' : '.'; + if( isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 ); + } + } + + var url = '#' + locationHash( slide ); + return formatSlideNumber( 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 + */ + function formatSlideNumber( a, delimiter, b, url ) { + + if( url === undefined ) { + url = '#' + locationHash(); + } + if( typeof b === 'number' && !isNaN( b ) ) { + return '' + + ''+ a +'' + + ''+ delimiter +'' + + ''+ b +'' + + ''; + } + else { + return '' + + ''+ a +'' + + ''; + } + + } + + /** + * Updates the state of all control/navigation arrows. + */ + function updateControls() { + + var routes = availableRoutes(); + var fragments = availableFragments(); + + // Remove the 'enabled' class from all directions + dom.controlsLeft.concat( dom.controlsRight ) + .concat( dom.controlsUp ) + .concat( dom.controlsDown ) + .concat( dom.controlsPrev ) + .concat( dom.controlsNext ).forEach( function( node ) { + node.classList.remove( 'enabled' ); + node.classList.remove( '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 ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + + // Prev/next buttons + if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + + // Highlight fragment directions + if( currentSlide ) { + + // Always apply fragment decorator to prev/next buttons + if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + + // Apply fragment decorators to directional buttons based on + // what slide axis they are in + if( isVerticalSlide( currentSlide ) ) { + if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + } + else { + if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + } + + } + + if( config.controlsTutorial ) { + + // Highlight control arrows with an animation to ensure + // that the viewer knows how to navigate + if( !hasNavigatedDown && routes.down ) { + dom.controlsDownArrow.classList.add( 'highlight' ); + } + else { + dom.controlsDownArrow.classList.remove( 'highlight' ); + + if( !hasNavigatedRight && routes.right && indexv === 0 ) { + dom.controlsRightArrow.classList.add( 'highlight' ); + } + else { + dom.controlsRightArrow.classList.remove( 'highlight' ); + } + } + + } + + } + + /** + * 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. + */ + function updateBackground( includeAll ) { + + var currentBackground = null; + + // Reverse past/future classes when in RTL mode + var horizontalPast = config.rtl ? 'future' : 'past', + horizontalFuture = config.rtl ? 'past' : 'future'; + + // Update the classes of all backgrounds to match the + // states of their slides (past/present/future) + toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { + + backgroundh.classList.remove( 'past' ); + backgroundh.classList.remove( 'present' ); + backgroundh.classList.remove( 'future' ); + + if( h < indexh ) { + backgroundh.classList.add( horizontalPast ); + } + else if ( h > indexh ) { + backgroundh.classList.add( horizontalFuture ); + } + else { + backgroundh.classList.add( 'present' ); + + // Store a reference to the current background element + currentBackground = backgroundh; + } + + if( includeAll || h === indexh ) { + toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) { + + backgroundv.classList.remove( 'past' ); + backgroundv.classList.remove( 'present' ); + backgroundv.classList.remove( 'future' ); + + if( v < indexv ) { + backgroundv.classList.add( 'past' ); + } + else if ( v > indexv ) { + backgroundv.classList.add( 'future' ); + } + else { + backgroundv.classList.add( 'present' ); + + // Only if this is the present horizontal and vertical slide + if( h === indexh ) currentBackground = backgroundv; + } + + } ); + } + + } ); + + // Stop content inside of previous backgrounds + if( previousBackground ) { + + stopEmbeddedContent( previousBackground, { unloadIframes: !shouldPreload( previousBackground ) } ); + + } + + // Start content in the current background + if( currentBackground ) { + + startEmbeddedContent( currentBackground ); + + var currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' ); + if( currentBackgroundContent ) { + + var 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. + var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null; + var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); + if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) { + dom.background.classList.add( 'no-transition' ); + } + + 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( function( classToBubble ) { + if( currentSlide.classList.contains( classToBubble ) ) { + dom.wrapper.classList.add( classToBubble ); + } + else { + dom.wrapper.classList.remove( classToBubble ); + } + } ); + } + + // Allow the first background to apply without transition + setTimeout( function() { + dom.background.classList.remove( 'no-transition' ); + }, 1 ); + + } + + /** + * Updates the position of the parallax background based + * on the current slide index. + */ + function updateParallax() { + + if( config.parallaxBackgroundImage ) { + + var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), + verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); + + var backgroundSize = dom.background.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 ); + } + + var slideWidth = dom.background.offsetWidth, + horizontalSlideCount = horizontalSlides.length, + horizontalOffsetMultiplier, + horizontalOffset; + + if( typeof config.parallaxBackgroundHorizontal === 'number' ) { + horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal; + } + else { + horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0; + } + + horizontalOffset = horizontalOffsetMultiplier * indexh * -1; + + var slideHeight = dom.background.offsetHeight, + verticalSlideCount = verticalSlides.length, + verticalOffsetMultiplier, + verticalOffset; + + if( typeof config.parallaxBackgroundVertical === 'number' ) { + verticalOffsetMultiplier = config.parallaxBackgroundVertical; + } + else { + verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); + } + + verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0; + + dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; + + } + + } + + /** + * Should the given element be preloaded? + * Decides based on local element attributes and global config. + * + * @param {HTMLElement} element + */ + function shouldPreload( element ) { + + // Prefer an explicit global preload setting + var preload = config.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 + */ + function loadSlide( slide, options ) { + + options = options || {}; + + // Show the slide element + slide.style.display = config.display; + + // Media elements with data-src attributes + toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) { + if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) { + element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); + element.setAttribute( 'data-lazy-loaded', '' ); + element.removeAttribute( 'data-src' ); + } + } ); + + // Media elements with children + toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) { + var sources = 0; + + toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) { + source.setAttribute( 'src', source.getAttribute( 'data-src' ) ); + source.removeAttribute( 'data-src' ); + source.setAttribute( 'data-lazy-loaded', '' ); + sources += 1; + } ); + + // 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 + var background = slide.slideBackgroundElement; + if( background ) { + background.style.display = 'block'; + + var backgroundContent = slide.slideBackgroundContentElement; + var backgroundIframe = slide.getAttribute( 'data-background-iframe' ); + + // If the background contains media, load it + if( background.hasAttribute( 'data-loaded' ) === false ) { + background.setAttribute( 'data-loaded', 'true' ); + + var 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 && !isSpeakerNotes() ) { + var video = document.createElement( 'video' ); + + if( backgroundVideoLoop ) { + video.setAttribute( 'loop', '' ); + } + + if( backgroundVideoMuted ) { + video.muted = true; + } + + // Inline video playback works (at least in Mobile Safari) as + // long as the video is muted and the `playsinline` attribute is + // present + if( isMobileDevice ) { + video.muted = true; + video.autoplay = true; + video.setAttribute( 'playsinline', '' ); + } + + // Support comma separated lists of video sources + backgroundVideo.split( ',' ).forEach( function( source ) { + video.innerHTML += ''; + } ); + + backgroundContent.appendChild( video ); + } + // Iframes + else if( backgroundIframe && options.excludeIframes !== true ) { + var 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 + var backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' ); + if( backgroundIframeElement ) { + + // Check if this iframe is eligible to be preloaded + if( shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) { + if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) { + backgroundIframeElement.setAttribute( 'src', backgroundIframe ); + } + } + + } + + } + + } + + /** + * Unloads and hides the given slide. This is called when the + * slide is moved outside of the configured view distance. + * + * @param {HTMLElement} slide + */ + function unloadSlide( slide ) { + + // Hide the slide element + slide.style.display = 'none'; + + // Hide the corresponding background element + var background = getSlideBackground( slide ); + if( background ) { + background.style.display = 'none'; + + // Unload any background iframes + toArray( background.querySelectorAll( 'iframe[src]' ) ).forEach( function( element ) { + element.removeAttribute( 'src' ); + } ); + } + + // Reset lazy-loaded media elements with src attributes + toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) { + element.setAttribute( 'data-src', element.getAttribute( 'src' ) ); + element.removeAttribute( 'src' ); + } ); + + // Reset lazy-loaded media elements with children + toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) { + source.setAttribute( 'data-src', source.getAttribute( 'src' ) ); + source.removeAttribute( 'src' ); + } ); + + } + + /** + * Determine what available routes there are for navigation. + * + * @return {{left: boolean, right: boolean, up: boolean, down: boolean}} + */ + function availableRoutes() { + + var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), + verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); + + var routes = { + left: indexh > 0, + right: indexh < horizontalSlides.length - 1, + up: indexv > 0, + down: indexv < verticalSlides.length - 1 + }; + + // Looped presentations can always be navigated as long as + // there are slides available + if( config.loop ) { + if( horizontalSlides.length > 1 ) { + routes.left = true; + routes.right = true; + } + + if( verticalSlides.length > 1 ) { + routes.up = true; + routes.down = true; + } + } + + // Reverse horizontal controls for rtl + if( config.rtl ) { + var left = routes.left; + routes.left = routes.right; + routes.right = left; + } + + return routes; + + } + + /** + * Returns an object describing the available fragment + * directions. + * + * @return {{prev: boolean, next: boolean}} + */ + function availableFragments() { + + if( currentSlide && config.fragments ) { + var fragments = currentSlide.querySelectorAll( '.fragment' ); + var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' ); + + return { + prev: fragments.length - hiddenFragments.length > 0, + next: !!hiddenFragments.length + }; + } + else { + return { prev: false, next: false }; + } + + } + + /** + * Enforces origin-specific format rules for embedded media. + */ + function formatEmbeddedContent() { + + var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) { + toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) { + var 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 + */ + function startEmbeddedContent( element ) { + + if( element && !isSpeakerNotes() ) { + + // Restart GIFs + toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( 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 + toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { + if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) { + return; + } + + // Prefer an explicit global autoplay setting + var autoplay = config.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' ) || !!closestParent( el, '.slide-background' ); + } + + if( autoplay && typeof el.play === 'function' ) { + + // If the media is ready, start playback + if( el.readyState > 1 ) { + startEmbeddedMedia( { target: el } ); + } + // Mobile devices never fire a loaded event so instead + // of waiting, we initiate playback + else if( isMobileDevice ) { + var 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( function() { + el.controls = true; + + // Once the video does start playing, hide the controls again + el.addEventListener( 'play', function() { + el.controls = false; + } ); + } ); + } + } + // If the media isn't loaded, wait before playing + else { + el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes + el.addEventListener( 'loadeddata', startEmbeddedMedia ); + } + + } + } ); + + // Normal iframes + toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) { + if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) { + return; + } + + startEmbeddedIframe( { target: el } ); + } ); + + // Lazy loading iframes + toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) { + if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) { + return; + } + + if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) { + el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes + el.addEventListener( 'load', startEmbeddedIframe ); + el.setAttribute( 'src', el.getAttribute( 'data-src' ) ); + } + } ); + + } + + } + + /** + * Starts playing an embedded video/audio element after + * it has finished loading. + * + * @param {object} event + */ + function startEmbeddedMedia( event ) { + + var isAttachedToDOM = !!closestParent( event.target, 'html' ), + isVisible = !!closestParent( event.target, '.present' ); + + if( isAttachedToDOM && isVisible ) { + event.target.currentTime = 0; + event.target.play(); + } + + event.target.removeEventListener( 'loadeddata', startEmbeddedMedia ); + + } + + /** + * "Starts" the content of an embedded iframe using the + * postMessage API. + * + * @param {object} event + */ + function startEmbeddedIframe( event ) { + + var iframe = event.target; + + if( iframe && iframe.contentWindow ) { + + var isAttachedToDOM = !!closestParent( event.target, 'html' ), + isVisible = !!closestParent( event.target, '.present' ); + + if( isAttachedToDOM && isVisible ) { + + // Prefer an explicit global autoplay setting + var autoplay = config.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' ) || !!closestParent( 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 + */ + function stopEmbeddedContent( element, options ) { + + options = extend( { + // Defaults + unloadIframes: true + }, options || {} ); + + if( element && element.parentNode ) { + // HTML5 media elements + toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( 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 + toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) { + if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' ); + el.removeEventListener( 'load', startEmbeddedIframe ); + }); + + // YouTube postMessage API + toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { + if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) { + el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' ); + } + }); + + // Vimeo postMessage API + toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( 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 + toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( 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' ); + } ); + } + } + + } + + /** + * Returns the number of past slides. This can be used as a global + * flattened index for slides. + * + * @param {HTMLElement} [slide=currentSlide] The slide we're counting before + * + * @return {number} Past slide count + */ + function getSlidePastCount( slide ) { + + if( slide === undefined ) { + slide = currentSlide; + } + + var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + // The number of past slides + var pastCount = 0; + + // Step through all slides and count the past ones + mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { + + var horizontalSlide = horizontalSlides[i]; + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + + for( var j = 0; j < verticalSlides.length; j++ ) { + + // Stop as soon as we arrive at the present + if( verticalSlides[j] === slide ) { + break mainLoop; + } + + pastCount++; + + } + + // Stop as soon as we arrive at the present + if( horizontalSlide === slide ) { + break; + } + + // Don't count the wrapping section for vertical slides + if( horizontalSlide.classList.contains( 'stack' ) === false ) { + pastCount++; + } + + } + + return pastCount; + + } + + /** + * Returns a value ranging from 0-1 that represents + * how far into the presentation we have navigated. + * + * @return {number} + */ + function getProgress() { + + // The number of past and total slides + var totalCount = getTotalSlides(); + var pastCount = getSlidePastCount(); + + if( currentSlide ) { + + var allFragments = currentSlide.querySelectorAll( '.fragment' ); + + // If there are fragments in the current slide those should be + // accounted for in the progress. + if( allFragments.length > 0 ) { + var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); + + // This value represents how big a portion of the slide progress + // that is made up by its fragments (0-1) + var fragmentWeight = 0.9; + + // Add fragment progress to the past slide count + pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight; + } + + } + + return Math.min( pastCount / ( totalCount - 1 ), 1 ); + + } + + /** + * Checks if this presentation is running inside of the + * speaker notes window. + * + * @return {boolean} + */ + function isSpeakerNotes() { + + return !!window.location.search.match( /receiver/gi ); + + } + + /** + * Reads the current URL (hash) and navigates accordingly. + */ + function readURL() { + + var hash = window.location.hash; + + // Attempt to parse the hash as either an index or name + var 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 ) { + var element; + + // 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 + var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false; + + if( element ) { + // If the slide exists and is not the current slide... + if ( !isSameNameAsCurrentSlide ) { + // ...find the position of the named slide and navigate to it + var indices = Reveal.getIndices(element); + slide(indices.h, indices.v); + } + } + // If the slide doesn't exist, navigate to the current slide + else { + slide( indexh || 0, indexv || 0 ); + } + } + else { + var hashIndexBase = config.hashOneBasedIndex ? 1 : 0; + + // Read the index components of the hash + var 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 !== indexh || v !== indexv || f !== undefined ) { + 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 + */ + function writeURL( delay ) { + + // Make sure there's never more than one timeout running + clearTimeout( writeURLTimeout ); + + // If a delay is specified, timeout this call + if( typeof delay === 'number' ) { + writeURLTimeout = setTimeout( writeURL, delay ); + } + else if( currentSlide ) { + // If we're configured to push to history OR the history + // API is not avaialble. + if( config.history || !window.history ) { + window.location.hash = locationHash(); + } + // If we're configured to reflect the current slide in the + // URL without pushing to history. + else if( config.hash ) { + window.history.replaceState( null, null, '#' + locationHash() ); + } + // 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 ); + } + } + + } + /** + * Retrieves the h/v location and fragment of the current, + * or specified, slide. + * + * @param {HTMLElement} [slide] If specified, the returned + * index will be for this slide rather than the currently + * active one + * + * @return {{h: number, v: number, f: number}} + */ + function getIndices( slide ) { + + // By default, return the current indices + var h = indexh, + v = indexv, + f; + + // If a slide is specified, return the indices of that slide + if( slide ) { + var isVertical = isVerticalSlide( slide ); + var slideh = isVertical ? slide.parentNode : slide; + + // Select all horizontal slides + var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + // Now that we know which the horizontal slide is, get its index + h = Math.max( horizontalSlides.indexOf( slideh ), 0 ); + + // Assume we're not vertical + v = undefined; + + // If this is a vertical slide, grab the vertical index + if( isVertical ) { + v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 ); + } + } + + if( !slide && currentSlide ) { + var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0; + if( hasFragments ) { + var currentFragment = currentSlide.querySelector( '.current-fragment' ); + if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) { + f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 ); + } + else { + f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1; + } + } + } + + return { h: h, v: v, f: f }; + + } + + /** + * Retrieves all slides in this presentation. + */ + function getSlides() { + + return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ) ); + + } + + /** + * Returns a list of all horizontal slides in the deck. Each + * vertical stack is included as one horizontal slide in the + * resulting array. + */ + function getHorizontalSlides() { + + return toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + } + + /** + * Returns all vertical slides that exist within this deck. + */ + function getVerticalSlides() { + + return toArray( dom.wrapper.querySelectorAll( '.slides>section>section' ) ); + + } + + /** + * Returns true if there are at least two horizontal slides. + */ + function hasHorizontalSlides() { + + return getHorizontalSlides().length > 1; + } + + /** + * Returns true if there are at least two vertical slides. + */ + function hasVerticalSlides() { + + return getVerticalSlides().length > 1; + + } + + /** + * Returns an array of objects where each object represents the + * attributes on its respective slide. + */ + function getSlidesAttributes() { + + return getSlides().map( function( slide ) { + + var attributes = {}; + for( var i = 0; i < slide.attributes.length; i++ ) { + var attribute = slide.attributes[ i ]; + attributes[ attribute.name ] = attribute.value; + } + return attributes; + + } ); + + } + + /** + * Retrieves the total number of slides in this presentation. + * + * @return {number} + */ + function getTotalSlides() { + + return getSlides().length; + + } + + /** + * Returns the slide element matching the specified index. + * + * @return {HTMLElement} + */ + function getSlide( x, y ) { + + var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; + var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' ); + + if( verticalSlides && verticalSlides.length && typeof y === 'number' ) { + return verticalSlides ? verticalSlides[ y ] : undefined; + } + + return horizontalSlide; + + } + + /** + * Returns the background element for the given slide. + * All slides, even the ones with no background properties + * defined, have a background element so as long as the + * index is valid an element will be returned. + * + * @param {mixed} x Horizontal background index OR a slide + * HTML element + * @param {number} y Vertical background index + * @return {(HTMLElement[]|*)} + */ + function getSlideBackground( x, y ) { + + var slide = typeof x === 'number' ? getSlide( x, y ) : x; + if( slide ) { + return slide.slideBackgroundElement; + } + + return undefined; + + } + + /** + * Retrieves the speaker notes from a slide. Notes can be + * defined in two ways: + * 1. As a data-notes attribute on the slide
+ * 2. As an