diff --git a/src/controllers/StaticController.php b/src/controllers/StaticController.php index 79446e6f1..70cf6fedf 100644 --- a/src/controllers/StaticController.php +++ b/src/controllers/StaticController.php @@ -269,8 +269,8 @@ EOD; $this->component("social")->sortWikiResources($data); } else if ($head_info['page_type'] == 'presentation') { $data['page_type'] = 'presentation'; - $data['INCLUDE_SCRIPTS'][] = "slidy"; - $data['INCLUDE_STYLES'][] = "slidy"; + $data['INCLUDE_SCRIPTS'][] = "frise"; + $data['INCLUDE_STYLES'][] = "frise"; } } if ((!empty($data['PAGE']) && diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php index 2a50a7c87..b05f00e84 100644 --- a/src/controllers/components/SocialComponent.php +++ b/src/controllers/components/SocialComponent.php @@ -4238,8 +4238,8 @@ EOD; } else if ($data["HEAD"]['page_type'] == 'presentation' && $data['CONTROLLER'] == 'group') { $data['page_type'] = 'presentation'; - $data['INCLUDE_SCRIPTS'][] = "slidy"; - $data['INCLUDE_STYLES'][] = "slidy"; + $data['INCLUDE_SCRIPTS'][] = "frise"; + $data['INCLUDE_STYLES'][] = "frise"; } } /** diff --git a/src/css/frise.css b/src/css/frise.css new file mode 100644 index 000000000..4333931f8 --- /dev/null +++ b/src/css/frise.css @@ -0,0 +1,999 @@ +/** + * FRISE (FRee Interactive Story Engine) + * A light-weight engine for writing interactive fiction and games. + * + * Copyright 2022-2024 Christopher Pollett chris@pollett.org + * + * @license + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * @file The CSS code used to style the elements used by a game + * @author Chris Pollett + * @link https://www.frise.org/ + * @copyright 2022 - 2024 + */ +/* make sure game tags we don't want to display are not displayed + */ +x-action, +x-game, +x-dollhouse, +x-slides, +x-object, +x-location +{ + display: none; +} +/** + * Prevents border around canvas's that respond to keyboard events such as + * those made by dollhouses + */ +canvas:focus +{ + outline:none; +} +/* + * General useful classes + */ +.none +{ + display: none; +} +.hidden +{ + visibility: hidden; +} +.block +{ + display: block; +} +.inline-block +{ + display: inline-block; +} +.inline +{ + display: inline; +} +.float-right +{ + float: right; +} +.float-left +{ + float: left; +} +.float-opposite +{ + float: right; +} +.rtl .float-opposite +{ + float: left; +} +.float-same +{ + float: left; +} +.rtl .float-same +{ + float: right; +} +.left +{ + text-align: left; +} +.right +{ + text-align: right; +} +.opposite +{ + text-align: left; +} +.rtl .opposite +{ + text-align: right; +} +.center +{ + text-align: center; +} +.fit-content +{ + width: fit-content; +} +.rounded +{ + border-radius: 20px; +} +.rounded-top +{ + border-radius: 20px 20px 0 0; +} +.small-width +{ + width: 100px; +} +.mobile .small-width +{ + width: 75px; +} +.medium-width +{ + width: 150px; +} +.mobile .medium-width +{ + width: 100px; +} +.large-width +{ + width: 200px; +} +.mobile .large-width +{ + width: 150px; +} +.thin-border +{ + border: solid 1px black; +} +.medium-border +{ + border: solid 2px black; +} +.thick-border +{ + border: solid 4px black; +} +/** + * Special Effects, Animation tags and CSS classes + */ + .repeating + { + animation-iteration-count: infinite; + } +/* classes for animation count*/ +.once +{ + animation-iteration-count: 1; +} +.twice +{ + animation-iteration-count: 2; +} +.thrice +{ + animation-iteration-count: 3; +} +.four-times +{ + animation-iteration-count: 4; +} +.five-times +{ + animation-iteration-count: 5; +} +.ten-times +{ + animation-iteration-count: 10; +} +.infinite +{ + animation-iteration-count: infinite; +} +/* classes for animation duration */ +.half-sec +{ + animation-duration: 0.5s; +} +.second +{ + animation-duration: 1s; +} +.two-second +{ + animation-duration: 2s; +} +.three-second +{ + animation-duration: 3s; +} +.five-second +{ + animation-duration: 5s; +} +.ten-second +{ + animation-duration: 10s; +} +.top-left-origin +{ + transform-origin: 0 0; +} +.top-right-origin +{ + transform-origin: 100% 0; +} +.top-mid-origin +{ + transform-origin: 50% 0; +} +.mid-left-origin +{ + transform-origin: 0 50%; +} +.mid-mid-origin, +.center-origin +{ + transform-origin: 50% 50%; +} +.mid-right-origin +{ + transform-origin: 100% 50%; +} +.bottom-right-origin +{ + transform-origin: 100% 100%; +} +.bottom-left-origin +{ + transform-origin: 0 100%; +} +.bottom-mid-origin +{ + transform-origin: 50% 100%; +} +x-outline, +.outline +{ + color: white; + text-shadow: + -1px -1px 0 black, + 1px -1px 0 black, + -1px 1px 0 black, + 1px 1px 0 black; +} +x-mild-blur, +.mild-blur +{ + filter: blur(1px); +} +x-blink, +.blink +{ + animation: blinker 1.2s ease infinite; +} +@keyframes blinker { + 50% { + opacity: 0; + } +} +x-blur, +x-unblurable, +.blur +{ + filter: blur(3px); +} +x-unblurable:active, +x-unblurable:hover, +.hover-unblur:active, +.hover-unblur:hover +{ + filter: blur(0); + transition: filter 1s ease-in; +} +x-heavy-blur, +.heavy-blur +{ + filter: blur(5px); +} + +x-blur, +x-heavy-blur, +x-mild-blur, +x-outline, +x-unblurable +{ + display: inline; +} + +x-condense, +.condense +{ + display: inline-block; + transform: scaleX(0.5); + transform-origin: 0; +} +x-expand, +.expand +{ + display: inline-block; + transform: scaleX(2); + transform-origin: 0; +} +x-emboss, +.emboss +{ + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3); + display: inline-block; + text-shadow: -2px -2px 1px rgba(255, 255, 255, 0.6), + 3px 3px 3px rgba(0, 0, 0, 0.4); +} +.rewind +{ + animation-direction: reverse !important; +} +@keyframes fade-in +{ + 0% {opacity: 0} + to {opacity: 1} +} +x-fade-in, +.fade-in +{ + animation: fade-in .5s ease-in-out forwards; + opacity: 1; +} +@keyframes fade-out +{ + 0% {opacity: 1} + to {opacity: 0} +} +x-fade-out, +.fade-out +{ + animation: fade-out .5s ease-in-out forwards; + opacity: 0; +} +@keyframes fade-in-out +{ + 0%, + to {opacity: 0} + 50% {opacity: 1} +} +x-fade-in-out, +.fade-in-out +{ + animation: fade-in-out 1s ease-in-out infinite alternate; +} +@keyframes scale-in +{ + 0% {transform: scale(1,1);} + to {transform: scale(0,0);} +} +x-scale-in, +.scale-in +{ + animation: scale-in .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes scale-out +{ + 0% {transform: scale(0,0);} + to {transform: scale(1,1);} +} +x-scale-out, +.scale-out +{ + animation: scale-out .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes slide-in-top +{ + 0% {transform: translate(0, -120%);} + to {transform: translate(0,0);} +} +x-slide-in-top, +.slide-in-top +{ + animation: slide-in-top .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes slide-out-top +{ + 0% {transform: translate(0, 0);} + to {transform: translate(0, -120%);} +} +x-slide-out-top, +.slide-out-top +{ + animation: slide-out-top .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes slide-in-bottom +{ + 0% {transform: translate(0, 120%);} + to {transform: translate(0,0);} +} +x-slide-in-bottom, +.slide-in-bottom +{ + animation: slide-in-bottom .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes slide-out-bottom +{ + 0% {transform: translate(0, 0);} + to {transform: translate(0, 120%);} +} +x-slide-out-bottom, +.slide-out-bottom +{ + animation: slide-out-bottom .5s ease-in-out forwards; + display:block; + height: 100%; +} +@keyframes slide-in-left +{ + 0% {transform: translate(-120%, 0);} + to {transform: translate(0,0);} +} +x-slide-in-left, +.slide-in-left +{ + animation: slide-in-left .5s ease-in-out forwards;; + display:block; + width: 100%; +} +@keyframes slide-out-left +{ + 0% {transform: translate(0, 0);} + to {transform: translate(-120%, 0);} +} +x-slide-out-left, +.slide-out-left +{ + animation: slide-out-left .5s ease-in-out forwards;; + display:block; + width: 100%; +} +@keyframes slide-in-right +{ + 0% {transform: translate(120%, 0);} + to {transform: translate(0,0);} +} +x-slide-in-right, +.slide-in-right +{ + animation: slide-in-right .5s ease-in-out; + display:block; + width: 100%; +} +@keyframes slide-out-right +{ + 0% {transform: translate(0, 0);} + to {transform: translate(120%, 0);} +} +x-slide-out-right, +.slide-out-right +{ + animation: slide-out-right .5s ease-in-out forwards; + display:block; + width: 100%; +} +x-vertical-mirror, +.vertical-mirror +{ + display: inline-block; + transform: scaleY(-1); +} +x-mirror, +.mirror +{ + display: inline-block; + transform: scaleX(-1); +} +@keyframes pulse +{ + 0% { transform: scale(0.7, 0.7);} + 20% { transform: scale(1.25, 1.25); } + 40% { transform: scale(0.8125, 0.8125); } + 60% { transform: scale(1.125, 1.125); } + 80% { transform: scale(0.9, 0.9); } + to { transform: scale(1, 1); } +} +x-pulse, +.pulse +{ + animation: pulse 0.8s; + display: inline-block; +} +@keyframes rumble +{ + 50% { + transform: translateY(-4px) + } +} +x-rumble, +.rumble +{ + animation: rumble linear 0.15s 0s infinite; + display:inline-block; +} +@keyframes shudder +{ + 50% { transform: translateX(4px) } +} +x-shudder, +.shudder +{ + animation: shudder linear 0.15s 0s infinite; + display: inline-block; +} +x-upside-down, +.upside-down +{ + display: inline-block; + transform: rotate(180deg); +} +/* + * Styles specific to the game content and nav areas + */ +#game-content +{ + font-family: "Oswald", serif; + font-size: 18pt; + height: 100%; + left: 300px; + line-height: 1.6; + overflow-x: scroll; + overflow-y: scroll; + position: fixed; + transition: left .25s ease-in; + top: 0px; + padding-top: 23px; + width: calc(100% - 320px); +} +.rtl +{ + direction: rtl; +} +.rtl #game-content +{ + left: unset; + right: 300px; + transition: right .25s ease-in; +} +.no-nav #game-content +{ + left: 20px; + right: 20px; + transition: unset; + width: calc(100% - 40px); +} +#main-nav, +#main-bar +{ + background: linear-gradient(to right, white, 97%, lightgray); + height: 100%; + left: 0; + overflow-y: scroll; + position: fixed; + text-align: center; + transition: left .25s ease-in; + top: 0px; + width: 240px; + z-index: 0; +} +.rtl #main-nav, +.rtl #main-bar +{ + background: linear-gradient(to left, white, 97%, lightgray); + left: unset; + right: 0; + transition: right .25s ease-in; +} +#main-bar +{ + background: white; + height: 50px; + text-align: left; + top: 0; + width: 50px; + z-index: 2; +} +.rtl #main-bar +{ + background: white; +} +.mobile #main-bar +{ + background: white; + height: 50px; + top: 0; + width: 50px; + z-index: 2; +} +.rtl #main-bar +{ + text-align: right; +} +#main-nav button, +#main-bar button +{ + border-radius: 10px; + font-size: 21pt; + padding: 2px 5px 5px 5px; + width: 40px; +} +.mobile #main-nav button, +.mobile #main-bar button +{ + font-size: 18pt; + height: 50px; + margin: 2px; + padding: 8px 10px 10px 10px; + width: 40px; +} +#main-nav h1 +{ + margin: 8px; +} +#main-nav x-button, +#main-nav input[type="range"] +{ + width: 170px; +} +#main-nav .nav-label +{ + font-size: 14pt; + margin: auto; + text-align: left; + width: 170px; +} +.rtl #main-nav .nav-label +{ + text-align: right; +} +#main-nav .nav-info +{ + font-size: 14pt; + margin: 0px auto 15px auto; + text-align: left; + width: 170px; +} +.rtl #main-nav .nav-info +{ + text-align: right; +} +.footer-space { + height: 1in; +} +/* + * Styles specific to when we are using frise for a slide show + */ +.slides #game-content +{ + left: 0; + right: 0; + margin: 0; + padding: 0; + width: 100%; +} +div.slide +{ + background-color: white; + color: black; + margin: 0; + padding: 20px; + font-family: "Gill Sans MT", "Gill Sans", GillSans, sans-serif; + font-size: 120%; + height: calc(95% - 50px); +} +.all div.slide +{ + height: fit-content; +} +.slide h1 +{ + line-height: 1.1; +} +.slide pre +{ + background-color: #EEE; + border-style: solid; + border-width: thin; + border-left-width: 1em; + border-color: #9AD; + color: #048; + font-size: 80%; + font-weight: bold; + padding: 0.2em 1em 0.2em 1em; + +} +.slide a img +{ + border-width: 0; + border-style: none; +} +.slide a:link, +.slide a:visited +{ + color: navy; +} +.slide a:active, +.slide a:hover +{ + color: red; + text-decoration: underline; +} +.slide a +{ + text-decoration: none; +} +ol, +ul +{ + margin: 0.5em 1.5em 0.5em 1.5em; + padding: 0; +} +.slide ul +{ + list-style-type: square; +} +.slide ul ul ul +{ + list-style-type: circle; +} +.slide ul ul, +.slide ul ul ul ul +{ + list-style-type: disc; +} +.slide li +{ + margin-top: 0.5em; +} +.slide li li +{ + font-size: 85%; + font-style: italic; +} +.slide li li li +{ + font-size: 85%; + font-style: normal; +} +.slide div dt +{ + margin-left: 0; + margin-top: 1em; + margin-bottom: 0.5em; + font-weight: bold; +} +.slide div dd +{ + margin-left: 2em; + margin-bottom: 0.5em; +} +.slide p, +.slide pre, +.slide ul, +.slide ol, +.slide blockquote, +.slide h2, +.slide h3, +.slide h4, +.slide h5, +.slide h6, +.slide dl, +.slide table +{ + margin-left: 1em; + margin-right: 1em; +} +.slide-footer +{ + background-color: #EEE; + font-size: 20px; + height: 50px; + left: 0; + position: fixed; + right: 0; + width: 100%; + top: calc(100% - 50px); +} +.slide-footer x-button +{ + font-size: 14px; + padding: 5px; + margin: 2px; +} +.slide-footer .big-p +{ + font-size: 30px; + position: relative; + top: 3px; +} +@media print +{ + body, + #game-screen, + #game-content + { + display: unset; + height: fit-content; + position: unset; + } + div.slide + { + display: unset; + height: unset; + page-break-inside: avoid; + position: unset; + } + div.page-break + { + page-break-after: always; + visibility: hidden; + } + .slide-footer + { + display: none; + } +} + + +/* + * Styles related to a Save/Load Game Location + */ +.filled +{ + background-color: blue; + color: white; +} +table.save-table +{ + width: 7in; +} +.mobile table.save-table +{ + width: 320px; +} +table.save-table, +table.save-table tr, +table.save-table th, +table.save-table td +{ + border-collapse: collapse; + border: 1px solid black; + padding:10px; + text-align: center; +} +.mobile table.save-table, +.mobile table.save-table tr, +.mobile table.save-table th, +.mobile table.save-table td +{ + padding:3px; +} +table.save-table td.save-name +{ + width: 3in; +} +.mobile table.save-table td.save-name +{ + width: 100px; +} +.mobile #save-disk, +.mobile #load-disk +{ + visibility: hidden; +} +/* + * Styles specific to how we want HTML to appear in the game. + * Also has styles for the new x- tags that we use for presentation + */ +h1 +{ + margin: 0px; + padding: 0px; +} +img +{ + max-width: 90%; +} +figure +{ + max-width: 90%; +} +figure > img +{ + max-width: 100%; +} +input +{ + border: 2px solid lightblue; + border-radius: 5px; + font-size: 18pt; + padding: 2px; +} +a.disabled +{ + color: gray; + cursor: default; + pointer-events: none; +} +x-button +{ + background-color: #F0F0F6; + border: 1px solid gray; + border-radius: 5px; + color: black; + display: inline-block; + font-size: 18pt; + font-weight: bold; + padding: 8px; + margin: 3px; +} +x-button.disabled +{ + border: 1px solid lightgray; + background-color: #F6F6FA; + color: #666; + cursor: not-allowed; +} +x-button:hover +{ + background-color: lightgray; +} + +x-button.disabled:hover +{ + color: #666; + background-color: #F6F6FA; +} +x-speaker +{ + border: 3px solid black; + border-radius: 10px; + display: block; + font-size:18pt; + margin: 10px; + min-height: 110px; + padding: 10px; + width: 90%; +} +.mobile x-speaker +{ + margin: 2px; + padding: 4px; + width: 82%; +} +x-speaker figure:first-of-type +{ + border-radius: 5px; + display: block; + float: left; + margin: 0; + width: 120px; +} +.rtl x-speaker figure:first-of-type +{ + float: right; +} +.mobile x-speaker figure:first-of-type +{ + width: 80px; +} +x-speaker figure:first-of-type > img +{ + border-radius: 10px; + display: block; + height: 100px; + margin: auto; + width: 100px; +} +.mobile x-speaker figure:first-of-type > img +{ + width: 70px; + height: 70px; +} diff --git a/src/css/slidy.css b/src/css/slidy.css deleted file mode 100755 index b29aa6483..000000000 --- a/src/css/slidy.css +++ /dev/null @@ -1,402 +0,0 @@ -/* slidy.css - - Copyright (c) 2005-2010 W3C (MIT, ERCIM, Keio), All Rights Reserved. - W3C liability, trademark, document use and software licensing - rules apply, see: - - http://www.w3.org/Consortium/Legal/copyright-documents - http://www.w3.org/Consortium/Legal/copyright-software -*/ -body -{ - margin: 0 0 0 0; - padding: 0 0 0 0; - width: 100%; - height: 100%; - color: black; - background-color: white; - font-family: "Gill Sans MT", "Gill Sans", GillSans, sans-serif; - font-size: 14pt; -} - -div.toolbar { - position: fixed; z-index: 200; - top: auto; bottom: 0; left: 0; right: 0; - height: 1.2em; text-align: right; - padding-left: 1em; - padding-right: 1em; - font-size: 60%; - color: red; - background-color: rgb(240,240,240); - border-top: solid 1px rgb(180,180,180); -} - -div.toolbar span.copyright { - color: black; - margin-left: 0.5em; -} - -div.initial_prompt { - position: absolute; - z-index: 1000; - bottom: 1.2em; - width: 100%; - background-color: rgb(200,200,200); - opacity: 0.35; - background-color: rgba(200,200,200, 0.35); - cursor: pointer; -} - -div.initial_prompt p.help { - text-align: center; -} - -div.initial_prompt p.close { - text-align: right; - font-style: italic; -} - -div.slidy_toc { - position: absolute; - z-index: 300; - width: 60%; - max-width: 30em; - height: 30em; - overflow: auto; - top: auto; - right: auto; - left: 4em; - bottom: 4em; - padding: 1em; - background: rgb(240,240,240); - border-style: solid; - border-width: 2px; - font-size: 60%; -} - -div.slidy_toc .toc_heading { - text-align: center; - width: 100%; - margin: 0; - margin-bottom: 1em; - border-bottom-style: solid; - border-bottom-color: rgb(180,180,180); - border-bottom-width: 1px; -} - -div.slide { - z-index: 20; - margin: 0 0 0 0; - padding-top: 0; - padding-bottom: 0; - padding-left: 20px; - padding-right: 20px; - border-width: 0; - clear: both; - top: 0; - bottom: 0; - left: 0; - right: 0; - line-height: 120%; - background-color: transparent; -} - -div.background { - display: none; -} - -div.handout { - margin-left: 20px; - margin-right: 20px; -} - -div.slide.titlepage { - text-align: center; -} - -div.slide.titlepage h1 { - padding-top: 10%; - margin-right: 0; -} - -div.slide h1 { - padding-left: 0; - padding-right: 20pt; - padding-top: 4pt; - padding-bottom: 4pt; - margin-top: 0; - margin-left: 0; - margin-right: 60pt; - margin-bottom: 0.5em; - display: block; - font-size: 160%; - line-height: 1.2em; - background: transparent; -} - -@media screen and (max-device-width: 1024px) -{ - div.slide { font-size: 100%; } -} - -@media screen and (max-device-width: 800px) -{ - div.slide { font-size: 200%; } - div.slidy_toc { - top: 1em; - left: 1em; - right: auto; - width: 80%; - font-size: 180%; - } -} - -div.toc-heading { - width: 100%; - border-bottom: solid 1px rgb(180,180,180); - margin-bottom: 1em; - text-align: center; -} - -img { - image-rendering: optimize-quality; -} - -pre { - font-size: 80%; - font-weight: bold; - line-height: 120%; - padding-top: 0.2em; - padding-bottom: 0.2em; - padding-left: 1em; - padding-right: 1em; - border-style: solid; - border-left-width: 1em; - border-top-width: thin; - border-right-width: thin; - border-bottom-width: thin; - border-color: #95ABD0; - color: #00428C; - background-color: #E4E5E7; -} - -li pre { margin-left: 0; } - -blockquote { font-style: italic } - -img { background-color: transparent } - -p.copyright { font-size: smaller } - -.center { text-align: center } -.footnote { font-size: smaller; margin-left: 2em; } - -a img { border-width: 0; border-style: none } - -a:visited { color: navy } -a:link { color: navy } -a:hover { color: red; text-decoration: underline } -a:active { color: red; text-decoration: underline } - -a {text-decoration: none} -.toolbar a:link {color: blue} -.toolbar a:visited {color: blue} -.toolbar a:active {color: red} -.toolbar a:hover {color: red} - -ul { list-style-type: square; } -ul ul { list-style-type: disc; } -ul ul ul { list-style-type: circle; } -ul ul ul ul { list-style-type: disc; } -li { margin-left: 0.5em; margin-top: 0.5em; } -li li { font-size: 85%; font-style: italic } -li li li { font-size: 85%; font-style: normal } - -div dt -{ - margin-left: 0; - margin-top: 1em; - margin-bottom: 0.5em; - font-weight: bold; -} -div dd -{ - margin-left: 2em; - margin-bottom: 0.5em; -} - - -p,pre,ul,ol,blockquote,h2,h3,h4,h5,h6,dl,table { - margin-left: 1em; - margin-right: 1em; -} - -p.subhead { font-weight: bold; margin-top: 2em; } - -.smaller { font-size: smaller } -.bigger { font-size: 130% } - -td,th { padding: 0.2em } - -ul { - margin: 0.5em 1.5em 0.5em 1.5em; - padding: 0; -} - -ol { - margin: 0.5em 1.5em 0.5em 1.5em; - padding: 0; -} - -ul { list-style-type: square; } -ul ul { list-style-type: disc; } -ul ul ul { list-style-type: circle; } -ul ul ul ul { list-style-type: disc; } - -ul li { - list-style: square; - margin: 0.1em 0em 0.6em 0; - padding: 0 0 0 0; - line-height: 140%; -} - -ol li { - margin: 0.1em 0em 0.6em 1.5em; - padding: 0 0 0 0px; - line-height: 140%; - list-style-type: decimal; -} - -li ul li { - font-size: 85%; - font-style: italic; - list-style-type: disc; - background: transparent; - padding: 0 0 0 0; -} -li li ul li { - font-size: 85%; - font-style: normal; - list-style-type: circle; - background: transparent; - padding: 0 0 0 0; -} -li li li ul li { - list-style-type: disc; - background: transparent; - padding: 0 0 0 0; -} - -li ol li { - list-style-type: decimal; -} - - -li li ol li { - list-style-type: decimal; -} - -/* - setting class="outline on ol or ul makes it behave as an - ouline list where blocklevel content in li elements is - hidden by default and can be expanded or collapsed with - mouse click. Set class="expand" on li to override default -*/ - -ol.outline li:hover { cursor: pointer } -ol.outline li.nofold:hover { cursor: default } - -ul.outline li:hover { cursor: pointer } -ul.outline li.nofold:hover { cursor: default } - -ol.outline { list-style:decimal; } -ol.outline ol { list-style-type:lower-alpha } - -ol.outline li.nofold { - padding: 0 0 0 20px; - background: transparent url(../graphics/nofold-dim.gif) no-repeat 0px 0.5em; -} -ol.outline li.unfolded { - padding: 0 0 0 20px; - background: transparent url(../graphics/fold-dim.gif) no-repeat 0px 0.5em; -} -ol.outline li.folded { - padding: 0 0 0 20px; - background: transparent url(../graphics/unfold-dim.gif) no-repeat 0px 0.5em; -} -ol.outline li.unfolded:hover { - padding: 0 0 0 20px; - background: transparent url(../graphics/fold.gif) no-repeat 0px 0.5em; -} -ol.outline li.folded:hover { - padding: 0 0 0 20px; - background: transparent url(../graphics/unfold.gif) no-repeat 0px 0.5em; -} - -ul.outline li.nofold { - padding: 0 0 0 20px; - background: transparent url(../graphics/nofold-dim.gif) no-repeat 0px 0.5em; -} -ul.outline li.unfolded { - padding: 0 0 0 20px; - background: transparent url(../graphics/fold-dim.gif) no-repeat 0px 0.5em; -} -ul.outline li.folded { - padding: 0 0 0 20px; - background: transparent url(../graphics/unfold-dim.gif) no-repeat 0px 0.5em; -} -ul.outline li.unfolded:hover { - padding: 0 0 0 20px; - background: transparent url(../graphics/fold.gif) no-repeat 0px 0.5em; -} -ul.outline li.folded:hover { - padding: 0 0 0 20px; - background: transparent url(../graphics/unfold.gif) no-repeat 0px 0.5em; -} - -/* for slides with class "title" in table of contents */ -a.titleslide { font-weight: bold; font-style: italic } - -/* - hide images for work around for save as bug - where browsers fail to save images used by CSS -*/ -img.hidden { display: none; visibility: hidden } -div.initial_prompt { display: none; visibility: hidden } - - div.slide { - visibility: visible; - position: inherit; - } - div.handout { - border-top-style: solid; - border-top-width: thin; - border-top-color: black; - } - -@media screen { - .hidden { display: none; visibility: visible } - - div.slide.hidden { display: block; visibility: visible } - div.handout.hidden { display: block; visibility: visible } - div.background { display: none; visibility: hidden } - body.single_slide div.initial_prompt { display: block; visibility: visible } - body.single_slide div.background { display: block; visibility: visible } - body.single_slide div.background.hidden { display: none; visibility: hidden } - body.single_slide .invisible { visibility: hidden } - body.single_slide .hidden { display: none; visibility: hidden } - body.single_slide div.slide { position: absolute } - body.single_slide div.handout { display: none; visibility: hidden } -} - -@media print { - .hidden { display: block; visibility: visible } - - div.slide pre { font-size: 60%; padding-left: 0.5em; } - div.toolbar { display: none; visibility: hidden; } - div.slidy_toc { display: none; visibility: hidden; } - div.background { display: none; visibility: hidden; } - div.slide { page-break-before: always } - /* :first-child isn't reliable for print media */ - div.slide.first-slide { page-break-before: avoid } -} \ No newline at end of file diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php index 72c8b41f9..8e8eafe70 100644 --- a/src/library/WikiParser.php +++ b/src/library/WikiParser.php @@ -511,14 +511,14 @@ class WikiParser implements CrawlConstants C\NS_LIB . "makeTableCallback", $document); if ($page_type == "presentation") { $lines = explode("\n", $document); - $out_document = ""; + $out_document = "<x-slides>"; $slide = ""; - $div = "<div class='slide'>"; + $div = "<x-slide>"; foreach ($lines as $line) { if (trim($line) == "....") { $slide = $this->processRegexes($slide); - $out_document .= $div .$this->cleanLinksAndParagraphs( - $slide) ."</div>"; + $out_document .= $div . $this->cleanLinksAndParagraphs( + $slide) ."</x-slide>"; $slide = ""; } else { $slide .= $line."\n"; @@ -526,9 +526,10 @@ class WikiParser implements CrawlConstants } $last_slide = ""; if (!empty(trim($slide))) { - $last_slide = $div . $this->processRegexes($slide) . "</div>"; + $last_slide = $div . $this->processRegexes($slide) . + "</x-slide>"; } - $document = $out_document . $last_slide; + $document = $out_document . $last_slide . "</x-slides>"; } else if ($handle_big_files) { $document= $this->processRegexes($document); $document = $this->cleanLinksAndParagraphs($document); diff --git a/src/scripts/frise.js b/src/scripts/frise.js new file mode 100644 index 000000000..228042566 --- /dev/null +++ b/src/scripts/frise.js @@ -0,0 +1,3156 @@ +/** + * FRISE (FRee Interactive Story Engine) + * A light-weight engine for writing interactive fiction and games. + * + * Copyright 2022-2024 Christopher Pollett chris@pollett.org + * + * @license + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * @file The JS code used to manage a FRISE game + * @author Chris Pollett + * @link https://www.frise.org/ + * @copyright 2022 - 2024 + */ +/* + * Global Variables + */ +/** + * Contains {locale => {to_translate_id => translation}} for each + * item in the game engine that needs to be localized. + * There are not too many strings to translate. For a particular game you + * could tack on to this tl variable after you load game.js in a script + * tag before you call initGame. + * @type {Object} + */ +var tl = { + "en": { + "init_slot_states_load" : "Load", + "init_slot_states_save" : "Save", + "restore_state_invalid_game" : + "Does not appear to be a valid game save", + "move_object_failed" : "moveObject(object_id, location) failed.\n" + + "Either the object or location does not exist.\n", + "slide_titles" : "Slide Titles" + } +}; +/** + * Locale to use when looking up strings to be output in a particular language. + * We set the default here, but a particular game can override this in + * its script tag before calling initGame() + * @type {string} + */ +var locale = "en"; +/** + * Flag to set the text direction of the game to right-to-left. + * (if false, the text direction is left-to-right) + * @type {boolean} + */ +var is_right_to_left = false; +/** + * Global game object used to track one interactive story fiction game + * @type {Game} + */ +var game; +/** + * Flag used to tell if current user is interacting on a mobile device or not + * @type {boolean} + */ +var is_mobile = window.matchMedia("(max-width: 1000px)").matches; +/** + * Number of x-include tags left to load before game can run + * @type {int} + */ +var includes_to_load = 0; +/** + * Used as part of the closure of makeGameObject so that every game + * object has an integer id. + * @type {number} + */ +var object_counter = 0; +/** + * Common Global Functions + */ +/** + * Returns an Element object from the current document which has the provided + * id. + * @param {string} id of element trying to get an object for + * @return {Element} desired element, if exists, else null + */ +function elt(id) +{ + return document.getElementById(id); +} +/** + * Returns a collection of objects for Element's that match the provided Element + * name in the current document. + * + * @param {string} name of HTML/XML Element wanted from current document + * @return {HTMLCollection} of matching Element's + */ +function tag(name) +{ + return document.getElementsByTagName(name); +} +/** + * Returns a list of Node's which match the CSS selector string provided. + * + * @param {string} a CSS selector string to match against current document + * @return {NodeList} of matching Element's + */ +function sel(selector) +{ + return document.querySelectorAll(selector); +} +/** + * Returns a game object based on the element in the current document of the + * provided id. + * + * @param {string} id of element in current document. + * @return {Object} a game object. + */ +function xelt(id) +{ + return makeGameObject(elt(id)); +} +/** + * Returns an array of game objects based on the elements in the current + * document that match the CSS selector passed to it. + * + * @param {string} a CSS selector for objects in the current document + * @return {Array} of game objects based on the tags that matched the selector + */ +function xsel(selector) +{ + let tag_objects = sel(selector); + return makeGameObjects(tag_objects); +} +/** + * Returns an array of game objects based on the elements in the current + * document of the given tag name. + * + * @param {string} a name of a tag such as x-location or x-object. + * @return {Array} of game objects based on the tags that matched the selector + */ +function xtag(name) +{ + let tag_objects = tag(name); + return makeGameObjects(tag_objects); +} +/** + * Sleep time many milliseconds before continuing to execute whatever code was + * executing. This function returns a Promise so needs to be executed with await + * so that the code following the sleep statement will be used to resolve the + * Promise + * + * @param {number} time time in milliseconds to sleep for + * @return {Promise} a promise whose resolve callback is to be executed after + * that many milliseconds. + */ +function sleep(time) +{ + return new Promise(resolve => setTimeout(resolve, time)); +} +/** + * Adds a link with link text message which when clicked will allow the + * rendering of a Location to continue. + * @param {string} message link text for the link that is pausing the rendering + * of the current location. + * @param {string} any html that should appear after the proceed link + * @return {Promise} which will be resolved which the link is clicked. + */ +function clickProceed(message, rest = "") +{ + let game_content = elt("game-content"); + game.tick++; + game_content.innerHTML += + `<a id='click-proceed${game.tick}' href=''>${message}</a>${rest}`; + return new Promise(resolve => + elt(`click-proceed${game.tick}`).addEventListener("click", resolve)); +} + +/** + * Given a rectangle within an Image object of a DollSlot doll_part + * specified by a point x,y and a width and a height. Scales it to a + * corresponding rectangle in the DollSlot coordinates (coordinates + * used to specify where it is within a Doll). + * + * @param {int} x coordinate of the image rectangle + * @param {int} y coordinate of the image rectangle + * @param {int} width of the image rectangle + * @param {int} height of the image rectangle + * @param {DollSlot} doll_part with respect to which the coordinate + * transformation is done + * @param {Array<int>} of length 4 containing output [x, y, width, height] + */ + function imageCoordinates(x, y, width, height, doll_part) + { + let scale_x = doll_part.image.width/parseInt(doll_part.width); + let scale_y = doll_part.image.height/parseInt(doll_part.height); + let part_x = parseInt(doll_part.x ?? 0); + let part_y = parseInt(doll_part.y ?? 0); + let out_x = Math.floor(scale_x * (x - part_x)); + let out_y = Math.floor(scale_y * (y - part_y)); + let out_width = Math.floor(scale_x * width); + let out_height = Math.floor(scale_y * height); + return [out_x, out_y, out_width, out_height]; + } + +/** + * Given an array of arrays [Object, property_name1, property_name2, ...] + * Parses the property names to int's in the given object. If the + * this results in Not a Number (NaN) then sets the property to the + * provided default value + * + * @param Array<Array<Object, String...>> + * @param int default_value + + */ +function parseObjectsInts(objects_ints, default_value = 0) +{ + for (const object_ints of objects_ints) { + let obj = object_ints[0]; + for (field_name of object_ints[1]) { + let convert = obj[field_name]; + convert = parseInt(convert); + obj[field_name] = (isNaN(convert)) ? default_value : convert; + } + } +} +/** + * Creates from an HTMLCollection or Nodelist of x-object or x-location + * elements an array of Location's or Object's suitable for a FRISE Game + * object. Elements of the collection whose name aren't of the form x- + * or which don't have id's are ignored. Otherwise, the id field of + * the output Object or Location is set to the value of the x-object or + * x-location's id. Details about how a suitable FRISE Game object is created + * can be found @see makeGameObject() + * + * @param {HTMLCollection|Nodelist} tag_objects collection to be converted into + * an array FRISE game objects or location objects. + * @return {Array} an array of FRISE game or location objects. + */ +function makeGameObjects(tag_objects) +{ + let game_objects = {}; + for (const tag_object of tag_objects) { + let game_object = makeGameObject(tag_object); + if (game_object && game_object.id) { + game_objects[game_object.id] = game_object; + } + } + return game_objects; +} +/** + * Upper cases first letter of a string + * @param {string} str string to upper case the first letter of + * @return {string} result of upper-casing + */ +function upperFirst(str) +{ + if (!str) { + return ""; + } + let upper_first = str.charAt(0).toUpperCase(); + return upper_first + str.slice(1); +} +/** + * Given two rectangles: [x1, y1, width1, height1] and [x2, y2, width2, height2] + * Computes an intersection [x_out, y_out, width_out, height_out] of the + * rectangles if the rectangles intersect and outputs false otherwise. + * + * @param {Array<int>} rect1 4-tuple [x1, y1, width1, height1] of first + * rectangle + * @param {Array<int>} rect2 4-tuple [x1, y1, width1, height1] of second + * rectangle + * @return {Array<int>|boolean} 4-tuple [x_out, y_out, width_out, height_out] of + * intersection rectangle or false otherwise + */ +function intersectRectangles(rect1, rect2) +{ + [x1, y1, width1, height1] = rect1.map((x) => {return 1*x}); //make int + [x2, y2, width2, height2] = rect2.map((x) => {return 1*x}); + let x_out = Math.max(x1, x2); + let y_out = Math.max(y1, y2); + let width_out = Math.min(x1 + width1, x2 + width2) - x_out; + let height_out = Math.min(y1 + height1, y2 + height2) - y_out; + if (isNaN(x_out) || isNaN(y_out) || isNaN(width_out) || + isNaN(height_out) || width_out <= 0 || height_out <= 0) { + return false; + } + return [x_out, y_out, width_out, height_out]; +} +/** + * Used to convert a DOM Element dom_object to an Object or Location suitable + * for a FRISE game. dom_object's whose tagName does not begin with x- + * will result in null being returned. If the tagName is x-location, a + * Location object will be returned, otherwise, a Javascript Object is returned. + * The innerHTML of any subtag of an x-object or an + * x-location beginning with x- becomes the value of a field in the resulting + * object with the name of the tag less x-. For example, a DOM Object + * representing the following HTML code: + * <x-object id="bob"> + * <x-name>Robert Smith</x-name> + * <x-age>25</x-age> + * </x-object> + * will be processed to a Javascript Object + * { + * id: "bob", + * name: "Robert Smith", + * age: "25" + * } + * @param {Element} DOMElement to be convert into a FRISE game Object or + * Location + * @return {Object} the resulting FRISE game Object or Location or + * null if the tagName of the DOMElement didn't begin with x- + */ +function makeGameObject(dom_object) +{ + if (!dom_object || dom_object.tagName.substring(0, 2) != "X-") { + return null; + } + let tag_name = dom_object.tagName.slice(2); + let type = dom_object.getAttribute("type"); + if (type) { + type = upperFirst(type); + } else if (tag_name && tag_name != "OBJECT") { + type = upperFirst(tag_name.toLowerCase()); + } else { + type = "Object"; + } + let game_object; + if (type == "Location") { + game_object = new Location(); + } else if (type == "Doll") { + game_object = new Doll(); + } else if (type == "Slots") { + type = "DollSlots"; + game_object = new DollSlots(); + } else if (type == "Slot") { + type = "DollSlot"; + game_object = new DollSlot(); + } else { + game_object = {}; + } + if (dom_object.id) { + game_object.id = dom_object.id; + } else { + game_object.id = "oid" + object_counter; + object_counter++; + } + let style = dom_object.getAttribute('style'); + if (typeof style != 'undefined') { + game_object.style = style; + } + let class_list = dom_object.getAttribute('class'); + if (typeof class_list != 'undefined') { + game_object["class"] = class_list; + } + let has = { + 'color' : false, + 'icon' : false, + 'height' : false, + 'position' : false, + 'present' : false, + 'slots' : false, + 'width' : false + }; + let script_tags = { + "text/action" : "X-ACTION", + "text/default-action" : "X-DEFAULT-ACTION", + "text/click-listener" : "X-CLICK-LISTENER", + "text/collision-listener" : "X-COLLISION-LISTENER", + "text/update-listener" : "X-UPDATE-LISTENER", + }; + for (const child of dom_object.children) { + let tag_name = child.tagName; + if (tag_name == 'SCRIPT') { + let script_type = child.getAttribute("type"); + if (typeof script_tags[script_type] != 'undefined') { + tag_name = script_tags[script_type]; + } + } + if (tag_name.substring(0, 2) != "X-") { + continue; + } + let attribute_name = tag_name.slice(2); + if (attribute_name) { + attribute_name = attribute_name.toLowerCase(); + has[attribute_name] = true; + if (attribute_name == 'present') { + if (!game_object[attribute_name]) { + game_object[attribute_name] = []; + } + let check = ""; + let is_else = false; + for(let check_attr of ['ck', 'check', 'else-ck', + 'else-check', 'else']) { + let tmp_check = child.getAttribute(check_attr); + if (tmp_check) { + check = tmp_check; + if (['else-ck', 'else-check'].includes(check_attr)) { + is_else = true; + } + break; + } + } + let stage = child.getAttribute("stage"); + if (!stage) { + stage = ""; + } + game_object[attribute_name].push([check, stage, is_else, + child.innerHTML]); + } else if (type == 'Doll' && attribute_name == 'slots') { + game_object[attribute_name] = makeGameObject(child); + } else if (type == 'DollSlots' && attribute_name == 'slot') { + let slot = makeGameObject(child); + if (slot.type == 'DollSlot') { + game_object.push(slot); + } + } else { + game_object[attribute_name] = child.innerHTML; + } + } + } + game_object.type = type; + if (type == 'Location') { + game_object.has_present = has['present']; + } else if (type == 'Object') { + game_object.has_position = has['position']; + } else if (type == 'Doll') { + game_object.has_height = has['height']; + game_object.has_icon = has['icon']; + game_object.has_color = has['color']; + game_object.has_width = has['width']; + if (!has['slots']) { + game_object.slots = new DollSlots(); + } + game_object.init(); + } + return game_object; +} +/** + * Used to change the display css property of the element of the provided id + * to display_type if it doesn't already have that value, if it does, + * then the display property is set to none. If display_type is not provided + * then its value is assumed to be block. + * + * @param {string} id of HTML Element to toggle display property of + * @param {string} display_type value to toggle between it and none. + */ +function toggleDisplay(id, display_type = "block") +{ + let obj = elt(id); + if (obj.style.display == display_type) { + value = "none"; + } else { + value = display_type; + } + obj.style.display = value; + if (value == "none") { + obj.setAttribute('aria-hidden', true); + } else { + obj.setAttribute('aria-hidden', false); + } +} +/** + * Used to toggle the display or non-display of the main navigation bar + * on the side of game screen + */ +function toggleMainNav() +{ + let game_content = elt('game-content'); + let nav_obj = elt('main-nav'); + if ((!nav_obj.style.left && !nav_obj.style.right) || + nav_obj.style.left == '0px' || nav_obj.style.right == '0px') { + game_content.style.width = "calc(100% - 40px)"; + if (is_right_to_left) { + game_content.style.right = "55px"; + nav_obj.style.right = '-300px'; + } else { + game_content.style.left = "55px"; + nav_obj.style.left = '-300px'; + } + if (is_mobile) { + nav_obj.style.width = "240px"; + game_content.style.width = "calc(100% - 70px)"; + } + nav_obj.style.backgroundColor = 'white'; + } else { + if (is_right_to_left) { + nav_obj.style.right = '0px'; + game_content.style.right = "300px"; + } else { + nav_obj.style.left = '0px'; + game_content.style.left = "300px"; + } + game_content.style.width = "calc(100% - 480px)"; + if (is_mobile) { + nav_obj.style.width = "100%"; + if (is_right_to_left) { + game_content.style.right = "100%"; + } else { + game_content.style.left = "100%"; + } + } + nav_obj.style.backgroundColor = 'lightgray'; + } +} +/** + * Adds click event listeners to all anchor objects in a list + * that have href targets beginning with #. Such a target + * is to a location within the current game, so the click event callback + * then calls game.takeTurn based on this url. + * + * @param {NodeList|HTMLCollection} anchors to add click listeners for + * game take turn callbacks. + */ +function addListenersAnchors(anchors) +{ + let call_toggle = false; + if (arguments.length > 1) { + if (arguments[1]) { + call_toggle = true; + } + } + for (const anchor of anchors) { + let url = anchor.getAttribute("href"); + if (url && url[0] == "#") { + let hash = url; + anchor.innerHTML = "<span tabindex='0'>" +anchor.innerHTML + + "</span>"; + let handle = async (event) => { + let transition = anchor.getAttribute('data-transition'); + let duration = anchor.getAttribute('data-duration'); + let iterations = anchor.getAttribute('data-iterations'); + let origin = anchor.getAttribute('data-transform-origin'); + let preset_origins = { 'top-left-origin' : "0 0", + 'top-mid-origin' : "50% 0", + 'top-right-origin' : "100% 0", + 'bottom-left-origin' : "0 50%", + 'bottom-mid-origin' : "50% 100%", + 'bottom-right-origin' : "100% 100%", + 'mid-left-origin' : "0 50%", + 'mid-right-origin' : "100% 50%", + 'mid-mid-origin' : "50% 50%", + 'center-origin' : "50% 50%" + }; + if (transition) { + let game_content = elt('game-content'); + game_content.classList.add(transition); + duration = (duration) ? parseInt(duration) : 2000; + iterations = (iterations) ? parseInt(iterations) : 1; + origin ??= "50% 50%"; + if (typeof preset_origins[origin] != 'undefined') { + origin = preset_origins[origin]; + } + game_content.style.animationDuration = duration + "ms"; + game_content.style.animationIterationCount = iterations; + game_content.style.transformOrigin = origin; + await sleep(duration); + elt('game-content').classList.remove(transition); + } + let is_slides = sel("x-slides")[0] ? true : false; + if (is_slides) { + window.location = hash; + game.takeTurn(hash); + return; + } + if (!anchor.classList.contains('disabled')) { + game.takeTurn(hash); + if (game.has_nav_bar && call_toggle) { + toggleMainNav(); + } + } + event.preventDefault(); + if (window.location.hash) { + delete window.location.hash; + } + }; + anchor.addEventListener('keydown', (event) => { + if (event.code == 'Enter' || event.code == 'Space') { + handle(event); + } + }); + anchor.addEventListener('click', (event) => handle(event)); + } else if (url) { + anchor.innerHTML = "<span tabindex='0'>" +anchor.innerHTML + + "</span>"; + anchor.addEventListener('click', (event) => { + window.location = url; + }); + } + } +} +/** + * Used to disable any volatile links which might cause + * issues if clicked during rendering of the staging + * portion of a presentation. For example, saves screen links + * in either the main-nav panel/main-nav bar or in the + * game content area. The current place where this is used + * is at the start of rendering a location. A location + * might have several x-present tags, some of which could + * be drawn after a clickProceed, or a delay. As these + * steps in rendering are not good "save places", at the + * start of rendering a location, all save are disabled. + * Only after the last drawable x-present for a location + * has completed are they renabled. + */ +function disableVolatileLinks() +{ + for (let location_id of game.volatileLinks()) { + let volatile_links = sel(`[href~="#${location_id}"]`); + for (const volatile_link of volatile_links) { + volatile_link.classList.add('disabled'); + } + } +} +/** + * Used to re-enable any disabled volatile links + * in either the main-nav panel/main-nav bar or in the + * game content area. The current place where this is used + * is at the end of rendering a location. For why this is called, + * @see disableVolatileLinks() + */ +function enableVolatileLinks() +{ + for (let location_id of game.volatileLinks()) { + let volatile_links = sel(`[href~="#${location_id}"]`); + for (const volatile_link of volatile_links) { + volatile_link.classList.remove('disabled'); + } + } +} +/** + * Given a string which may contain Javascript string interpolation expressions, + * i.e., ${some_expression}, produces a string where those expressions have + * been replaced with their values. + * + * @param {string} text to have Javascript interpolation expressions replaced + * with their values + * @return {string} result of carrying out the replacement + */ +function interpolateVariables(text) +{ + old_text = ""; + while (old_text != text) { + old_text = text; + let interpolate_matches = text.match(/\$\{([^\}]+)\}/); + if (interpolate_matches && interpolate_matches[1]) { + let interpolate_var = interpolate_matches[1]; + if (interpolate_var.replace(/\s+/, "") != "") { + interpolate_var = interpolate_var.replaceAll(/\>\;?/g, ">"); + interpolate_var = interpolate_var.replaceAll(/\<\;?/g, "<"); + interpolate_var = interpolate_var.replaceAll(/\&\;?/g, "&"); + if (interpolate_var) { + let interpolate_value = eval(interpolate_var); + text = text.replace(/\$\{([^\}]+)\}/, interpolate_value); + } + } + } + } + return text; +} +/** + * A data-ck attribute on any tag other than an x-present tag can + * contain a Javascript boolean expression to control the display + * or non-display of that element. This is similar to a ck attribute + * of an x-present tag. This method evalutes data-ck expressions for + * each tag in the text in its section argument and adds a class="none" + * attribute to that tag if it evaluates to false (causing it not to + * display). The string after these substitutions is returned. + * + * @param {string} section of text to check for data-ck attributes and + * for which to carry out the above described substitutions + * @return {string} after substitutions have been carried out. + */ +function evaluateDataChecks(section) +{ + let quote = `(?:(?:'([^']*)')|(?:"([^"]*)"))`; + let old_section = ""; + while (section != old_section) { + old_section = section; + let data_ck_pattern = new RegExp( + "\<(?:[^\>]+)((?:data\-)?ck\s*\=\s*(" + quote + "))(?:[^\>]*)\>", + 'i'); + section = section.replace(/\&/g, "&"); + let data_ck_match = section.match(data_ck_pattern); + if (!data_ck_match || section.includes("cli" + data_ck_match[1])) { + //don't want above to accidentally also match onclick= + continue; + } + let condition = (typeof data_ck_match[3] == 'string') ? + data_ck_match[3] : data_ck_match[4]; + if (typeof condition == 'string') { + let check_result = (condition.replace(/\s+/, "") != "") ? + eval(condition) : true; + if (check_result) { + section = section.replace(data_ck_match[1], " "); + } else { + section = section.replace(data_ck_match[1], + " class='none' "); + } + } + } + return section; +} +/** + * Returns the game object with the provided id. + * @param {string} object_id to get game object for + * @return {object} game object associated with object_id if it exists + */ +function obj(object_id) +{ + return game.objects[object_id]; +} +/** +* Returns the game location with the provided id. +* @param {string} object_id to get game Location for +* @return {Location} game location associated with object_id if it exists + */ +function loc(location_id) +{ + return game.locations[location_id]; +} +/** +* Returns the game doll (paperdoll) with the provided id. +* @param {string} object_id to get game doll for +* @return {Location} game doll associated with object_id if it exists + */ +function doll(doll_id) +{ + return game.dolls[doll_id]; +} +/** + * Returns the game object associated with the main-character. This + * function is just an abbreviation for obj('main-character') + * @return {object} game object associated with main-character + */ +function mc() +{ + return game.objects['main-character']; +} +/** + * Returns the location object associated with the main-character current + * position. + * @return {Location} associated with main-character position + */ +function here() +{ + return game.locations[game.objects['main-character'].position]; +} +/** + * Returns the Location object the player begins the game at + */ +function baseLoc() +{ + return game.locations[game.base_location]; +} +/** + * For use in default actions only!!! Returns whether the main-character is + * in the Location of the default action. In all other cases returns false + * @return {boolean} where the main-character is in the room of the current + * default action + */ +function isHere() +{ + return game['is_here']; +} +/** + * Returns whether the main character has ever been to the location + * given by location_id + * + * @param {string} location_id id of location checking if main character has + * been to + * @return {boolean} whther the main chracter has been there + */ +function hasVisited(location_id) +{ + return (loc(location_id).visited > 0); +} +/** + * Encapsulates one place that objects can be in a Game. + */ +class Location +{ + /** + * An array of [check_condition, staging, is_else, text_to_present] tuples + * typically coming from the x-present-tag's in the HTML of a Location. + * @type {Array} + */ + present = []; + /** + * Number of times main-character has visited a location + * @type {int} + */ + visited = 0; + /** + * Used to display a description of a location to the game content + * area. This description is based on the x-present tags that were + * in the x-location tag from which the Location was parse. For + * each such x-present tag in the order it was in the original HTML, + * the ck/else-ck condition and staging is first evaluated + * once/if the condition is satisfied, staging is processed (this may + * contain a delay or a clickProceed call), then the HTML contents of the + * tag are shown. In the case where the ck or else-ck evaluates to false + * than the x-present tag's contents are omitted. In addition to the + * usual HTML tags, an x-present tag can have x-speaker subtags. These + * allow one to present text from a speaker in bubbles. An x-present tag + * may also involve input tags to receive/update values for x-objects or + * x-locations. + */ + async renderPresentation() + { + disableVolatileLinks(); + let game_content = elt("game-content"); + game_content.innerHTML = ""; + if (typeof this["style"] != 'undefined' && this["style"]) { + game_content.setAttribute('style', this["style"]); + } + if (typeof this["class"] != 'undefined' && this["class"]) { + game_content.setAttribute('class', this["class"]); + } + game_content.scrollTop = 0; + game_content.scrollLeft = 0; + let check, staging, is_else, section_html; + let check_result, proceed, pause; + check_result = false; + for (let section of this.present) { + if (!section[3]) { + continue; + } + [check, staging, is_else, section_html] = section; + if (is_else && check_result) { + continue; + } + [check_result, proceed, pause] = + this.evaluateCheckConditionStaging(check, staging); + if (check_result) { + let prepared_section = this.prepareSection(section_html); + if (proceed) { + let old_inner_html = game_content.innerHTML; + event = await clickProceed(proceed); + event.preventDefault(); + game_content.innerHTML = old_inner_html; + } else if (pause) { + await sleep(pause); + } + /* + A given prepared section might involve + <x-stage></x-stage> tags to simulate a typewriter effect, + or delays or clicks to continue. + The code below is used to present these tags. + */ + let rest_section = prepared_section; + let final_section = ""; + let xstage_open = "<x-stage"; + let xstage_close = "</x-stage"; + let open_pos, end_pos, greater_pos; + end_pos = rest_section.lastIndexOf(xstage_close); + if (end_pos > 0) { + final_section = rest_section.substring(end_pos + + xstage_close.length); + greater_pos = final_section.search(">"); + if (greater_pos >= 0) { + final_section = final_section.substring(greater_pos + 1); + } else { + greater_pos = final_section.length; + } + rest_section = rest_section.substring(0, end_pos + + xstage_close.length + greater_pos); + } + let inner_html = game_content.innerHTML; + while ((open_pos = rest_section.search(xstage_open)) != -1) { + let animation_speed = 50; + let click_matches = false; + let is_short_tag = false; + inner_html += rest_section.substring(0, open_pos); + rest_section = rest_section.substring(open_pos + + xstage_open.length); + greater_pos = rest_section.search(">"); + if (rest_section.charAt(greater_pos - 1) == "/") { + is_short_tag = true; + } + if (greater_pos >= 0) { + let tag_contents = rest_section.substring(0, + greater_pos + 1); + let delay_matches = tag_contents.match( + /delay\=(\"(\d+)\"|\'(\d+)\')/); + if (delay_matches) { + animation_speed = + (typeof delay_matches[2] == 'undefined') ? + delay_matches[3] : delay_matches[2]; + animation_speed = parseInt(animation_speed); + } + click_matches = tag_contents.match( + /delay\=(\"click\"|\'click\')/); + rest_section = rest_section.substring(greater_pos + 1); + } else { + rest_section = ""; + } + end_pos = rest_section.search(xstage_close); + open_pos = rest_section.search(xstage_open); + let stage_text = ""; + if (is_short_tag || end_pos == -1 || (open_pos > 0 && + end_pos > open_pos)) { + end_pos = 0; + // assume tag was not for delayed typing, just for delay + } else { + stage_text = rest_section.substring(0, end_pos); + rest_section = rest_section.substring(end_pos + + xstage_close.length); + greater_pos = rest_section.search(">"); + if (greater_pos >= 0) { + rest_section = + rest_section.substring(greater_pos + 1); + } + } + if (click_matches) { + game_content.innerHTML = inner_html; + event = await clickProceed(stage_text); + event.preventDefault(); + event.stopPropagation(); + game_content.innerHTML = inner_html; + } else { + let old_length = inner_html.length; + inner_html += stage_text; + game.stop_stage = false; + let game_content = elt('game-content'); + game_content.setAttribute('tabindex', 0); + game_content.addEventListener('click', () => { + game.stop_stage = true; + }); + game_content.addEventListener('keydown', () => { + game.stop_stage = true; + }); + await sleep(animation_speed); + for (let i = old_length; i <= old_length + end_pos; + i++) { + game_content.innerHTML = inner_html.substring(0, i) + await sleep(animation_speed); + if (game.stop_stage) { + game_content.onclick = null; + break; + } + } + } + } + game_content.innerHTML = inner_html + rest_section + + final_section; + } + } + // footer used so a game location page doesn't get squished + if (!sel("x-slides")[0]) { + game_content.innerHTML += "<div class='footer-space'></div>"; + } + this.prepareControls(); + if (this['screen-footer']) { + let footer = elt('screen-footer'); + if (!footer) { + footer = document.createElement('div'); + footer.id = 'screen-footer'; + elt('game-screen').appendChild(footer); + } + footer.innerHTML = this['screen-footer']; + } + let anchors = sel("#game-screen a, #game-screen x-button"); + addListenersAnchors(anchors); + this.renderDollHouses(); + if (typeof sessionStorage['show_footer'] != 'undefined') { + if (!sessionStorage['show_footer'] || + sessionStorage['show_footer'] == 'false') { + let slide_footer = + sel('#game-screen .slide-footer')[0]; + slide_footer.style.display = 'none'; + } + } + if (!game.volatileLinks().includes(mc().position)) { + enableVolatileLinks(); + } + } + /** + * For each Dollhouse in the current main-nav and game-content + * areas of the HTML browser window draw the Dollhouses contents to a + * canvas tag associated with it. Then sets up any key, click, and + * collision listeners associated with the Dollhouse to handle events + * on that canvas. + */ + renderDollHouses() + { + for (const screen_id of ["main-nav", "game-content"]) { + let screen_part = elt(screen_id); + if (!screen_part) { + continue; + } + let part_width = screen_part.clientWidth; + let part_height = screen_part.clientHeight; + let dom_dollhouses = sel(`#${screen_id} x-dollhouse`); + for (const dom_dollhouse of dom_dollhouses) { + let dollhouse = makeGameObject(dom_dollhouse); + if (!dollhouse['doll-id']) { + continue; + } + let doll_obj = doll(dollhouse['doll-id']); + if (!doll_obj) { + continue; + } + let width = dollhouse.width ?? part_width; + let height = dollhouse.height ?? part_height; + let source_x = dollhouse['source-x'] ?? (doll_obj['source-x'] ?? + 0); + dollhouse['source-x'] = source_x; + let source_y = dollhouse['source-y'] ?? (doll_obj['source-y'] ?? + 0); + dollhouse['source-y'] = source_y; + let source_width = dollhouse['source-width'] ?? + (doll_obj['source-width'] ?? doll_obj.width); + dollhouse['source-width'] = source_width; + let source_height = dollhouse['source-height'] ?? + (doll_obj['source-height'] ?? doll_obj.height); + dollhouse['source-height'] = source_height; + let canvas = document.createElement('canvas'); + let dom_class = dom_dollhouse.getAttribute('class'); + dom_class = (dom_class) ? `block ${dom_class}` : 'block'; + let dom_style = dom_dollhouse.getAttribute('style'); + dom_style ??= ''; + canvas.setAttribute('class', dom_class); + canvas.setAttribute('style', dom_style); + canvas.setAttribute('width', width); + canvas.setAttribute('height', height); + canvas.setAttribute('data-source-x', source_x); + canvas.setAttribute('data-source-y', source_y); + canvas.setAttribute('data-source-width', source_width); + canvas.setAttribute('data-source-height', source_height); + let is_navigable = (typeof dollhouse.navigable != 'undefined' && + (dollhouse.navigable === true || + dollhouse.navigable === "true")); + if (is_navigable) { + let player_slot = doll_obj.getPlayerSlot(dollhouse); + player_slot.init(doll_obj); + dollhouse.player_slot = player_slot; + } + dollhouse.doll = doll_obj; + dollhouse.canvas = canvas; + dom_dollhouse.parentNode.insertBefore(canvas, + dom_dollhouse.nextSibling); + doll_obj.render(canvas); + if (is_navigable || + doll_obj.has_collision_listeners || + doll_obj.has_click_listeners || + doll_obj.has_update_listeners) { + this.addDollHouseListeners(dollhouse); + } + } + } + } + /** + * Sets up the canvas element listeners associated with a Dollhouse. + * Possible + * + * @param {Dollhouse} whose canvas will add listeners to + */ + addDollHouseListeners(dollhouse) + { + let canvas = dollhouse.canvas; + let doll_obj = dollhouse.doll; + let player_slot = doll_obj.getPlayerSlot(dollhouse); + dollhouse.player_slot = player_slot; + if (dollhouse.initialized) { + return; + } + if ((dollhouse.navigable === true || + dollhouse.navigable === "true")) { + parseObjectsInts([ [player_slot, ["x", "y", "width", "height"]], + [doll_obj, ["width", "height"]], [dollhouse, ["source-x", + "source-y", "source-width", "source-height", + 'padding-top', 'padding-left', 'padding-bottom', + 'padding-right']] ]); + dollhouse['source-width'] = (dollhouse['source-width'] > 0) ? + dollhouse['source-width'] : doll_obj.width; + dollhouse['source-height'] = (dollhouse['source-height'] > 0) ? + dollhouse['source-height'] : doll_obj.height; + let player_half_width = Math.floor(player_slot.width/2); + let player_half_height = Math.floor(player_slot.height/2); + let step_x = dollhouse['player-step-x'] ?? + (dollhouse['player-step'] ?? 1); + step_x = parseInt(step_x); + let step_y = dollhouse['player-step-y'] ?? + (dollhouse['player-step'] ?? 1); + step_y = parseInt(step_y); + let scroll_step_x = 0; + let scroll_step_y = 0; + let x_min = dollhouse['source-x'] + player_half_width; + let y_min = dollhouse['source-y'] + player_half_height; + let x_max = dollhouse['source-x'] + dollhouse['source-width'] - + player_half_width; + let y_max = dollhouse['source-y'] + dollhouse['source-height'] - + player_half_height; + if (dollhouse['scrollable'] && dollhouse['scrollable'] == "true") { + scroll_step_x = step_x; + scroll_step_y = step_y; + x_min = player_half_width; + x_max = doll_obj.width - player_half_width; + y_min = player_half_height; + y_max = doll_obj.height - player_half_height; + } + x_min += dollhouse['padding-left']; + x_max -= dollhouse['padding-right']; + y_min += dollhouse['padding-top']; + y_max -= dollhouse['padding-bottom']; + canvas.setAttribute('tabindex', 0); + canvas.focus(); + canvas.addEventListener('keydown', event => { + if (player_slot.old_x && + dollhouse.has_collision_listeners) { + return; + } + let change = false; + let old_values = [player_slot.x, player_slot.y, + dollhouse['source-x'], dollhouse['source-y'], + player_slot.width, player_slot.height + ]; + let x_pos = player_slot.x + player_half_width; + let y_pos = player_slot.y + player_half_height; + switch(event.code) { + case 'KeyW': + case 'ArrowUp': + if (y_pos > y_min) { + change = true; + dollhouse['source-y'] -= scroll_step_y; + player_slot.y -= step_y; + } + break; + case 'KeyS': + case 'ArrowDown': + if (y_pos < y_max) { + change = true; + dollhouse['source-y'] += scroll_step_y; + player_slot.y += step_y; + } + break; + case 'KeyA': + case 'ArrowLeft': + if (x_pos > x_min) { + change = true; + dollhouse['source-x'] -= scroll_step_x; + player_slot.x -= step_x; + } + break; + case 'KeyD': + case 'ArrowRight': + if (x_pos < x_max) { + change = true; + dollhouse['source-x'] += scroll_step_x; + player_slot.x += step_x; + } + break; + } + if (change) { + [player_slot.old_x, player_slot.old_y, + player_slot.old_source_x, player_slot.old_source_y, + player_slot.old_width, player_slot.old_height] = + old_values; + } + }); + } + if (doll_obj.has_click_listeners) { + canvas.addEventListener("click", (event) => { + if (doll_obj['click-listener']) { + eval(doll_obj['click-listener']); + } + if (doll_obj.slots.has_click_listeners) { + let rect = canvas.getBoundingClientRect(); + let x = event.clientX - rect.left; + let y = event.clientY - rect.top; + let source_x = dollhouse['source-x'] ?? 0; + let source_y = dollhouse['source-y'] ?? 0; + let source_width = dollhouse['source-width'] ?? + doll_obj.width; + let source_height = dollhouse['source-height'] ?? + doll_obj.height; + x = Math.floor((source_width * x / canvas.width) + source_x); + y = Math.floor((source_height * y / canvas.height) + + source_y); + for (const slot of doll_obj.slots.items) { + if (typeof slot != 'undefined' && + slot['click-listener']) { + parseObjectsInts([[slot, + ["x", "y", "width", "height"]]]); + if (slot.x <= x && x <= slot.x + slot.width && + slot.y <= y && y <= slot.y + slot.height) { + eval(slot['click-listener']); + doll_obj.redraw = true; + } + } + } + } + }); + } + if (dollhouse['navigable'] || doll_obj.has_update_listeners || + doll_obj.has_collision_listeners || + doll_obj.has_click_listeners) { + let refresh = (dollhouse.refresh) ? parseInt(dollhouse.refresh): + game.dollhouse_refresh_rate; + game.dollhouse_update_ids[game.dollhouse_update_ids.length] = + setInterval((event) => { + if (doll_obj['update-listener']) { + eval(doll_obj['update-listener']); + doll_obj.redraw = true; + } + for (const slot of doll_obj.slots.items) { + if (typeof slot != 'undefined' && + slot['update-listener']) { + eval(slot['update-listener']); + doll_obj.redraw = true; + } + } + if (player_slot && player_slot.old_x) { + doll_obj.redraw = true; + } + if (doll_obj.has_collision_listeners) { + doll_obj.checkCollisions(dollhouse); + } + if (player_slot && player_slot.old_x) { + delete player_slot.old_x; + delete player_slot.old_y; + delete player_slot.old_source_x; + delete player_slot.old_source_y; + delete player_slot.old_width; + delete player_slot.old_height; + } + if (doll_obj.redraw) { + let source_x = dollhouse['source-x'] ?? 0; + canvas.setAttribute('data-source-x', source_x); + let source_y = dollhouse['source-y'] ?? 0; + canvas.setAttribute('data-source-y', source_y); + doll_obj.render(canvas); + doll_obj.redraw = false; + } + }, refresh); + } + dollhouse.initialized = true; + } + /** + * Prepares input, textareas, and select tags in the game so that they can + * bind to game Object or Location fields by adding various Javascript + * Event handlers. An input tag like + * <input data-for="bob" name="name" > + * binds with the name field of the bob game object (i.e., obj(bob).name). + * In the case above, as the default type of an input tag is text, this + * would produce a text field whose initial value is the current value + * obj(bob).name. If the user changes the field, the value of the obj + * changes with it. This set up binds input tags regardless of type, so it + * can be used with other types such as range, email, color, etc. + */ + prepareControls() + { + const content_areas = ["main-nav", "game-content"]; + for (const content_area of content_areas) { + let content = elt(content_area); + if (!content) { + continue; + } + if (content_area == 'main-nav') { + if (!content.hasOwnProperty("originalHTML")) { + content.originalHTML = content.innerHTML; + } + content.innerHTML = interpolateVariables(content.originalHTML); + content.innerHTML = evaluateDataChecks(content.innerHTML); + game.initializeGameNavListeners(); + } + let control_types = ["input", "textarea", "select"]; + for (const control_type of control_types) { + let control_fields = content.querySelectorAll(control_type); + for (const control_field of control_fields) { + let target_object = null; + let target_name = control_field.getAttribute("data-for"); + if (typeof target_name != "undefined") { + if (game.objects[target_name]) { + target_object = game.objects[target_name]; + } else if (game.locations[target_name]) { + target_object = game.locations[target_name]; + } + if (target_object) { + let target_field = control_field.getAttribute( + "name"); + let control_subtype = ''; + if (target_field) { + if (control_type == "input") { + control_subtype = + control_field.getAttribute("type"); + if (control_subtype == 'radio') { + if (control_field.value == + target_object[target_field]) { + control_field.checked = + target_object[target_field]; + } + } else { + control_field.value = + target_object[target_field]; + } + } else if (target_object[target_field]) { + /* if don't check + target_object[target_field] not empty + then select tags get extra blank option + */ + control_field.value = + target_object[target_field]; + } + if(!control_field.disabled) { + if (control_type == "select") { + control_field.addEventListener("change", + (evt) => { + target_object[target_field] = + control_field.value; + }); + } else if (control_subtype == "radio") { + control_field.addEventListener("click", + (evt) => { + if (control_field.checked) { + target_object[target_field] = + control_field.value; + } + }); + } else { + control_field.addEventListener("input", + (evt) => { + target_object[target_field] = + control_field.value; + }); + } + } + } + } + } + } + } + } + } + /** + * Evaluates the condition in a ck or else-ck attribute of an x-present tag. + * + * @param {string} condition contents from a ck, check, else-ck, + * or else-check attribute. Conditions can be boolean conditions + * on game variables. If an x-present tag did not have a ck attribute, + * condition is null. + * @param {string} staging contents from a stage attribute. + * If no such attribute, this will be an empty string. + * Such an attribute could have a sequence of + * pause(some_millisecond); and clickProceed(some_string) commands + * @return {Array} [check_result, proceed, pause, typing] if the condition + * involved a boolean expression, then check_result will hold the result + * of the expression (so the caller then could prevent the the display of + * an x-present tag if false), proceed is the link text (if any) for a + * link for the first clickProceed (which is supposed to delay the + * presentation of the x-present tag until after the user clicks the + * link) is found (else ""), pause (if non zero) is the number of + * milliseconds to sleep before presenting the x-present tag according to + * the condition, + */ + evaluateCheckConditionStaging(condition, staging) + { + let proceed = ""; + let pause = 0; + condition = (typeof condition == "string") ? condition : ""; + let check_result = (condition.replace(/\s+/, "") != "") ? + eval(condition) : true; + if (typeof check_result != "boolean") { + check_result = false; + console.log(condition + " didn't evaluate to a boolean"); + } + let staging_remainder = staging; + let old_staging = ""; + while (check_result && old_staging != staging_remainder) { + old_staging = staging_remainder; + let click_pattern = /clickProceed\([\'\"]([^)]+)[\'\"]\);?/; + let click_match = staging_remainder.match(click_pattern); + if (click_match) { + proceed = click_match[1]; + break; + } + let pause_pattern = /pause\(([^)]+)\);?/; + let pause_match = staging_remainder.match(pause_pattern); + if (pause_match) { + pause += parseInt(pause_match[1]); + } + } + return [check_result, proceed, pause]; + } + /** + * A given Location contains one or more x-present tags which are + * used when rendering that location to the game-content area. This + * method takes the text from one such tag, interpolates any game + * variables into it, and adds to any x-speaker tags in it the HTML + * code to render that speaker bubble (adds img tag for speaker icon, etc). + * + * @param {string} section text to process before putting into the + * game content area. + * @return {string} resulting HTML text after interpolation and processing + */ + prepareSection(section) + { + let old_section = ""; + let quote = `(?:(?:'([^']*)')|(?:"([^"]*)"))`; + section = evaluateDataChecks(section); + section = interpolateVariables(section); + while (section != old_section) { + old_section = section; + let speaker_pattern = new RegExp( + "\<x-speaker([^\>]*)name\s*\=\s*(" + quote + ")([^\>]*)>", + 'i'); + let expression_pattern = new RegExp( + "expression\s*\=\s*("+quote+")", 'i'); + let speaker_match = section.match(speaker_pattern); + if (speaker_match) { + let speaker_id = speaker_match[4]; + let pre_name = (speaker_match[3]) ? speaker_match[3] : ""; + let post_name = (speaker_match[5]) ? speaker_match[5] : ""; + let rest_of_tag = pre_name + " " + post_name; + let expression_match = rest_of_tag.match(expression_pattern); + if (speaker_id && game.objects[speaker_id]) { + let speaker = game.objects[speaker_id]; + let name = speaker.name; + if (expression_match && expression_match[3]) { + name += " <b>(" + expression_match[3] + ")</b>"; + } + let icon = speaker.icon; + let html_fragment = speaker_match[0]; + let html_fragment_mod = html_fragment.replace(/name\s*\=/, + "named="); + if (icon) { + section = + section.replace(html_fragment, html_fragment_mod + + "<figure><img src='" + speaker.icon + "' " + + "loading='lazy' ></figure><div>" + name + + "</div><hr>"); + } + } + } + } + return section; + } +} +/** + * Encapsulates one paperdoll that might be drawn or animated to + * a canvas in the Game. A Doll has a width and height and either + * an image or background color. It also has a number of DollSlot + * subrectangles, on which other images or solid colors may be drawn. + * DollSlots can be animated if they responds to update events, can collide + * if respond to collision events, or can be clicked on if they respond to + * click events. A Doll can be navigable if it has a player dollslot + * and scrollable if it is vaigable and its dimension are large than its + * viewable area. + */ +class Doll +{ + /** + * Whether the current Doll or any of its DollSlot objects respond to + * click events + * @type {boolean} + */ + has_click_listeners = false; + /** + * Whether the current Doll or any of its DollSlot objects have an update + * listener specified to be called according to the update time function + * @type {boolean} + */ + has_update_listeners = false; + /** + * Whether the current Doll or any of its DollSlot objects respond to + * collision between DollSlot events + * @type {boolean} + */ + has_collision_listeners = false; + /** + * Number of images in DollSlot object on this Doll that + * have yet to load + * @type {int} + */ + num_loading; + /** + * If the Doll is navigable, then this holds the index of the PlayerSlot + * in the array this.slots.items of DollSlot objects + * @type {int|boolean} + */ + player_index = false; + /** + * An array of canvas objects on which this Doll has been requested to + * render, but can't yet (say because relies on images which are still + * loading) + * @type {Array<Canvas>} + */ + render_requests; + /** + * Is true if the Doll is currently being drawn in some other thread + * @type {boolean} + */ + rendering; + /** + * Checks for any collisions between DollSlot objects on the current Doll + * that have collision listeners specified. If there are then the + * listeners are called with an event containing info about the two + * DollSlots involved and the Dollhouse on which the Doll will be rendered. + * + * @param {Dollhouse} dollhouse on which the current Doll is to be rendered + */ + checkCollisions(dollhouse) + { + if (this['collision-listener']) { + eval(this['collision-listener']); + } + let items = this.slots.items; + let len = items.length; + if (len == 1) { + return; + } + for (let i = 0; i < len; i++) { + for (let j = i + 1; j < len; j++) { + let s1 = items[i]; + let s2 = items[j]; + if (typeof s1 == 'undefined' || + typeof s2 == 'undefined' || + (!s1['collision-listener'] && !s2['collision-listener'])) { + continue; + } + let rect = intersectRectangles( + [s1.x, s1.y, s1.width, s1.height], + [s2.x, s2.y, s2.width, s2.height]); + if (rect) { + var event = { + 'a' : s1, + 'b' : s2, + 'dollhouse' : dollhouse + }; + if (s1['collision-listener']) { + eval(s1['collision-listener']); + } + if (s2['collision-listener']) { + eval(s2['collision-listener']); + } + } + } + } + } + /** + * Returns the DollSlot used to manage the player's coordinates and + * icon image within the current Doll (provided the Doll is navigable). + * If the player DollSlot doesn't exist an attempt is made to create it. + * If a Dollhouse is provided this is used in the attempt to create the + * player slot. + * + * @param {DollHouse?} dollhouse that may be used to help create the player + * DollSlot + * @return {DollSlot} the player DollSlot + */ + getPlayerSlot(dollhouse = null) + { + let player_slot; + let original_dh = dollhouse; + dollhouse ??= {'player-width': 50, 'player-height': 50, + 'player-x': -1, 'player-x' : -1}; + if (this.player_index === false) { + player_slot = new DollSlot(); + player_slot.type = 'DollSlot'; + this.setPlayerSlot(player_slot); + } + player_slot = this.slots.items[this.player_index]; + if (dollhouse) { + parseObjectsInts([[dollhouse, ['player-width', 'player-height', + 'player-x', 'player-x']]]); + } + player_slot.width ??= dollhouse['player-width']; + player_slot.height ??= dollhouse['player-height']; + player_slot.x ??= (dollhouse['player-x'] >= 0) ? dollhouse['player-x'] - + Math.floor(player_slot.width/2) : + Math.floor((this.width - player_slot.width)/2); + player_slot.y ??= (dollhouse['player-y'] >= 0) ? + dollhouse['player-y'] - Math.floor(player_slot.height/2) : + Math.floor((this.height - player_slot.height)/2); + if (typeof mc().icon !== 'undefined') { + player_slot.icon = mc().icon; + } else { + player_slot.color = (dollhouse['player-color']) ? + dollhouse['player-color'] : 'blue'; + } + if (original_dh) { + player_slot.init(this); + } + this.setPlayerSlot(player_slot); + return player_slot; + } + /** + * Used to set up the Doll. This involves loading all images related to + * doll, and initializing the has_click_listeners, has_collision_listeners, + * has_update_listeners booleant + */ + init() + { + this.num_loading = 0; + this.render_requests = []; + this.rendering = false; + if (this.icon) { + this.image = this.loadImage(this.icon); + } + this.slots.init(this); + let listener_has_map = { + 'click-listener' : 'has_click_listeners', + 'collision-listener' : 'has_collision_listeners', + 'update-listener' : 'has_update_listeners' + }; + for (const listener in listener_has_map) { + let has_listener = listener_has_map[listener]; + if (this[listener] || this.slots[has_listener]) { + this[has_listener] = true; + } + } + } + /** + * Initiates loading of the image at URL into an Image object and returns + * this object. Adds one to num_loading property of this Doll, + * so know Doll is tracking one more image to be loaded. + * Sets up a callback such that if all the images that have been requested + * for this Doll have loaded, and there have been requests to render + * this doll, then the Doll is rendered to each canvas requested. + * + * @param {string} URL of image to load + * @return {HTMLImageElement} image to be loaded + */ + loadImage(url) + { + let image = new Image(); + image.src = url; + this.num_loading++; + image.onload = () => { + this.num_loading--; + if (this.num_loading <= 0) { + this.num_loading = 0; + } else { + return; + } + if (this.render_requests.length > 0 && !this.rendering) { + this.rendering = true; + for (const canvas of this.render_requests) { + this.render(canvas); + } + this.render_requests = []; + this.rendering = false; + } + } + return image; + } + /** + * If the passed slot is a DollSlot adds it to slots and returns true; + * otherwise, does nothing and returns false + * @param {DollSlot} slot to insert + * @return {boolean} whether the item was added or not + */ + push(slot) + { + return this.slots.push(slot); + } + /** + * Draws the current Doll to the provided canvas view. Note given the + * Doll's coordinate system only some portion of the Doll may be + * visible. If not all of the images for this Doll have been downloaded + * a render request is made instead. + * + * @param {HTMLCanvasElement} canvas to draw Doll onto + */ + render(canvas) + { + if (!canvas) { + return; + } + if (this.num_loading > 0) { + this.render_requests.push(canvas); + return; + } + const context = canvas.getContext("2d"); + let source_x = parseInt(canvas.getAttribute('data-source-x')); + let source_y = parseInt(canvas.getAttribute('data-source-y')); + let source_width = parseInt(canvas.getAttribute('data-source-width')); + let source_height = parseInt(canvas.getAttribute('data-source-height')); + let view_width = parseInt(canvas.getAttribute('width')); + let view_height = parseInt(canvas.getAttribute('height')); + if (is_mobile) { + let max_mobile_width = window.screen.width - 40; + let max_mobile_height = view_height * max_mobile_width / view_width; + if (view_width > max_mobile_width) { + view_width = max_mobile_width; + view_height = max_mobile_height; + canvas.width = view_width; + canvas.height = view_height; + } + } + let color = this.color ?? 'white'; + context.clearRect(0, 0, view_width, view_height); + let scale_x = view_width/source_width; + let scale_y = view_height/source_height; + if (this.image) { + let rect = intersectRectangles( + [source_x, source_y, source_width, source_height], + [0, 0, source_width, source_height]); + let coords = imageCoordinates(rect[0], rect[1], + rect[2], rect[3], this); + let dest_rect = [Math.floor(scale_x * (rect[0] - source_x)), + Math.floor(scale_y * (rect[1] - source_y)), + Math.floor(scale_x * rect[2]), + Math.floor(scale_y * rect[3]) + ]; + context.drawImage(this.image, coords[0], coords[1], coords[2], + coords[3], dest_rect[0], dest_rect[1], dest_rect[2], + dest_rect[3]); + } else if (this.color) { + context.fillStyle = this.color; + context.fillRect(0, 0, view_width, view_height); + } + let doll_slots = this.slots.items; + if (!doll_slots) { + return; + } + for (const doll_slot of doll_slots) { + if (typeof doll_slot == 'undefined') { + continue; + } + let rect = intersectRectangles( + [source_x, source_y, source_width, source_height], + [doll_slot.x, doll_slot.y, doll_slot.width, doll_slot.height]); + if (rect) { + let dest_rect = [Math.floor(scale_x * (rect[0] - source_x)), + Math.floor(scale_y * (rect[1] - source_y)), + Math.floor(scale_x * rect[2]), + Math.floor(scale_y * rect[3]) + ]; + if (doll_slot.image) { + let coords = imageCoordinates(rect[0], rect[1], + rect[2], rect[3], doll_slot, true); + context.drawImage(doll_slot.image, + coords[0], coords[1], + coords[2], coords[3], dest_rect[0], dest_rect[1], + dest_rect[2], dest_rect[3]); + } else { + context.fillStyle = doll_slot.color; + context.fillRect(dest_rect[0], dest_rect[1], + dest_rect[2], dest_rect[3]); + } + } + } + } + /** + * Sets the DollSlot object that corresponds to the person playing the + * game to player_slot. This object holds player info when a Doll is + * navigable + * + * @param {DollSlot} player_slot a doll slot with player's rectangle info + * on the doll, and icon or color to draw for player + */ + setPlayerSlot(player_slot) + { + if (this.player_index === false) { + this.player_index = this.push(player_slot); + } else { + this.slots.items[this.player_index] = player_slot; + } + } +} + +/** + * Encapsulates the image slots a Doll might have + */ +class DollSlots +{ + /** + * Whether any of the DollSlot object managed by this DollSlots has + * a click listener. Such a click listener is called if a the + * canvas used to draw a Doll is clicked and that click is within the + * rectangle of the listening DollSlot. + * @type {boolean} + */ + has_click_listeners = false; + /** + * Whether any of the DollSlot object managed by this DollSlots has + * a collision listener. Such a listener is called if the rectangle + * of a listening DollSlot intersects with another DollSlot in this + * DollSlots. + * @type {boolean} + */ + has_collision_listeners = false; + /** + * Whether any of the DollSlot object managed by this DollSlots has + * an update listener a timer is used to call such listeners periodically + * @type {boolean} + */ + has_update_listeners = false; + /** + * Array of DollSlot objects managed by this DollSlots. + * @type {Array} + */ + items = []; + /** + * If not null, Doll on which this DollSlot lives. + * @type {Doll?} + */ + parent; + /** + * Returns the DollSlot at index i if it exists else return null + * @param {int} DollSlot index to get + * @return {DollSlot?} the desired DollSlot if present + */ + get(i) + { + if (typeof this.items[i] != 'undefined') { + return this.items[i]; + } + return null; + } + /** + * Calls init on each of the DollSlots in items. Initializes the + * has_click_listeners, has_collision_listeners, and has_update_listeners + * property based on checking for the corresponding listeners in + * each DollSlot iterated over. + * + * @param {Doll?} parent Doll used to help initialize each Doll (used + * for Image loading (Doll needs to keep track of how many of its + * DollSlots still need to be loaded)) + */ + init(parent) + { + let listener_has_map = { + 'click-listener' : 'has_click_listeners', + 'collision-listener' : 'has_collision_listeners', + 'update-listener' : 'has_update_listeners' + }; + for (const doll_slot of this.items) { + if (doll_slot.type == 'DollSlot') { + doll_slot.init(parent); + for (const listener in listener_has_map) { + let has_listener = listener_has_map[listener]; + if (doll_slot[listener]) { + this[has_listener] = true; + } + } + } + } + } + /** + * If the passed slot is a DollSlot adds it to items Array at index i + * and return true; otherwise, does nothing and returns false + * @param {int} i index in items array to insert DollSlot + * @param {DollSlot} slot to insert + * @return {boolean} whether the item was added or not + */ + set(i, slot) + { + if (typeof slot['type'] != 'undefined' && slot['type'] == 'DollSlot') { + this.items[i] = slot; + return i; + } else { + return false; + } + } + /** + * If the passed slot is a DollSlot adds it to items Array and returns true; + * otherwise, does nothing and returns false + * @param {DollSlot} slot to insert + * @return {boolean} whether the item was added or not + */ + push(slot) + { + return this.set(this.items.length, slot); + } + /** + * Removes and delete the DollSlot at location index from this DollSlots + * items array of DollSlot objects + * @param {int} index of DollSlot to remove + */ + remove(index) + { + if (typeof this.items[i] != 'undefined') { + delete this.items[index]; + return true; + } + return false; + } + /** + * Removes the DollSlot of the given slot_id name from this DollSlots object + * + * @param {string} slot_id id property of the DollSlot to remove + * @return {boolean} true if a DollSlot with the given id is found and + * removed, false otherwise + */ + removeById(slot_id) + { + for (const i = 0; i < this.items.length; i++) { + if (this.items[i] && this.items[i].id == slot_id) { + delete this.items[i]; + return true; + } + } + return false; + } +} + +/** + * Encapsulates a single image slot for use in a Doll + */ +class DollSlot +{ + /** + * The filename or object to be used by this slot in a paper Doll + * @type {} + */ + icon = ""; + /** + * The image to be used by this slot in a paper Doll + * @type {Image} + */ + image = null; + /** + * The height of the slot in a paper Doll + * @type {int} + */ + height; + /** + * The width of the slot in a paper Doll + * @type {int} + */ + width; + /** + * The x coordinate within the overall Doll for this DollSlot + * @type {int} + */ + x; + /** + * The y coordinate within the overall Doll for this DollSlot + * @type {int} + */ + y; + /** + * Initializes this DollSlot within its parent Doll, parent_doll. + * This involves loading any Image associated with this DollSlot + * or discarding any image, if the DollSlot is going to be presented + * as a solid color. + */ + init(parent_doll) + { + if (this.icon) { + this.image = parent_doll.loadImage(this.icon); + } else if (this.image) { + delete this.image; + } + } +} +/** + * Class used to encapsulate an interactive story game. It has fields + * to track the locations and objects in the game, the history of moves of + * the game, and how many moves have been made. It has methods to + * take a turn in such a game, to save state, load state, + * restore prev/next state from history, render the state of such + * a game. + */ +class Game +{ + /** + * If there are any dollhouses currently being drawn which are updateable + * this gives the default refresh rate in milliseconds + * @type {int} + */ + dollhouse_refresh_rate = 50; + /** + * ids of keyboard and setTimeouts that are currently updating + * dollhouses (if any) in the the location being presented + * @type {Array<int>} + */ + dollhouse_update_ids = []; + /** + * A semi-unique identifier for this particular game to try to ensure + * two different games hosted in the same folder don't collide in + * sessionStorage. + * @type {string} + */ + id; + /** + * Whether game page was just reloaded + * @type {boolean} + */ + reload; + /** + * Current date followed by a space followed by the current time of + * the most recent game capture. Used in providing a description of + * game saves. + * @type {number} + */ + timestamp; + /** + * A counter that is incremented each time Javascript draws a new + * clickProceed a tag. Each such tag is given an id, tick is used to ensure + * these id's are unique. + * @type {number} + */ + tick = 0; + /** + * Whether this particular game has a nav bar or not + * @type {boolean} + */ + has_nav_bar; + /** + * List of all Game Object's managed by the FRISE script. An object + * can be used to represent a thing such as a person, tool, piece of + * clothing, letter, etc. In an HTML document, a game object is defined + * using an x-object tag. + * @type {Array<Object>} + */ + objects; + /** + * List of all game Location's managed by the FRISE script. A Location + * can be used to represent a place the main character can go. This + * could be standard locations in the game, as well as Locations + * like a Save page, Inventory page, Status page, etc. + * In an HTML document a game Location is defined using an x-location tag. + * @type {Array<Location>} + */ + locations; + /** + * + * + * @type {Array<Doll>} + */ + dolls; + /** + * Used to maintain a stack (using Array push/pop) of Game State Objects + * based on the turns the user has taken (the top of the stack corresponds + * to the previous turn). A Game State Object is a serialized string: + * { + * timestamp: capture_time, + * objects: array_of_game_objects_at_capture_time, + * locations: array_of_game_locations_at_capture_time, + * } + * @type {Array} + */ + history; + /** + * Used to maintain a stack (using Array push/pop) of Game State Objects + * based on the the number previous turn clicks the user has done. + * I.e., when a user clicks previous turn, the current state is pushed onto + * this array so that if the user then clicks next turn the current + * state can be restored. + * A Game State Object is a serialized string: + * { + * timestamp: capture_time, + * objects: array_of_game_objects_at_capture_time, + * locations: array_of_game_locations_at_capture_time, + * } + * @type {Array} + */ + future_history; + /** + * Id of first room main-character is in; + * @type {String} + */ + base_location; + /** + * Is set to true just before a default action for a location the + * main character is at is executed; otherwise, false + * @type {Boolean} + */ + is_here; + /** + * List of id's of buttons and links to disable during the staging + * phases of rendering a presentation or when viewing those locations. + * @type {Array} + */ + volatile_links = ['saves', 'inventory']; + /** + * Sets up a game object with empty history, an initialized main navigation, + * and with objects and locations parsed out of the current HTML file + */ + constructor() + { + let title_elt = tag('title')[0]; + if (!title_elt) { + title_elt = tag('x-game')[0]; + } + this.reload = false; //current + let doc_length = 0; + let middle_five = ""; + if (title_elt) { + doc_length = title_elt.innerHTML.length; + if (doc_length > 8) { + let half_length = Math.floor(doc_length/2); + middle_five = title_elt.innerHTML.slice( + half_length, half_length + 5); + } + } + // a semi-unique code for this particular game + this.id = encodeURI(middle_five + doc_length); + this.initializeMainNavGameContentArea(); + this.initializeObjectsLocationsDolls(); + this.clearHistory(); + } + /** + * Writes to console information about which objects and locations + * might not be properly defined. + */ + debug() + { + let none = "none"; + console.log("Game objects without position:"); + for (let obj of Object.values(this.objects)) { + if (!obj.has_position) { + console.log(" " + obj.id); + none = ""; + } + } + if (none) { + console.log(" " + none); + } + none = "none"; + console.log("Game locations without x-present:"); + for (loc of Object.values(this.locations)) { + if (!loc.has_present) { + console.log(" " +loc.id); + none = ""; + } + } + none = "none"; + console.log("Game dolls missing required attributes:"); + for (const doll_obj of Object.values(this.dolls)) { + if (!doll_obj.has_icon && !doll_obj.has_color) { + console.log(" " + doll_obj.icon + " has no icon or color"); + none = ""; + } + if (!doll.has_height) { + console.log(" " + doll_obj.height + " has no height"); + none = ""; + } + if (!doll.has_width) { + console.log(" " + doll_obj.width + " has no width"); + none = ""; + } + } + if (none) { + console.log(" " + none); + } + return true; + } + /** + * Used to reset the game to the condition at the start of a game + */ + reset() + { + sessionStorage.removeItem("current" + this.id); + this.initializeMainNavGameContentArea(); + this.initializeObjectsLocationsDolls(); + this.clearHistory(); + } + /** + * Sets up the main navigation bar and menu on the side of the screen + * determined by the is_right_to_left variable. Sets up an initially empty + * game content area which can be written to by calling a Location + * object's renderPresentation. The main navigation consists of a hamburger + * menu toggle button for the navigation as well as previous + * and next history arrows at the top of screen. The rest of the main + * navigation content is determined by the contents of the x-main-nav + * tag in the HTML file for the game. If this tag is not present, the + * game will not have a main navigation bar and menu. + */ + initializeMainNavGameContentArea() + { + let body_objs = tag("body"); + if (body_objs[0] === undefined) { + return; + } + let body_obj = body_objs[0]; + let game_screen = elt('game-screen'); + if (!game_screen) { + body_obj.innerHTML = '<div id="game-screen"></div>' + + body_obj.innerHTML; + game_screen = elt('game-screen'); + } + let main_nav_objs = tag("x-main-nav"); + if (typeof main_nav_objs[0] === "undefined") { + game_screen.innerHTML = `<div id="game-content" tabindex="0"> + </div>`; + this.has_nav_bar = false; + return; + } + this.has_nav_bar = true; + let main_nav_obj = main_nav_objs[0]; + let history_buttons; + if (is_right_to_left) { + history_buttons = + `<button id="previous-history">→</button> + <button id="next-history">←</button>`; + } else { + history_buttons = + `<button id="previous-history">←</button> + <button id="next-history">→</button>`; + } + game_screen.innerHTML = ` + <div id="main-bar"> + <button id="main-toggle" + class="float-left"><span class="main-close">≡</button> + </div> + <div id="main-nav"> + ${history_buttons} + <div id="game-nav"> + ${main_nav_obj.innerHTML} + </div> + </div> + <div id="game-content"></div>`; + this.initializeGameNavListeners(); + } + /** + * Used to initialize the event listeners for the next/previous history + * buttons. It also adds listeners to all the a tag and x-button tags + * to process their href attributes before following any link is followed + * to its target. + * @see addListenersAnchors + */ + initializeGameNavListeners() + { + elt('main-toggle').onclick = (evt) => { + toggleMainNav('main-nav', 0); + }; + elt('next-history').onclick = (evt) => { + this.nextHistory(); + } + elt('previous-history').onclick = (evt) => { + this.previousHistory(); + } + let anchors = sel('#game-nav a, #game-nav x-button'); + addListenersAnchors(anchors, is_mobile); + } + /** + * Checks if the game is being played on a mobile device. If not, this + * method does nothing, If it is being played on a mobile device, + * then this method sets up the viewport so that the HTML will + * display properly. Also, in the case where the game is being played on a + * mobile device, this method also sets it so the main nav bar on the side + * of the screen is closed. + */ + initializeScreen() + { + let html = tag("html")[0]; + if (is_right_to_left) { + html.classList.add("rtl"); + } + if (!this.has_nav_bar) { + html.classList.add("no-nav"); + } + let is_slides = sel("x-slides")[0] ? true : false; + if (is_slides) { + html.classList.add("slides"); + } + if(!is_mobile) { + return; + } + html.classList.add("mobile"); + let head = tag("head")[0]; + head.innerHTML += `<meta name="viewport" `+ + `content="width=device-width, initial-scale=1.0" >`; + if (this.has_nav_bar) { + toggleMainNav(); + } + } + /** + * For each object, if object.position is defined, then adds the object + * to the location.item array of the Location whose id is + * given by object.position. Sets up the dolls Array of paperdolls + * used in the game (so their images start loading). + */ + initializeObjectsLocationsDolls() + { + this.objects = xtag("x-object"); + this.locations = xtag("x-location"); + this.dolls = xtag('x-doll'); + for (const oid in this.objects) { + let object = this.objects[oid]; + if (object.hasOwnProperty("position")) { + let location_name = object.position; + if (this.locations.hasOwnProperty(location_name)) { + let location = this.locations[location_name] + if (!location.hasOwnProperty("items")) { + location.items = []; + } + location.items.push(object.id); + } + if (typeof object.original_position == 'undefined') { + object.original_position = object.position; + } + } + } + for (const lid in this.locations) { + let location = this.locations[lid]; + if (!location.hasOwnProperty("items")) { + location.items = []; + } + if (typeof location.original_items == 'undefined') { + location.original_items = location.items; + } + } + } + /** + * Creates a JSON encoded string representing the current state of + * the game (all of the object and location states and where the main + * character is). + * + * @return {string} JSON encoded current state of game. + */ + captureState() + { + let now = new Date(); + let date = now.getFullYear() + '-' + (now.getMonth() + 1) + + '-' + now.getDate(); + let time = now.getHours() + ":" + now.getMinutes() + ":" + + now.getSeconds(); + game.timestamp = date + " " + time; + return JSON.stringify({ + timestamp: game.timestamp, + base_location: game.base_location, + objects: this.objects, + locations: this.locations + }); + } + /** + * Sets the current state of the game (current settings for all objects, + * locations, and main character position), based on the state given in + * a JSON encode string representing a game state. + * + * @param {string} gave_save a JSON encoded state of the a FRISE game. + */ + restoreState(game_save) + { + let game_state = JSON.parse(game_save); + if (!game_state || !game_state.timestamp || + !game_state.objects || !game_state.locations) { + alert(tl[locale]['restore_state_invalid_game']); + return false; + } + this.timestamp = game_state.timestamp; + this.base_location = game_state.base_location; + /* + during development, changing an object or location's text might + not be viewable on a reload unless we copy some fields of the + reparsed html file into a save game object. + */ + let old_objects = this.objects; + this.objects = game_state.objects; + for (const field in old_objects) { + if (!this.objects.hasOwnProperty(field)) { + /* we assume our game never deletes objects or locations, so + if we find an object in old_objects (presumably it's coming + from a more recently parsed HTML file) that was not + in the saved state, we copy it over. + */ + this.objects[field] = old_objects[field]; + } else { + if (old_objects.hasOwnProperty('action')) { + this.objects['action'] = old_objects['action']; + } else if (this.objects.hasOwnProperty('action')) { + delete this.objects['action']; + } + } + } + let old_locations = this.locations; + let locations = game_state.locations; + let location; + this.locations = {}; + for (const location_name in old_locations) { + if (!locations.hasOwnProperty(location_name)) { + location = old_locations[location_name]; + } else { + let location_object = locations[location_name]; + location = new Location(); + for (const field in location_object) { + location[field] = location_object[field]; + if (field == 'present' || field == 'action' || + field == 'default-action') { + if (!old_locations[ + location_name].hasOwnProperty(field)) { + delete location[field]; + } else { + location[field] = + old_locations[location_name][field]; + } + } + } + } + this.locations[location_name] = location; + } + return true; + } + /** + * Deletes the game state capture history for the game. After this + * calling this method, the game's next and previous arrow buttons + * won't do anything until new turns have occurred. + */ + clearHistory() + { + this.history = []; + this.future_history = []; + let next_history_elt = elt('next-history'); + if (next_history_elt) { + next_history_elt.disabled = true; + elt('previous-history').disabled = true; + } + } + /** + * Called when the left arrow button on the main nav page is + * clicked to go back one turn in the game history. Pushes the current + * game state to the future_history game state array, then pops the most + * recent game state from the history game state array and sets it as + * the current state. + */ + previousHistory() + { + if (this.history.length == 0) { + return; + } + let current_state = this.captureState(); + this.future_history.push(current_state); + let previous_game_state = this.history.pop(); + this.restoreState(previous_game_state); + sessionStorage["current" + this.id] = previous_game_state; + this.describeMainCharacterLocation(); + if (this.history.length == 0) { + elt('previous-history').disabled = true; + } else { + elt('previous-history').disabled = false; + } + elt('next-history').disabled = false; + } + /** + * Called when the right arrow button on the main nav page is + * clicked to go forward one turn in the game history (assuming the user had + * clicked previous at least once). Pushes the current game state + * to the history game state array, then pops the game state from the + * future_history game state array and sets it as the current state. + */ + nextHistory() + { + if (this.future_history.length == 0) { + return; + } + let current_state = this.captureState(); + this.history.push(current_state); + let next_game_state = this.future_history.pop(); + this.restoreState(next_game_state); + sessionStorage["current" + this.id] = next_game_state; + this.describeMainCharacterLocation(); + if (this.future_history.length == 0) { + elt('next-history').disabled = true; + } else { + elt('next-history').disabled = false; + } + elt('previous-history').disabled = false; + } + /** + * Initializes the save slots for the saves location page of a game. + * This involves looking at session storage and determining which slots + * have games already saved to them, and for those slots, determining also + * what time the game was saved. + */ + initSlotStates() + { + let saves_location = game.locations['saves']; + for (const field in saves_location) { + let slot_matches = field.match(/^slot(\d+)/); + if (slot_matches && slot_matches[1]) { + let slot_number = parseInt(slot_matches[1]); + let game_save = localStorage.getItem("slot" + game.id + + slot_number); + if (game_save) { + let game_state = JSON.parse(game_save); + saves_location["slot" + slot_number] = + tl[locale]['init_slot_states_load']; + saves_location["delete" + slot_number] = ""; + saves_location["filled" + slot_number] = 'filled'; + saves_location["filename" + slot_number] = + game_state.timestamp; + } else { + saves_location["slot" + slot_number] = + tl[locale]['init_slot_states_save']; + saves_location["filled" + slot_number] = 'not-filled'; + saves_location["delete" + slot_number] = "disabled"; + saves_location["filename" + slot_number] = '...'; + } + } + } + } + /** + * Saves the current game state to a localStorage save slot if that + * slot if empty; otherwise, if the slot has data in it, then sets + * the current game state to the state stored at that slot. When saving, + * this method also records the timestamp of the save time to the + * game's saves location. + * + * @param {number} slot_number + */ + saveLoadSlot(slot_number) + { + slot_number = parseInt(slot_number); + let saves_location = game.locations['saves']; + let game_state = localStorage.getItem("slot" + game.id + slot_number); + if (game_state) { + this.clearHistory(); + sessionStorage["current" + game.id] = game_state; + this.restoreState(game_state); + } else { + let save_state = this.captureState(); + game_state = this.history[this.history.length - 1]; + this.restoreState(game_state); + saves_location['filename' + slot_number] = this.timestamp; + localStorage.setItem("slot" + game.id + slot_number, game_state); + this.restoreState(save_state); + this.evaluateAction(saves_location['default-action']); + } + } + /** + * Deletes any game data from localStorage at location + * "slot" + slot_number. + * + * @param {number} slot_number which save game to delete. Games are stored + * at a localStorage field "slot" + slot_number where it is intended + * (but not enforced) that the slot_number be an integer. + */ + deleteSlotData(slot_number) + { + slot_number = parseInt(slot_number); + let saves_location = game.locations['saves']; + localStorage.removeItem("slot" + game.id + slot_number); + saves_location['filled' + slot_number] = "not-filled"; + saves_location['delete' + slot_number] = "disabled"; + saves_location['slot' + slot_number] = + tl[locale]['init_slot_states_save']; + saves_location['filename' + slot_number] = "..."; + } + /** + * Deletes any game data from localStorage at location + * "slot" + slot_number, updates the game's saves location to reflect the + * change. + * + * @param {number} slot_number which save game to delete. Games are stored + * at a localStorage field "slot" + slot_number where it is intended + * (but not enforced) that the slot_number be an integer. + */ + deleteSlot(slot_number) + { + this.deleteSlotData(slot_number); + let saves_location = game.locations['saves']; + this.evaluateAction(saves_location['default-action']); + } + /** + * Deletes all game saves from sessionStorage + */ + deleteSlotAll() + { + let i = 0; + let saves_location = game.locations['saves']; + while (saves_location.hasOwnProperty('filename' + i)) { + this.deleteSlotData(i); + i++; + } + this.evaluateAction(saves_location['default-action']); + } + /** + * Launches a file picker to allow the user to select a file + * containing a saved game state, then tries to load the current game + * from this file. + */ + load() + { + let file_load = elt('file-load'); + if (!file_load) { + file_load = document.createElement("input"); + file_load.type = "file"; + file_load.id = 'file-load'; + file_load.style.display = 'none'; + file_load.addEventListener('change', (event) => { + let to_read = file_load.files[0]; + let file_reader = new FileReader(); + file_reader.readAsText(to_read, 'UTF-8') + file_reader.addEventListener('load', (load_event) => { + let game_state = load_event.target.result; + this.clearHistory(); + sessionStorage["current" + this.id] = game_state; + this.restoreState(game_state); + game.describeMainCharacterLocation(); + }); + }); + } + file_load.click(); + } + /** + * Creates a downloadable save file for the current game state. + */ + save() + { + let game_state = this.history[this.history.length - 1]; + let file = new Blob([game_state], {type: "plain/text"}); + let link = document.createElement("a"); + link.href = URL.createObjectURL(file); + link.download = "game_save.txt"; + link.click(); + } + /** + * Computes one turn of the current game based on the provided url hash + * fragment. A url hash fragment is the part of the url after a # symbol. + * In non-game HTML, #fragment is traditionally used to indicate the browser + * should show the page as if it had been scrolled to where the element + * with id attribute fragment is. In a FRISE game, a fragment has + * the form #action_1_name;action_2_name;...;action_n_name;next_location_id + * Such a fragment when processed by takeTurn will cause the Javascript in + * x-action tags with id's action_1_name, action_2_name,...,action_n_name + * to be invoked in turn. Then the main-character object is moved to + * location next_location_id. If the fragment, only consists of + * 1 item, i.e., is of the form, #next_location_id, then this method + * just moves the main-character to next_location_id. + * After carrying out the action and moving the main-character, + * takeTurn updates the game state history and future_history + * accordingly. Then for each object and each location, + * if the object/location, has an x-default-action tag, this default action + * is executed. Finally, the Location of the main-character is presented + * (its renderPresentation is called). + * takeTurn supports two special case action #previous and #next + * which move one step back or forward (if possible) in the Game state + * history. + * @param {string} hash url fragment ot use when computing one turn of the + * current game. + */ + takeTurn(hash) + { + let new_game_state; + let is_slides = sel("x-slides")[0] ? true : false; + if (!is_slides) { + if (this.has_nav_bar) { + if (hash == "#previous") { + this.previousHistory(); + return; + } else if (hash == "#next") { + this.nextHistory(); + return; + } + } + if (sessionStorage["current" + game.id]) { + new_game_state = sessionStorage["current" + game.id]; + } + } + if (!this.moveMainCharacter(hash)) { + return; + } + if (!is_slides) { + this.future_history = []; + if (this.has_nav_bar) { + elt('next-history').disabled = true; + } + if (sessionStorage["current" + game.id]) { + this.history.push(new_game_state); + } + this.evaluateDefaultActions(this.objects); + this.evaluateDefaultActions(this.locations); + sessionStorage["current" + game.id] = this.captureState(); + } + this.describeMainCharacterLocation(); + if (!is_slides) { + game.reload = false; + if (this.has_nav_bar) { + if (this.history.length == 0) { + elt('previous-history').disabled = true; + } else { + elt('previous-history').disabled = false; + } + } + } + } + /** + * For each game Object and each game Location in x_entities evaluate the + * Javascript (if it exists) of its default action (from its + * x-default-action tag). + * + * @param {Array} of game Object's or Location's + */ + evaluateDefaultActions(x_entities) + { + for (const object_name in x_entities) { + let game_entity = x_entities[object_name]; + if (mc().position == object_name && game_entity + instanceof Location) { + game['is_here'] = true; + } else { + game['is_here'] = false; + } + if (game_entity && game_entity['default-action']) { + this.evaluateAction(game_entity['default-action']); + } + } + } + /** + * Moves a game Object to a new game Location. If the object had a + * previous location, then also deletes the object from there. + * + * @param {string} object_id of game Object to move + * @param {string} destination_id of game Location to move it to + */ + moveObject(object_id, destination_id) + { + let move_object = this.objects[object_id]; + if (!move_object || !this.locations[destination_id]) { + alert(tl[locale]['move_object_failed'] + + "\nmoveObject('" + object_id + "', '" + destination_id + "')"); + return false; + } + if (move_object.hasOwnProperty("position")) { + let old_position = move_object.position; + let old_location = this.locations[old_position]; + old_location.items = old_location.items.filter((value) => { + return value != object_id; + }); + } + move_object.position = destination_id; + let new_location = this.locations[destination_id]; + if (!new_location.items) { + new_location.items = []; + } + new_location.items.push(object_id); + return true; + } + /** + * Moves the main character according to the provided url fragment. + * + * @param {string} hash a url fragment as described above + */ + moveMainCharacter(hash) + { + if (!hash || hash <= 1) { + return true; + } + hash = hash.substring(1); + let hash_parts = hash.split(/\s*\;\s*/); + let destination = hash_parts.pop(); + for (const hash_part of hash_parts) { + let hash_matches = hash_part.match(/([^\(]+)(\(([^\)]+)\))?\s*/); + let action = elt(hash_matches[1]); + let args = []; + if (typeof hash_matches[3] !== 'undefined') { + args = hash_matches[3].split(","); + } + if (action && action.tagName == 'X-ACTION' + || (action.tagName == 'SCRIPT' && + action.getAttribute('type') == 'text/action')) { + let code = action.innerHTML; + if (code) { + this.evaluateAction(code, args); + } + } + } + if (destination == "exit") { + this.describeMainCharacterLocation(); + return false; + } + if (destination == "previous") { + this.previousHistory(); + return false; + } + if (destination == "next") { + this.nextHistory(); + return false; + } + let mc = obj('main-character'); + if (mc.position != mc.destination && mc.position != 'saves') { + mc.old_position = mc.position; + } + if (this.dollhouse_update_ids.length > 0) { + for (const update_id of this.dollhouse_update_ids) { + clearTimeout(update_id); + } + this.dollhouse_update_ids = []; + } + this.moveObject('main-character', destination); + this.locations[mc.position].visited++; + return true; + } + /** + * Given a string holding pre-Javascript code from an x-action tag, + * evaluates the code. If this function is passed additional arguments + * then an args array is set up that can be used as a closure variable for + * this eval call. + * + * @param {string} Javascript code. + */ + evaluateAction(code) + { + var args = []; + if (arguments.length > 1) { + if (arguments[1]) { + args = arguments[1]; + } + } + eval(code); + } + /** + * Used to present the location that the Main Character is currently at. + */ + describeMainCharacterLocation() + { + let main_character = this.objects['main-character']; + let position = main_character.position; + let location = this.locations[position]; + location.renderPresentation(); + } + /** + * Return the array of link ids which should be disable while performing + * the staging of a presentation + * + * @return {Array} + */ + volatileLinks() + { + return this.volatile_links; + } +} +/** + * Module initialization function used to set up the game object corresponding + * to the current HTML document. It first loads any x-included game fragments + * before calling @see finishInitGame to perform the rest of the game + * initialization process + */ +async function initGame() +{ + loadIncludes(finishInitGame); +} +/** + * This module initialization is called to initialize FRISE when it is being + * used to show a slide presentation. It first loads any x-included slideshow + * fragments before calling @see finishInitSlides to perform the rest of the + * game initialization process + */ +async function initSlides() +{ + loadIncludes(finishInitSlides); +} +/** + * Function used to load any x-included game fragments into this game. + * This function is run before the rest of the game's initialization is + * done in @see finishInitGame or finishInitSlides + * + * @param {Function} callback called to complete game initialization after + * the included game fragments are laoded. This is typically either + * finishInitGame or finishInitSlides + */ +async function loadIncludes(callback) +{ + let includes = sel("x-include"); + includes_to_load = includes.length; + if (includes_to_load == 0) { + return callback(); + } + for (const include_node of includes) { + if (include_node.nextSibling.nodeName == 'X-LOADED') { + includes_to_load--; + continue; + } + let url = include_node.textContent.trim(); + fetch(url).then(response => response.text()).then( + data => { + let loaded_node = document.createElement('x-loaded'); + loaded_node.innerHTML = data; + include_node.parentNode.insertBefore(loaded_node, + include_node.nextSibling); + includes_to_load --; + if (includes_to_load <= 0) { + if (window.location.search == "?view-source") { + let encodeEntities = (text) => { + let textArea = document.createElement('textarea'); + textArea.innerText = text; + return textArea.innerHTML; + } + let source = "<!doctype html>\n" + + document.documentElement.outerHTML; + source = encodeEntities(source); + let body = sel('body')[0]; + body.innerHTML = `<pre><code>${source}</pre></code>`; + } else { + callback(); + } + } + } + ); + } + if (includes_to_load <= 0) { + if (window.location.search == "?view-source") { + alert("<!DOCTYPE html>\n" + document.documentElement.outerHTML); + } else { + callback(); + } + } +} +/** + * This function is called after all images for Doll's have + * loaded to finish initializing the Frise Game. To do this, if there is a + * current game state in sessionStorage it is used to initialize the game + * state, otherwise,* the game state is based on the start of the game. After + * this state is set up, the current location is drawn to the game content area. + */ +async function finishInitGame() +{ + game = new Game(); + /* + Any game specific customizations are assumed to be in the function + localInitGame if it exists + */ + if (typeof localInitGame == 'function') { + localInitGame(); + } + let use_session = false; + let is_slides = sel("x-slides")[0] ? true : false; + if (sessionStorage["current" + game.id] && !is_slides) { + use_session = true; + game.restoreState(sessionStorage["current" + game.id]); + game.reload = true; + } else { + game.base_location = game.objects['main-character'].position; + } + if (is_slides) { + window.addEventListener('hashchange', (event) => { + game.takeTurn(window.location.hash); + }) + } + game.takeTurn(""); + game.clearHistory(); + game.initializeScreen(); +} +/** + * If the current game is a slideshow presentation + * this function is called after all images for Doll's have + * loaded to finish initializing the slides for the presentation. This + * sets up the forward, backward, table of conents, all slides on a single + * page, and reset buttons as well as keyboard listeners to go this through + * slides + */ +async function finishInitSlides() +{ + let slides = sel('x-slides')[0]; + let home = sel(`link[rel=prev], meta[name=parent], + meta[property=parent]`)[0] ?? ""; + if (home) { + home = home.getAttribute('content') ?? + home.getAttribute('href'); + if (home) { + home = `<x-button href='${home}'>🏠</x-button>`; + } + } + let slides_in_transition = slides.getAttribute("in-transition"); + let slides_out_transition = slides.getAttribute("out-transition"); + let slides_duration = slides.getAttribute("duration"); + slides_duration = (slides_duration) ? slides_duration : "500"; + let slides_iterations = slides.getAttribute("iteration"); + let slides_origin = slides.getAttribute("origin"); + let slides_children = slides.children; + if (!slides_children) { + return; + } + let slide_titles = sel('x-slides x-slide h1:first-of-type'); + let x_game = sel('x-game')[0]; + if (!x_game) { + x_game = document.createElement('x-game'); + let body = sel('body')[0]; + if (!body) { + return; + } + body.appendChild(x_game); + } + let i = 1; + let num_slides = 0; + let start_slide = ""; + let all_slides = "<x-present><div class='all'>"; + let title_location = `<x-location id="(titles)"><x-present> + <h1>${tl['en']['slide_titles']}</h1><ol style="font-size:130%;">`; + let game_contents = `<x-object id="main-character"> + <x-position>(1)</x-position></x-object>`; + for (const slide of slides_children) { + if (slide.tagName == 'X-SLIDE') { + num_slides++; + } + } + for (const slide_title of slide_titles) { + title_location += `<li><a href='#(${i})' + >${slide_title.textContent}</a></li>`; + i++; + } + let title_slide_footer = ` <span class="big-p">(</span> + <x-button href="#(1)">⟳</x-button> + <x-button class="slides-titles" href="#(1)">-/${num_slides}</x-button> + <x-button href="#(all)">≡</x-button> + <span class="big-p">)</span> `; + title_location += `</ol></x-present><x-screen-footer + ><div class='slide-footer center' + >${title_slide_footer}</div></x-screen-footer></x-location>`; + game_contents += title_location; + i = 1; + let slide_footer, all_slide_footer; + for (const slide of slides_children) { + if (slide.tagName == 'X-SLIDE') { + let in_transition = slide.getAttribute('in-transition'); + in_transition ??= (slides_in_transition ?? ""); + let out_transition = slide.getAttribute('out-transition'); + out_transition ?? (out_transition ?? slides_out_transition); + let all_class_attribute = (slide.className) ? + ` class='slide ${slide.className}' `: ` class='slide' `; + let class_attribute = (slide.className) ? + ` class='slide ${slide.className} ${in_transition}' `: + ` class='slide ${in_transition}' `; + let styles = slide.getAttribute('style'); + let style_attribute = (styles) ? ` style='${styles}' ` : ""; + let duration = slide.getAttribute('duration'); + duration = (duration) ? duration : slides_duration; + let iterations = slide.getAttribute('iterations'); + iterations = (iterations) ? iterations : slides_iterations; + let origin = slide.getAttribute('origin'); + origin = (origin) ? origin : slides_origin; + if (out_transition) { + out_transition += ` data-transition="${out_transition}" `; + } else { + out_transition = ""; + } + if (duration) { + out_transition += ` data-duration="${duration}" `; + } + if (iterations) { + out_transition += ` data-iterations="${iterations}" `; + } + if (origin) { + out_transition += ` data-transform-origin="${origin}" `; + } + let slide_contents = `<div ${class_attribute} ${style_attribute}> + ${slide.innerHTML}</div>`; + let current_slide = `<x-present>${slide_contents}`; + let all_slide_contents = `<div ${all_class_attribute} + ${style_attribute}>${slide.innerHTML}</div>`; + if (i > 1) { + slide_footer = `<x-button class="slides-previous" + href="#(${i - 1})" ${out_transition}>←</x-button>`; + } else { + slide_footer = `<x-button class="hidden">←</x-button>` + } + slide_footer += ` <span class="big-p">(</span> + <x-button class="slides-restart" href="#(1)">⟳</x-button> + ${home} <x-button class="slides-titles" href="#(titles)" + >${i}/${num_slides}</x-button> + <x-button class="all-slides" href="#(all)">≡</x-button> + <span class="big-p">)</span>`; + if (i < num_slides) { + slide_footer += `<x-button class="slides-next" + href="#(${i + 1})" ${out_transition}>→</x-button>` + } else { + slide_footer += `<x-button class="hidden">→</x-button>` + } + all_slides += all_slide_contents; + if (i < num_slides) { + all_slides +=`<div class="page-break"><hr></div>`; + } + current_slide += `</x-present>`; + game_contents += `<x-location id="(${i})"> + ${current_slide}<x-screen-footer> + <div class='slide-footer center' + >${slide_footer}</div></x-screen-footer> + </x-location>`; + i++; + } + } + all_slide_footer = `<div class='slide-footer center'> + <span class="big-p">(</span> + <x-button class="slides-restart" href="#(1)">⟳</x-button> + <x-button class="slides-titles" href="#(titles)" + >-/${num_slides}</x-button> + <x-button class="all-slides" href="#(1)">≡</x-button> + <span class="big-p">)</span></div>`; + all_slides += `</div></x-present>`; + game_contents += `<x-location id="(all)"> + ${all_slides}<x-screen-footer> + <div class='slide-footer center' + >${all_slide_footer}</div></x-screen-footer> + </x-location>`; + x_game.innerHTML = game_contents; + let body = sel('body')[0]; + let start_x = 0; + let start_swipe_ck = (e) => { + start_x = (e.changedTouches ? e.changedTouches[0] : e).clientX; + }; + let end_swipe_ck = (e) => { + let dx = (e.changedTouches ? e.changedTouches[0] : e).clientX - + start_x; + if (dx > 30) { + let previous = sel('#screen-footer .slides-previous')[0]; + if (previous) { + previous.click(); + } + } else if (dx < -30 ) { + let next = sel('#screen-footer .slides-next')[0]; + if (next) { + next.click(); + } + } + } + body.addEventListener('mousedown', start_swipe_ck, false); + body.addEventListener('touchstart', start_swipe_ck, false); + body.addEventListener('mouseup', end_swipe_ck, false); + body.addEventListener('touchend', end_swipe_ck, false); + body.addEventListener('keydown', (event) => { + let hash = window.location.hash ?? 1; + let page_num = parseInt(hash.substring(2, hash.length - 1)); + page_num = (isNaN(page_num)) ? 1 : page_num; + if (['ArrowLeft'].includes(event.code) && page_num > 1) { + let previous = sel('#screen-footer .slides-previous')[0]; + if (previous) { + previous.click(); + } + } else if (['Space', 'ArrowRight', 'Enter'].includes(event.code) + && page_num < num_slides) { + let next = sel('#screen-footer .slides-next')[0]; + if (next) { + next.click(); + } + } else if (['KeyT'].includes(event.code)) { + let titles = sel('#screen-footer .slides-titles')[0]; + if (titles) { + titles.click(); + } + } else if (['KeyF', 'KeyH'].includes(event.code)) { + let slide_footer = sel('#game-screen .slide-footer')[0]; + if (slide_footer.style.display != 'none') { + slide_footer.style.display = 'none'; + sessionStorage['show_footer'] = false; + } else { + slide_footer.style.display = 'block'; + sessionStorage['show_footer'] = true; + } + } else if (event.code == 'KeyR' || event.code == 'Digit1') { + window.location = `#(1)`; + } else if (event.code == 'KeyA') { + if (mc().position == '(all)') { + window.location = "#(1)"; + } else { + window.location = "#(all)"; + } + } + }); + finishInitGame(); + if (window.location.hash) { + game.takeTurn(window.location.hash); + } +} +window.addEventListener("load", (event) => { + let has_onload = sel("body")[0].getAttribute("onload") ? true : false; + let is_slides = sel("x-slides")[0] ? true : false; + let is_game = sel("x-game")[0] ? true : false; + if (has_onload) { + return; + } else if (is_slides) { + initSlides(); + } else if (is_game) { + initGame(); + } +}); diff --git a/src/scripts/slidy.js b/src/scripts/slidy.js deleted file mode 100644 index 0deff54f2..000000000 --- a/src/scripts/slidy.js +++ /dev/null @@ -1,2386 +0,0 @@ -/* slidy.js - Copyright (c) 2005-2013 W3C (MIT, ERCIM, Keio), All Rights Reserved. - W3C liability, trademark, document use and software licensing - rules apply, see: - http://www.w3.org/Consortium/Legal/copyright-documents - http://www.w3.org/Consortium/Legal/copyright-software - Defines single name "w3c_slidy" in global namespace - Adds event handlers without trampling on any others -*/ -// the slidy object implementation -var w3c_slidy = { - // classify which kind of browser we're running under - ns_pos: (typeof window.pageYOffset!='undefined'), - khtml: ((navigator.userAgent).indexOf("KHTML") >= 0 ? true : false), - opera: ((navigator.userAgent).indexOf("Opera") >= 0 ? true : false), - ipad: ((navigator.userAgent).indexOf("iPad") >= 0 ? true : false), - iphone: ((navigator.userAgent).indexOf("iPhone") >= 0 ? true : false), - android: ((navigator.userAgent).indexOf("Android") >= 0 ? true : false), - ie: (typeof document.all != "undefined" && !this.opera), - // data for swipe and double tap detection on touch screens - last_tap: 0, - prev_tap: 0, - start_x: 0, - start_y: 0, - delta_x: 0, - delta_y: 0, - // are we running as XHTML? (doesn't work on Opera) - is_xhtml: /xml/.test(document.contentType), - slide_number: 0, // integer slide count: 0, 1, 2, ... - slide_number_element: null, // element containing slide number - slides: [], // set to array of slide div's - notes: [], // set to array of handout div's - backgrounds: [], // set to array of background div's - observers: [], // list of observer functions - toolbar: null, // element containing toolbar - title: null, // document title - last_shown: null, // last incrementally shown item - eos: null, // span element for end of slide indicator - toc: null, // table of contents - outline: null, // outline element with the focus - selected_text_len: 0, // length of drag selection on document - view_all: 0, // 1 to view all slides + handouts - want_toolbar: true, // user preference to show/hide toolbar - mouse_click_enabled: true, // enables left click for next slide - scroll_hack: 0, // IE work around for position: fixed - disable_slide_click: false, // used by clicked anchors - lang: "en", // updated to language specified by html file - help_anchor: null, // used for keyboard focus hack in showToolbar() - help_page: "http://www.w3.org/Talks/Tools/Slidy2/help/help.html", - help_text: "Navigate with mouse click, space bar, Cursor Left/Right, " + - "or Pg Up and Pg Dn. Use S and B to change font size.", - size_index: 0, - size_adjustment: 0, - sizes: new Array("10pt", "12pt", "14pt", "16pt", "18pt", "20pt", - "22pt", "24pt", "26pt", "28pt", "30pt", "32pt"), - // needed for efficient resizing - last_width: 0, - last_height: 0, - // Needed for cross browser support for relative width/height on - // object elements. The work around is to save width/height attributes - // and then to recompute absolute width/height dimensions on resizing - objects: [], - up_link: null, - // attach initialiation event handlers - set_up: function () { - var init = function() { - w3c_slidy.init(); - }; - if (typeof window.addEventListener != "undefined") { - window.addEventListener("load", init, false); - } else { - window.attachEvent("onload", init); - } - }, - hide_slides: function () { - if (this.body && !w3c_slidy.initialized) { - this.body.style.visibility = "hidden"; - } else { - setTimeout(w3c_slidy.hide_slides, 50); - } - }, - // hack to persuade IE to compute correct document height - // as needed for simulating fixed positioning of toolbar - ie_hack: function () { - window.resizeBy(0,-1); - window.resizeBy(0, 1); - }, - init: function () { - this.body = document.body; - if (typeof setDisplay === "function") { - setDisplay("top-container", false); - } - this.body.style.visibility = "visible"; - this.init_localization(); - this.add_toolbar(); - this.wrap_implicit_slides(); - this.collect_slides(); - this.collect_notes(); - this.collect_backgrounds(); - this.objects = this.body.getElementsByTagName("object"); - this.patch_anchors(); - this.slide_number = this.find_slide_number(location.href); - window.offscreenbuffering = true; - this.size_adjustment = this.find_size_adjust(); - this.time_left = this.find_duration(); - this.hide_image_toolbar(); // suppress IE image toolbar popup - this.init_outliner(); // activate fold/unfold support - this.title = document.title; - this.keyboardless = (this.ipad||this.iphone||this.android); - if (this.keyboardless) { - w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden") - this.want_toolbar = 0; - } - // work around for opera bug - this.is_xhtml = (this.body.tagName == "BODY" ? false : true); - if (this.slides.length > 0) { - var slide = this.slides[this.slide_number]; - if (this.slide_number > 0) { - this.set_visibility_all_incremental("visible"); - this.last_shown = this.previous_incremental_item(null); - this.set_eos_status(true); - } else { - this.last_shown = null; - this.set_visibility_all_incremental("hidden"); - this.set_eos_status( - !this.next_incremental_item(this.last_shown)); - } - this.set_location(); - this.add_class(this.slides[0], "first-slide"); - w3c_slidy.show_slide(slide); - } - this.toc = this.table_of_contents(); - this.add_initial_prompt(); - // bind event handlers without interfering with custom page scripts - // Tap events behave too weirdly to support clicks reliably on - // iPhone and iPad, so exclude these from click handler - if (!this.keyboardless) { - this.add_listener(this.body, "click", this.mouse_button_click); - this.add_listener(this.body, "mousedown", - this.mouse_button_down); - } - this.add_listener(document, "keydown", this.key_down); - this.add_listener(document, "keypress", this.key_press); - this.add_listener(window, "resize", this.resized); - this.add_listener(window, "scroll", this.scrolled); - this.add_listener(window, "unload", this.unloaded); - this.add_listener(document, "gesturechange", function () { - return false; - }); - this.attach_touch_handers(this.slides); - this.single_slide_view(); - this.resized(); - this.show_toolbar(); - // for back button detection - setInterval(function () { - w3c_slidy.check_location(); - }, 200); - w3c_slidy.initialized = true; - }, - // create div element with links to each slide - table_of_contents: function () { - var toc = this.create_element("div"); - this.add_class(toc, "slidy_toc hidden"); - //toc.setAttribute("tabindex", "0"); - var heading = this.create_element("div"); - this.add_class(heading, "toc-heading"); - heading.innerHTML = this.localize("Table of Contents"); - toc.appendChild(heading); - var previous = null; - for (var i = 0; i < this.slides.length; ++i) { - var title = this.has_class(this.slides[i], "title"); - var num = document.createTextNode((i + 1) + ". "); - toc.appendChild(num); - var a = this.create_element("a"); - a.setAttribute("href", "#(" + (i+1) + ")"); - if (title) { - this.add_class(a, "titleslide"); - } - var name = document.createTextNode(this.slide_name(i)); - a.appendChild(name); - a.onclick = w3c_slidy.toc_click; - a.onkeydown = w3c_slidy.toc_key_down; - a.previous = previous; - if (previous) { - previous.next = a; - } - toc.appendChild(a); - if (i == 0) { - toc.first = a; - } - if (i < this.slides.length - 1) { - var br = this.create_element("br"); - toc.appendChild(br); - } - previous = a; - } - toc.focus = function () { - if (this.first) { - this.first.focus(); - } - } - toc.onmouseup = w3c_slidy.mouse_button_up; - toc.onclick = function (e) { - e||(e=window.event); - if (w3c_slidy.selected_text_len <= 0) { - w3c_slidy.hide_table_of_contents(true); - } - w3c_slidy.stop_propagation(e); - if (e.cancel != undefined) { - e.cancel = true; - } - if (e.returnValue != undefined) { - e.returnValue = false; - } - return false; - }; - this.body.insertBefore(toc, this.body.firstChild); - return toc; - }, - is_shown_toc: function () { - return !w3c_slidy.has_class(w3c_slidy.toc, "hidden"); - }, - show_table_of_contents: function () { - w3c_slidy.remove_class(w3c_slidy.toc, "hidden"); - var toc = w3c_slidy.toc; - toc.focus(); - }, - hide_table_of_contents: function (focus) { - w3c_slidy.add_class(w3c_slidy.toc, "hidden"); - if (focus && !w3c_slidy.opera && - !w3c_slidy.has_class(w3c_slidy.toc, "hidden")) { - w3c_slidy.set_focus(); - } - }, - toggle_table_of_contents: function () { - if (w3c_slidy.is_shown_toc()) { - w3c_slidy.hide_table_of_contents(true); - } else { - w3c_slidy.show_table_of_contents(); - } - }, - // called on clicking toc entry - toc_click: function (e) { - if (!e) { - e = window.event; - } - var target = w3c_slidy.get_target(e); - if (target && target.nodeType == 1) { - var uri = target.getAttribute("href"); - if (uri) { - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = w3c_slidy.find_slide_number(uri); - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.last_shown = null; - w3c_slidy.set_location(); - w3c_slidy.set_visibility_all_incremental("hidden"); - w3c_slidy.set_eos_status( - !w3c_slidy.next_incremental_item(w3c_slidy.last_shown)); - w3c_slidy.show_slide(slide); - try { - if (!w3c_slidy.opera) { - w3c_slidy.set_focus(); - } - } catch (e) { - } - } - } - w3c_slidy.hide_table_of_contents(true); - w3c_slidy.stop_propagation(e); - return w3c_slidy.cancel(e); - }, - // called onkeydown for toc entry - toc_key_down: function (event) { - var key; - if (!event) { - var event = window.event; - } - // kludge around NS/IE differences - if (window.event) { - key = window.event.keyCode; - } else if (event.which) { - key = event.which; - } else { - return true; // Yikes! unknown browser - } - // ignore event if key value is zero - // as for alt on Opera and Konqueror - if (!key) { - return true; - } - // check for concurrent control/command/alt key - // but are these only present on mouse events? - if (event.ctrlKey || event.altKey) { - return true; - } - if (key == 13) { - var uri = this.getAttribute("href"); - if (uri) { - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = w3c_slidy.find_slide_number(uri); - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.last_shown = null; - w3c_slidy.set_location(); - w3c_slidy.set_visibility_all_incremental("hidden"); - w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item( - w3c_slidy.last_shown)); - w3c_slidy.show_slide(slide); - try { - if (!w3c_slidy.opera) { - w3c_slidy.set_focus(); - } - } catch (e) { - } - } - w3c_slidy.hide_table_of_contents(true); - return w3c_slidy.cancel(event); - } - if (key == 40 && this.next) { - this.next.focus(); - return w3c_slidy.cancel(event); - } - if (key == 38 && this.previous) { - this.previous.focus(); - return w3c_slidy.cancel(event); - } - return true; - }, - touchstart: function (e) { - // a double touch often starts with a - // single touch due to fingers touching - // down at slightly different times - // thus avoid calling preventDefault here - this.prev_tap = this.last_tap; - this.last_tap = (new Date).getTime(); - var tap_delay = this.last_tap - this.prev_tap; - if (tap_delay <= 200) { - // double tap - } - var touch = e.touches[0]; - this.pageX = touch.pageX; - this.pageY = touch.pageY; - this.screenX = touch.screenX; - this.screenY = touch.screenY; - this.clientX = touch.clientX; - this.clientY = touch.clientY; - this.delta_x = this.delta_y = 0; - }, - touchmove: function (e) - { - // override native gestures for single touch - if (e.touches.length > 1) { - return; - } - e.preventDefault(); - var touch = e.touches[0]; - this.delta_x = touch.pageX - this.pageX; - this.delta_y = touch.pageY - this.pageY; - }, - touchend: function (e) { - // default behavior for multi-touch - if (e.touches.length > 1) { - return; - } - var delay = (new Date).getTime() - this.last_tap; - var dx = this.delta_x; - var dy = this.delta_y; - var abs_dx = Math.abs(dx); - var abs_dy = Math.abs(dy); - if (delay < 500 && (abs_dx > 100 || abs_dy > 100)) { - if (abs_dx > 0.5 * abs_dy) { - e.preventDefault(); - if (dx < 0) { - w3c_slidy.next_slide(true); - } else { - w3c_slidy.previous_slide(true); - } - } else if (abs_dy > 2 * abs_dx) { - e.preventDefault(); - w3c_slidy.toggle_table_of_contents(); - } - } - }, - // ### OBSOLETE ### - before_print: function () { - this.show_all_slides(); - this.hide_toolbar(); - alert("before print"); - }, - // ### OBSOLETE ### - after_print: function () { - if (!this.view_all) { - this.single_slide_view(); - this.show_toolbar(); - } - alert("after print"); - }, - // ### OBSOLETE ### - print_slides: function () { - this.before_print(); - window.print(); - this.after_print(); - }, - // ### OBSOLETE ?? ### - toggle_view: function () { - if (this.view_all) { - this.single_slide_view(); - this.show_toolbar(); - this.view_all = 0; - } else { - this.show_all_slides(); - this.hide_toolbar(); - this.view_all = 1; - } - }, - // prepare for printing ### OBSOLETE ### - show_all_slides: function () { - this.remove_class(this.body, "single_slide"); - this.set_visibility_all_incremental("visible"); - }, - // restore after printing ### OBSOLETE ### - single_slide_view: function () { - this.add_class(this.body, "single_slide"); - this.set_visibility_all_incremental("visible"); - this.last_shown = this.previous_incremental_item(null); - }, - // suppress IE's image toolbar pop up - hide_image_toolbar: function () { - if (!this.ns_pos) { - var images = document.getElementsByTagName("IMG"); - for (var i = 0; i < images.length; ++i) { - images[i].setAttribute("galleryimg", "no"); - } - } - }, - unloaded: function (e) { - }, - // Safari and Konqueror don't yet support getComputedStyle() - // and they always reload page when location.href is updated - is_KHTML: function () { - var agent = navigator.userAgent; - return (agent.indexOf("KHTML") >= 0 ? true : false); - }, - // find slide name from first h1 element - // default to document title + slide number - slide_name: function (index) { - var name = null; - var slide = this.slides[index]; - var heading = this.find_heading(slide); - if (heading) { - name = this.extract_text(heading); - } - if (!name) { - name = this.title + "(" + (index + 1) + ")"; - } - name.replace(/\&/g, "&"); - name.replace(/\</g, "<"); - name.replace(/\>/g, ">"); - return name; - }, - // find first h1 element in DOM tree - find_heading: function (node) { - if (!node || node.nodeType != 1) { - return null; - } - if (node.nodeName == "H1" || node.nodeName == "h1") { - return node; - } - var child = node.firstChild; - while (child) { - node = this.find_heading(child); - if (node) { - return node; - } - child = child.nextSibling; - } - return null; - }, - // recursively extract text from DOM tree - extract_text: function (node) { - if (!node) { - return ""; - } - // text nodes - if (node.nodeType == 3) { - return node.nodeValue; - } - // elements - if (node.nodeType == 1) { - node = node.firstChild; - var text = ""; - while (node) { - text = text + this.extract_text(node); - node = node.nextSibling; - } - return text; - } - return ""; - }, - // find copyright text from meta element - find_copyright: function () { - var name, content; - var meta = document.getElementsByTagName("meta"); - for (var i = 0; i < meta.length; ++i) { - name = meta[i].getAttribute("name"); - content = meta[i].getAttribute("content"); - if (name == "copyright") { - return content; - } - } - return null; - }, - find_size_adjust: function () { - var name, content, offset; - var meta = document.getElementsByTagName("meta"); - for (var i = 0; i < meta.length; ++i) { - name = meta[i].getAttribute("name"); - content = meta[i].getAttribute("content"); - if (name == "font-size-adjustment") { - return 1 * content; - } - } - return 1; - }, - // <meta name="duration" content="20" > for 20 minutes - find_duration: function () { - var name, content, offset; - var meta = document.getElementsByTagName("meta"); - for (var i = 0; i < meta.length; ++i) { - name = meta[i].getAttribute("name"); - content = meta[i].getAttribute("content"); - if (name == "duration") { - return 60000 * content; - } - } - return null; - }, - replace_by_non_breaking_space: function (str) { - for (var i = 0; i < str.length; ++i) { - str[i] = 160; - } - }, - // ### CHECK ME ### is use of "li" okay for text/html? - // for XHTML do we also need to specify namespace? - init_outliner: function () { - var items = document.getElementsByTagName("li"); - for (var i = 0; i < items.length; ++i) { - var target = items[i]; - if (!this.has_class(target.parentNode, "outline")) { - continue; - } - target.onclick = this.outline_click; - if (this.foldable(target)) { - target.foldable = true; - target.onfocus = function () { - w3c_slidy.outline = this; - }; - target.onblur = function () { - w3c_slidy.outline = null; - }; - if (!target.getAttribute("tabindex")) { - target.setAttribute("tabindex", "0"); - } - if (this.has_class(target, "expand")) { - this.unfold(target); - } else { - this.fold(target); - } - } else { - this.add_class(target, "nofold"); - target.visible = true; - target.foldable = false; - } - } - }, - foldable: function (item) { - if (!item || item.nodeType != 1) { - return false; - } - var node = item.firstChild; - while (node) { - if (node.nodeType == 1 && this.is_block(node)) { - return true; - } - node = node.nextSibling; - } - return false; - }, - // ### CHECK ME ### switch to add/remove "hidden" class - fold: function (item) { - if (item) { - this.remove_class(item, "unfolded"); - this.add_class(item, "folded"); - } - var node = item ? item.firstChild : null; - while (node) { - if (node.nodeType == 1 && this.is_block(node)) { // element - w3c_slidy.add_class(node, "hidden"); - } - node = node.nextSibling; - } - item.visible = false; - }, - // ### CHECK ME ### switch to add/remove "hidden" class - unfold: function (item) { - if (item) { - this.add_class(item, "unfolded"); - this.remove_class(item, "folded"); - } - var node = item ? item.firstChild : null; - while (node) { - if (node.nodeType == 1 && this.is_block(node)) { // element - w3c_slidy.remove_class(node, "hidden"); - } - node = node.nextSibling; - } - item.visible = true; - }, - outline_click: function (e) { - if (!e) { - e = window.event; - } - var rightclick = false; - var target = w3c_slidy.get_target(e); - while (target && target.visible == undefined) { - target = target.parentNode; - } - if (!target) { - return true; - } - if (e.which) { - rightclick = (e.which == 3); - } else if (e.button) { - rightclick = (e.button == 2); - } - if (!rightclick && target.visible != undefined) { - if (target.foldable) { - if (target.visible) { - w3c_slidy.fold(target); - } else { - w3c_slidy.unfold(target); - } - } - w3c_slidy.stop_propagation(e); - e.cancel = true; - e.returnValue = false; - } - return false; - }, - add_initial_prompt: function () { - var prompt = this.create_element("div"); - prompt.setAttribute("class", "initial_prompt"); - var p1 = this.create_element("p"); - prompt.appendChild(p1); - p1.setAttribute("class", "help"); - if (this.keyboardless) { - p1.innerHTML = "swipe left to move to next slide"; - } else { - p1.innerHTML = "Space, Right Arrow or swipe left to move to " + - "next slide, click help below for more details"; - } - this.add_listener(prompt, "click", function (e) { - this.body.removeChild(prompt); - w3c_slidy.stop_propagation(e); - if (e.cancel != undefined) { - e.cancel = true; - } - if (e.returnValue != undefined) { - e.returnValue = false; - } - return false; - }); - this.body.appendChild(prompt); - this.initial_prompt = prompt; - setTimeout(function() { - w3c_slidy.body.removeChild(prompt); - }, 5000); - }, - add_toolbar: function () { - var counter, page; - this.toolbar = this.create_element("div"); - this.toolbar.setAttribute("class", "toolbar"); - // a reasonably behaved browser - var right = this.create_element("div"); - right.setAttribute("style", "float: right; text-align: right"); - counter = this.create_element("span") - counter.innerHTML = this.localize("slide") + " n/m"; - right.appendChild(counter); - this.toolbar.appendChild(right); - var left = this.create_element("div"); - left.setAttribute("style", "text-align: left"); - // global end of slide indicator - this.eos = this.create_element("span"); - this.eos.innerHTML = "* "; - left.appendChild(this.eos); - var hamburger = this.create_element("a"); - hamburger.setAttribute("href", "javascript:toggleOptions()"); - hamburger.innerHTML = "≡"; - left.appendChild(hamburger); - this.spacer = this.create_element("span"); - this.spacer.innerHTML = " "; - left.appendChild(this.spacer); - var help = this.create_element("a"); - help.setAttribute("href", this.help_page); - help.setAttribute("title", this.localize(this.help_text)); - help.innerHTML = this.localize("help?"); - left.appendChild(help); - this.help_anchor = help; // save for focus hack - var gap1 = document.createTextNode(" "); - left.appendChild(gap1); - var contents = this.create_element("a"); - contents.setAttribute("href", - "javascript:w3c_slidy.toggle_table_of_contents()"); - contents.setAttribute("title", this.localize("table of contents")); - contents.innerHTML = this.localize("contents?"); - left.appendChild(contents); - var gap2 = document.createTextNode(" "); - left.appendChild(gap2); - var links = document.getElementsByTagName("link"); - for (var i = 0; i < links.length; i++) { - if (links[i].getAttribute("rel") == "prev" ) { - w3c_slidy.up_link = links[i].getAttribute("href"); - } - } - if (w3c_slidy.up_link) { - var contents = this.create_element("a"); - contents.setAttribute("href", w3c_slidy.up_link); - contents.setAttribute("title", - this.localize("Pre-slideshow link")); - contents.innerHTML = this.localize("up?"); - left.appendChild(contents); - } - var copyright = this.find_copyright(); - if (copyright) { - var span = this.create_element("span"); - span.className = "copyright"; - span.innerHTML = copyright; - left.appendChild(span); - } - this.toolbar.setAttribute("tabindex", "0"); - this.toolbar.appendChild(left); - // ensure that click isn't passed through to the page - this.toolbar.onclick = function (e) { - if (!e) { - e = window.event; - } - var target = e.target; - if (!target && e.srcElement) { - target = e.srcElement; - } - // work around Safari bug - if (target && target.nodeType == 3) { - target = target.parentNode; - } - w3c_slidy.stop_propagation(e); - if (target && target.nodeName.toLowerCase() != "a") { - w3c_slidy.mouse_button_click(e); - } - }; - this.slide_number_element = counter; - this.set_eos_status(false); - this.body.appendChild(this.toolbar); - }, - // wysiwyg editors make it hard to use div elements - // e.g. amaya loses the div when you copy and paste - // this function wraps div elements around implicit - // slides which start with an h1 element and continue - // up to the next heading or div element - wrap_implicit_slides: function () { - var i, heading, node, next, div; - var headings = document.getElementsByTagName("h1"); - if (!headings) { - return; - } - for (i = 0; i < headings.length; ++i) { - heading = headings[i]; - if (heading.parentNode != this.body) { - continue; - } - node = heading.nextSibling; - div = document.createElement("div"); - this.add_class(div, "slide"); - this.body.replaceChild(div, heading); - div.appendChild(heading); - while (node) { - if (node.nodeType == 1) { // an element - if (node.nodeName == "H1" || node.nodeName == "h1") { - break; - } - if (node.nodeName == "DIV" || node.nodeName == "div") { - if (this.has_class(node, "slide")) { - break; - } - if (this.has_class(node, "handout")) { - break; - } - } - } - next = node.nextSibling; - node = this.body.removeChild(node); - div.appendChild(node); - node = next; - } - } - }, - attach_touch_handers: function(slides) { - var i, slide; - for (i = 0; i < slides.length; ++i) { - slide = slides[i]; - this.add_listener(slide, "touchstart", this.touchstart); - this.add_listener(slide, "touchmove", this.touchmove); - this.add_listener(slide, "touchend", this.touchend); - } - }, - // return new array of all slides - collect_slides: function () { - var slides = new Array(); - var divs = this.body.getElementsByTagName("div"); - for (var i = 0; i < divs.length; ++i) { - div = divs.item(i); - if (this.has_class(div, "slide")) { - // add slide to collection - slides[slides.length] = div; - // hide each slide as it is found - this.add_class(div, "hidden"); - // add dummy <br> at end for scrolling hack - var node1 = document.createElement("br"); - div.appendChild(node1); - var node2 = document.createElement("br"); - div.appendChild(node2); - } else if (this.has_class(div, "background")) { - // work around for Firefox SVG reload bug - // which otherwise replaces 1st SVG graphic with 2nd - div.style.display = "block"; - } - } - this.slides = slides; - }, - // return new array of all <div class="handout"> - collect_notes: function () { - var notes = new Array(); - var divs = this.body.getElementsByTagName("div"); - for (var i = 0; i < divs.length; ++i) { - div = divs.item(i); - if (this.has_class(div, "handout")) { - // add note to collection - notes[notes.length] = div; - // and hide it - this.add_class(div, "hidden"); - } - } - this.notes = notes; - }, - // return new array of all <div class="background"> - // including named backgrounds e.g. class="background titlepage" - collect_backgrounds: function () { - var backgrounds = new Array(); - var divs = this.body.getElementsByTagName("div"); - for (var i = 0; i < divs.length; ++i) { - div = divs.item(i); - if (this.has_class(div, "background")) { - // add background to collection - backgrounds[backgrounds.length] = div; - // and hide it - this.add_class(div, "hidden"); - } - } - this.backgrounds = backgrounds; - }, - // set click handlers on all anchors - patch_anchors: function () { - var self = w3c_slidy; - var handler = function (event) { - // compare this.href with location.href - // for link to another slide in this doc - if (self.page_address(this.href) == self.page_address( - location.href)) { // yes, so find new slide number - var newslidenum = self.find_slide_number(this.href); - if (newslidenum != self.slide_number) { - var slide = self.slides[self.slide_number]; - self.hide_slide(slide); - self.slide_number = newslidenum; - slide = self.slides[self.slide_number]; - self.show_slide(slide); - self.set_location(); - } - } else { - w3c_slidy.stop_propagation(event); - } - this.blur(); - self.disable_slide_click = true; - }; - var anchors = this.body.getElementsByTagName("a"); - for (var i = 0; i < anchors.length; ++i) { - if (window.addEventListener) { - anchors[i].addEventListener("click", handler, false); - } else { - anchors[i].attachEvent("onclick", handler); - } - } - }, - // ### CHECK ME ### see which functions are invoked via setTimeout - // either directly or indirectly for use of w3c_slidy vs this - show_slide_number: function () { - var timer = w3c_slidy.get_timer(); - w3c_slidy.slide_number_element.innerHTML = timer + - w3c_slidy.localize("slide") + " " + - (w3c_slidy.slide_number + 1) + "/" + w3c_slidy.slides.length; - }, - // every 200mS check if the location has been changed as a - // result of the user activating the Back button/menu item - // doesn't work for Opera < 9.5 - check_location: function () { - var hash = location.hash; - if (w3c_slidy.slide_number > 0 && (hash == "" || hash == "#")) { - w3c_slidy.goto_slide(0); - } else if (hash.length > 2 && - hash != "#(" + (w3c_slidy.slide_number + 1)+")") { - var num = parseInt(location.hash.substr(2)); - if (!isNaN(num)) { - w3c_slidy.goto_slide(num - 1); - } - } - if (w3c_slidy.time_left && w3c_slidy.slide_number > 0) { - w3c_slidy.show_slide_number(); - if (w3c_slidy.time_left > 0) { - w3c_slidy.time_left -= 200; - } - } - }, - get_timer: function () { - var timer = ""; - if (w3c_slidy.time_left) { - var mins, secs; - secs = Math.floor(w3c_slidy.time_left/1000); - mins = Math.floor(secs / 60); - secs = secs % 60; - timer = (mins ? mins+"m" : "") + secs + "s "; - } - return timer; - }, - // this doesn't push location onto history stack for IE - // for which a hidden iframe hack is needed: load page into - // the iframe with script that set's parent's location.hash - // but that won't work for standalone use unless we can - // create the page dynamically via a javascript: URL - // ### use history.pushState if available - set_location: function () { - var uri = w3c_slidy.page_address(location.href); - var hash = "#(" + (w3c_slidy.slide_number+1) + ")"; - if (w3c_slidy.slide_number >= 0) - uri = uri + hash; - if (typeof(history.pushState) != "undefined") { - document.title = w3c_slidy.title + " (" + - (w3c_slidy.slide_number+1) + ")"; - history.pushState(0, document.title, hash); - w3c_slidy.show_slide_number(); - w3c_slidy.notify_observers(); - return; - } - if (uri != location.href) {// && !khtml - location.href = uri; - } - if (this.khtml) { - hash = "(" + (w3c_slidy.slide_number+1) + ")"; - } - if (!this.ie && location.hash != hash && location.hash != "") { - location.hash = hash; - } - document.title = w3c_slidy.title + " (" + - (w3c_slidy.slide_number+1) + ")"; - w3c_slidy.show_slide_number(); - w3c_slidy.notify_observers(); - }, - notify_observers: function () { - var slide = this.slides[this.slide_number]; - for (var i = 0; i < this.observers.length; ++i) { - this.observers[i](this.slide_number+1, - this.find_heading(slide).innerText, location.href); - } - }, - add_observer: function (observer) { - for (var i = 0; i < this.observers.length; ++i) { - if (observer == this.observers[i]) { - return; - } - } - this.observers.push(observer); - }, - remove_observer: function (o) - { - for (var i = 0; i < this.observers.length; ++i) { - if (observer == this.observers[i]) { - this.observers.splice(i,1); - break; - } - } - }, - page_address: function (uri) { - var i = uri.indexOf("#"); - if (i < 0) { - i = uri.indexOf("%23"); - } - // check if anchor is entire page - if (i < 0) { - return uri; // yes - } - return uri.substr(0, i); - }, - // history hack with thanks to Bertrand Le Roy - push_hash: function (hash) { - if (hash == "") { - hash = "#(1)"; - } - window.location.hash = hash; - var doc = document.getElementById( - "historyFrame").contentWindow.document; - doc.open("javascript:'<html></html>'"); - doc.write("<html><head><script>" + - "window.parent.w3c_slidy.on_frame_loaded('"+ - (hash) + "');</script></head><body>hello mum</body></html>"); - doc.close(); - }, - // find current slide based upon location - // first find target anchor and then look - // for associated div element enclosing it - // finally map that to slide number - find_slide_number: function (uri) { - // first get anchor from page location - var i = uri.indexOf("#"); - // check if anchor is entire page - if (i < 0) { - return 0; // yes - } - var anchor = unescape(uri.substr(i+1)); - // now use anchor as XML ID to find target - var target = document.getElementById(anchor); - if (!target) { - // does anchor look like "(2)" for slide 2 ?? - // where first slide is (1) - var re = /\((\d)+\)/; - if (anchor.match(re)) { - var num = parseInt(anchor.substring(1, anchor.length-1)); - if (num > this.slides.length) { - num = 1; - } - if (--num < 0) { - num = 0; - } - return num; - } - // accept [2] for backwards compatibility - re = /\[(\d)+\]/; - if (anchor.match(re)) { - var num = parseInt(anchor.substring(1, anchor.length-1)); - if (num > this.slides.length) { - num = 1; - } - if (--num < 0) { - num = 0; - } - return num; - } - // oh dear unknown anchor - return 0; - } - // search for enclosing slide - while (true) { - // browser coerces html elements to uppercase! - if (target.nodeName.toLowerCase() == "div" && - this.has_class(target, "slide")) { - // found the slide element - break; - } - // otherwise try parent element if any - target = target.parentNode; - if (!target) { - return 0; // no luck! - } - } - for (i = 0; i < slides.length; ++i) { - if (slides[i] == target) - return i; // success - } - // oh dear still no luck - return 0; - }, - previous_slide: function (incremental) { - if (!w3c_slidy.view_all) { - var slide; - if ((incremental || w3c_slidy.slide_number == 0) && - w3c_slidy.last_shown != null) { - w3c_slidy.last_shown = - w3c_slidy.hide_previous_item(w3c_slidy.last_shown); - w3c_slidy.set_eos_status(false); - } else if (w3c_slidy.slide_number > 0) { - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - - w3c_slidy.slide_number = w3c_slidy.slide_number - 1; - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.set_visibility_all_incremental("visible"); - w3c_slidy.last_shown = - w3c_slidy.previous_incremental_item(null); - w3c_slidy.set_eos_status(true); - w3c_slidy.show_slide(slide); - } - w3c_slidy.set_location(); - if (!w3c_slidy.ns_pos) { - w3c_slidy.refresh_toolbar(200); - } - } - }, - next_slide: function (incremental) { - if (!w3c_slidy.view_all) { - var slide, last = w3c_slidy.last_shown; - if (incremental || - w3c_slidy.slide_number == w3c_slidy.slides.length - 1) { - w3c_slidy.last_shown = - w3c_slidy.reveal_next_item(w3c_slidy.last_shown); - } - if ((!incremental || w3c_slidy.last_shown == null) && - w3c_slidy.slide_number < w3c_slidy.slides.length - 1) { - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = w3c_slidy.slide_number + 1; - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.last_shown = null; - w3c_slidy.set_visibility_all_incremental("hidden"); - w3c_slidy.show_slide(slide); - } else if (!w3c_slidy.last_shown) { - if (last && incremental) { - w3c_slidy.last_shown = last; - } - } - w3c_slidy.set_location(); - w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item( - w3c_slidy.last_shown)); - if (!w3c_slidy.ns_pos) { - w3c_slidy.refresh_toolbar(200); - } - } - }, - // to first slide with nothing revealed - // i.e. state at start of presentation - first_slide: function () { - if (!w3c_slidy.view_all) { - var slide; - if (w3c_slidy.slide_number != 0) { - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = 0; - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.last_shown = null; - w3c_slidy.set_visibility_all_incremental("hidden"); - w3c_slidy.show_slide(slide); - } - w3c_slidy.set_eos_status( - !w3c_slidy.next_incremental_item(w3c_slidy.last_shown)); - w3c_slidy.set_location(); - } - }, - // goto last slide with everything revealed - // i.e. state at end of presentation - last_slide: function () { - if (!w3c_slidy.view_all) { - var slide; - w3c_slidy.last_shown = null; - if (w3c_slidy.last_shown == null && - w3c_slidy.slide_number < w3c_slidy.slides.length - 1) { - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = w3c_slidy.slides.length - 1; - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.set_visibility_all_incremental("visible"); - w3c_slidy.last_shown = w3c_slidy.previous_incremental_item(null); - w3c_slidy.show_slide(slide); - } else { - w3c_slidy.set_visibility_all_incremental("visible"); - w3c_slidy.last_shown = - w3c_slidy.previous_incremental_item(null); - } - w3c_slidy.set_eos_status(true); - w3c_slidy.set_location(); - } - }, - // ### check this and consider add/remove class - set_eos_status: function (state) { - if (this.eos) { - this.eos.style.color = (state ? "rgb(240,240,240)" : "red"); - } - }, - // first slide is 0 - goto_slide: function (num) { - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.slide_number = num; - slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.last_shown = null; - w3c_slidy.set_visibility_all_incremental("hidden"); - w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item( - w3c_slidy.last_shown)); - document.title = w3c_slidy.title + " (" + - (w3c_slidy.slide_number+1) + ")"; - w3c_slidy.show_slide(slide); - w3c_slidy.show_slide_number(); - }, - show_slide: function (slide) { - this.sync_background(slide); - this.remove_class(slide, "hidden"); - }, - hide_slide: function (slide) { - this.add_class(slide, "hidden"); - }, - set_focus: function (element) { - if (element) { - element.focus(); - } else { - w3c_slidy.help_anchor.focus(); - setTimeout(function() { - w3c_slidy.help_anchor.blur(); - }, 1); - } - }, - // show just the backgrounds pertinent to this slide - // when slide background-color is transparent - // this should now work with rgba color values - sync_background: function (slide) { - var background; - var bgColor; - if (slide.currentStyle) { - bgColor = slide.currentStyle["backgroundColor"]; - } else if (document.defaultView) { - var styles = document.defaultView.getComputedStyle(slide,null); - if (styles) { - bgColor = styles.getPropertyValue("background-color"); - } else { // broken implementation probably due Safari or Konqueror - bgColor = "transparent"; - } - } else { - bgColor == "transparent"; - } - if (bgColor == "transparent" || - bgColor.indexOf("rgba") >= 0 || - bgColor.indexOf("opacity") >= 0) { - var slideClass = this.get_class_list(slide); - for (var i = 0; i < this.backgrounds.length; i++) { - background = this.backgrounds[i]; - var bgClass = this.get_class_list(background); - if (this.matching_background(slideClass, bgClass)) { - this.remove_class(background, "hidden"); - } else { - this.add_class(background, "hidden"); - } - } - } else {// forcibly hide all backgrounds - this.hide_backgrounds(); - } - }, - hide_backgrounds: function () { - for (var i = 0; i < this.backgrounds.length; i++) { - background = this.backgrounds[i]; - this.add_class(background, "hidden"); - } - }, - // compare classes for slide and background - matching_background: function (slideClass, bgClass) { - var i, count, pattern, result; - // define pattern as regular expression - pattern = /\w+/g; - // check background class names - result = bgClass.match(pattern); - for (i = count = 0; i < result.length; i++) { - if (result[i] == "hidden") { - continue; - } - if (result[i] == "background") { - continue; - } - ++count; - } - if (count == 0) { // default match - return true; - } - // check for matches and place result in array - result = slideClass.match(pattern); - // now check if desired name is present for background - for (i = count = 0; i < result.length; i++) { - if (result[i] == "hidden") { - continue; - } - if (this.has_token(bgClass, result[i])) { - return true; - } - } - return false; - }, - resized: function () { - var width = 0; - width = window.innerWidth; // Non IE browser - var height = 0; - height = window.innerHeight; // Non IE browser - if (height && (width/height > 1.05*1024/768)) { - width = height * 1024.0/768; - } - // IE fires onresize even when only font size is changed! - // so we do a check to avoid blocking < and > actions - if (width != w3c_slidy.last_width || height != w3c_slidy.last_height) { - if (width >= 1100) { - w3c_slidy.size_index = 5; // 4 - } else if (width >= 1000) { - w3c_slidy.size_index = 4; // 3 - } else if (width >= 800) { - w3c_slidy.size_index = 3; // 2 - } else if (width >= 600) { - w3c_slidy.size_index = 2; // 1 - } else if (width) { - w3c_slidy.size_index = 0; - } - // add in font size adjustment from meta element e.g. - // <meta name="font-size-adjustment" content="-2" > - // useful when slides have too much content ;-) - if (0 <= w3c_slidy.size_index + w3c_slidy.size_adjustment && - w3c_slidy.size_index + w3c_slidy.size_adjustment < - w3c_slidy.sizes.length) { - w3c_slidy.size_index = w3c_slidy.size_index + - w3c_slidy.size_adjustment; - } - // enables cross browser use of relative width/height - // on object elements for use with SVG and Flash media - w3c_slidy.adjust_object_dimensions(width, height); - if (this.body.style.fontSize != - w3c_slidy.sizes[w3c_slidy.size_index]) { - this.body.style.fontSize = - w3c_slidy.sizes[w3c_slidy.size_index]; - } - w3c_slidy.last_width = width; - w3c_slidy.last_height = height; - // force reflow to work around Mozilla bug - if (w3c_slidy.ns_pos) { - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.show_slide(slide); - } - // force correct positioning of toolbar - w3c_slidy.refresh_toolbar(200); - } - }, - scrolled: function () { - if (w3c_slidy.toolbar && !w3c_slidy.ns_pos) { - w3c_slidy.hack_offset = w3c_slidy.scroll_x_offset(); - // hide toolbar - w3c_slidy.toolbar.style.display = "none"; - // make it reappear later - if (w3c_slidy.scrollhack == 0 && !w3c_slidy.view_all) { - setTimeout(function () {w3c_slidy.show_toolbar(); }, 1000); - w3c_slidy.scrollhack = 1; - } - } - }, - hide_toolbar: function () { - w3c_slidy.add_class(w3c_slidy.toolbar, "hidden"); - window.focus(); - }, - refresh_toolbar: function (interval) { - if (!w3c_slidy.ns_pos) { - w3c_slidy.hide_toolbar(); - setTimeout(function () { - w3c_slidy.show_toolbar(); - }, interval); - } - }, - // restores toolbar after short delay - show_toolbar: function () { - if (w3c_slidy.want_toolbar) { - w3c_slidy.toolbar.style.display = "block"; - - if (!w3c_slidy.ns_pos) { - // adjust position to allow for scrolling - var xoffset = w3c_slidy.scroll_x_offset(); - w3c_slidy.toolbar.style.left = xoffset; - w3c_slidy.toolbar.style.right = xoffset; - w3c_slidy.toolbar.style.bottom = 0; //bottom; - } - w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden"); - } - w3c_slidy.scrollhack = 0; - // set the keyboard focus to the help link on the - // toolbar to ensure that document has the focus - // IE doesn't always work with window.focus() - // and this hack has benefit of Enter for help - try { - if (!w3c_slidy.opera) { - w3c_slidy.set_focus(); - } - } catch (e) { - } - }, - // invoked via F key - toggle_toolbar: function () { - if (!w3c_slidy.view_all) { - if (w3c_slidy.has_class(w3c_slidy.toolbar, "hidden")) { - w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden") - w3c_slidy.want_toolbar = 1; - } else { - w3c_slidy.add_class(w3c_slidy.toolbar, "hidden") - w3c_slidy.want_toolbar = 0; - } - } - }, - scroll_x_offset: function () { - if (window.pageXOffset) { - return self.pageXOffset; - } - if (document.documentElement && - document.documentElement.scrollLeft) { - return document.documentElement.scrollLeft; - } - if (this.body) { - return this.body.scrollLeft; - } - return 0; - }, - scroll_y_offset: function () { - if (window.pageYOffset) { - return self.pageYOffset; - } - if (document.documentElement && - document.documentElement.scrollTop) { - return document.documentElement.scrollTop; - } - if (this.body) { - return this.body.scrollTop; - } - return 0; - }, - // looking for a way to determine height of slide content - // the slide itself is set to the height of the window - optimize_font_size: function () { - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - //var dh = documentHeight(); //getDocHeight(document); - var dh = slide.scrollHeight; - var wh = getWindowHeight(); - var u = 100 * dh / wh; - alert("window utilization = " + u + "% (doc " - + dh + " win " + wh + ")"); - }, - // from document object - get_doc_height: function (doc) { - if (!doc) { - doc = document; - } - if (doc && doc.body && doc.body.offsetHeight) { - return doc.body.offsetHeight; // ns/gecko syntax - } - if (doc && doc.body && doc.body.scrollHeight) { - return doc.body.scrollHeight; - } - alert("couldn't determine document height"); - }, - get_window_height: function () { - return window.innerHeight; - }, - document_height: function () { - var sh, oh; - sh = this.body.scrollHeight; - oh = this.body.offsetHeight; - if (sh && oh) { - return (sh > oh ? sh : oh); - } - // no idea! - return 0; - }, - smaller: function () { - if (w3c_slidy.size_index > 0) { - --w3c_slidy.size_index; - } - w3c_slidy.toolbar.style.display = "none"; - this.body.style.fontSize = w3c_slidy.sizes[w3c_slidy.size_index]; - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.show_slide(slide); - setTimeout(function () { - w3c_slidy.show_toolbar(); - }, 50); - }, - bigger: function () { - if (w3c_slidy.size_index < w3c_slidy.sizes.length - 1) { - ++w3c_slidy.size_index; - } - w3c_slidy.toolbar.style.display = "none"; - this.body.style.fontSize = w3c_slidy.sizes[w3c_slidy.size_index]; - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - w3c_slidy.hide_slide(slide); - w3c_slidy.show_slide(slide); - setTimeout(function () { - w3c_slidy.show_toolbar(); - }, 50); - }, - // enables cross browser use of relative width/height - // on object elements for use with SVG and Flash media - // with thanks to Ivan Herman for the suggestion - adjust_object_dimensions: function (width, height) { - for ( var i = 0; i < w3c_slidy.objects.length; i++ ) - { - var obj = this.objects[i]; - var mimeType = obj.getAttribute("type"); - if (mimeType == "image/svg+xml" || - mimeType == "application/x-shockwave-flash") { - if ( !obj.initialWidth ) { - obj.initialWidth = obj.getAttribute("width"); - } - if ( !obj.initialHeight ) { - obj.initialHeight = obj.getAttribute("height"); - } - if ( obj.initialWidth && obj.initialWidth.charAt( - obj.initialWidth.length-1) == "%" ) { - var w = parseInt(obj.initialWidth.slice(0, obj.initialWidth.length-1)); - var newW = width * (w/100.0); - obj.setAttribute("width",newW); - } - if ( obj.initialHeight && obj.initialHeight.charAt( - obj.initialHeight.length - 1) == "%" ) { - var h = parseInt(obj.initialHeight.slice(0, - obj.initialHeight.length-1)); - var newH = height * (h/100.0); - obj.setAttribute("height", newH); - } - } - } - }, - // needed for Opera to inhibit default behavior - // since Opera delivers keyPress even if keyDown - // was cancelled - key_press: function (event) { - if (!event) { - event = window.event; - } - if (!w3c_slidy.key_wanted) { - return w3c_slidy.cancel(event); - } - return true; - }, - // See e.g. http://www.quirksmode.org/js/events/keys.html for keycodes - key_down: function (event) { - var key, target, tag; - w3c_slidy.key_wanted = true; - if (!event) { - event = window.event; - } - // kludge around NS/IE differences - if (window.event) { - key = window.event.keyCode; - target = window.event.srcElement; - } else if (event.which) { - key = event.which; - target = event.target; - } else { - return true; // Yikes! unknown browser - } - // ignore event if key value is zero - // as for alt on Opera and Konqueror - if (!key) { - return true; - } - // avoid interfering with keystroke - // behavior for non-slidy chrome elements - if (!w3c_slidy.slidy_chrome(target) && - w3c_slidy.special_element(target)) { - return true; - } - // check for concurrent control/command/alt key - // but are these only present on mouse events? - if (event.ctrlKey || event.altKey || event.metaKey) { - return true; - } - // dismiss table of contents if visible - if (w3c_slidy.is_shown_toc() && key != 9 && key != 16 && - key != 38 && key != 40) { - w3c_slidy.hide_table_of_contents(true); - if (key == 27 || key == 84 || key == 67) { - return w3c_slidy.cancel(event); - } - } - if (key == 34) {// Page Down - if (w3c_slidy.view_all) { - return true; - } - w3c_slidy.next_slide(false); - return w3c_slidy.cancel(event); - } else if (key == 33) { // Page Up - if (w3c_slidy.view_all) { - return true; - } - w3c_slidy.previous_slide(false); - return w3c_slidy.cancel(event); - } else if (key == 32) { // space bar - w3c_slidy.next_slide(true); - return w3c_slidy.cancel(event); - } else if (key == 37) { // Left arrow - w3c_slidy.previous_slide(!event.shiftKey); - return w3c_slidy.cancel(event); - } else if (key == 36) { // Home - w3c_slidy.first_slide(); - return w3c_slidy.cancel(event); - } else if (key == 35) { // End - w3c_slidy.last_slide(); - return w3c_slidy.cancel(event); - } else if (key == 39) { // Right arrow - w3c_slidy.next_slide(!event.shiftKey); - return w3c_slidy.cancel(event); - } else if (key == 13) {// Enter - if (w3c_slidy.outline) { - if (w3c_slidy.outline.visible) { - w3c_slidy.fold(w3c_slidy.outline); - } else { - w3c_slidy.unfold(w3c_slidy.outline); - } - return w3c_slidy.cancel(event); - } - } else if (key == 188) { // < for smaller fonts - w3c_slidy.smaller(); - return w3c_slidy.cancel(event); - } else if (key == 190) { // > for larger fonts - w3c_slidy.bigger(); - return w3c_slidy.cancel(event); - } else if (key == 189 || key == 109) { // - for smaller fonts - w3c_slidy.smaller(); - return w3c_slidy.cancel(event); - } else if (key == 187 || key == 191 || key == 107) { - // = + for larger fonts - w3c_slidy.bigger(); - return w3c_slidy.cancel(event); - } else if (key == 83) { // S for smaller fonts - w3c_slidy.smaller(); - return w3c_slidy.cancel(event); - } else if (key == 66) { // B for larger fonts - w3c_slidy.bigger(); - return w3c_slidy.cancel(event); - } else if (key == 90) { // Z for last slide - w3c_slidy.last_slide(); - return w3c_slidy.cancel(event); - } else if (key == 70) { // F for toggle toolbar - w3c_slidy.toggle_toolbar(); - return w3c_slidy.cancel(event); - } else if (key == 65) { // A for toggle view single/all slides - w3c_slidy.toggle_view(); - return w3c_slidy.cancel(event); - } else if (key == 75) { // toggle action of left click for next page - w3c_slidy.mouse_click_enabled = !w3c_slidy.mouse_click_enabled; - var alert_msg = (w3c_slidy.mouse_click_enabled ? - "enabled" : "disabled") + " mouse click advance"; - alert(w3c_slidy.localize(alert_msg)); - return w3c_slidy.cancel(event); - } else if (key == 84 || key == 67) { // T or C for table of contents - if (w3c_slidy.toc) { - w3c_slidy.toggle_table_of_contents(); - } - return w3c_slidy.cancel(event); - } else if (key == 72) {// H for help - window.location = w3c_slidy.help_page; - return w3c_slidy.cancel(event); - } else if (key == 85 && w3c_slidy.up_link) { //U for up link - window.location = w3c_slidy.up_link; - return w3c_slidy.cancel(event); - } - return true; - }, - // safe for both text/html and application/xhtml+xml - create_element: function (name) { - if (this.xhtml && (typeof document.createElementNS != 'undefined')) { - return document.createElementNS("http://www.w3.org/1999/xhtml", name); - } - return document.createElement(name); - }, - get_element_style: function (elem, IEStyleProp, CSSStyleProp) { - if (elem.currentStyle) { - return elem.currentStyle[IEStyleProp]; - } else if (window.getComputedStyle) { - var compStyle = window.getComputedStyle(elem, ""); - return compStyle.getPropertyValue(CSSStyleProp); - } - return ""; - }, - // the string str is a whitespace separated list of tokens - // test if str contains a particular token, e.g. "slide" - has_token: function (str, token) { - if (str) { - // define pattern as regular expression - var pattern = /\w+/g; - // check for matches - // place result in array - var result = str.match(pattern); - // now check if desired token is present - for (var i = 0; i < result.length; i++) { - if (result[i] == token) { - return true; - } - } - } - return false; - }, - get_class_list: function (element) { - if (typeof element.className != 'undefined') { - return element.className; - } - return element.getAttribute("class"); - }, - has_class: function (element, name) { - if (element.nodeType != 1) { - return false; - } - var regexp = new RegExp("(^| )" + name + "\W*"); - if (typeof element.className != 'undefined') { - return regexp.test(element.className); - } - return regexp.test(element.getAttribute("class")); - }, - remove_class: function (element, name) { - var regexp = new RegExp("(^| )" + name + "\W*"); - var clsval = ""; - if (typeof element.className != 'undefined') { - clsval = element.className; - if (clsval) { - clsval = clsval.replace(regexp, ""); - element.className = clsval; - } - } else { - clsval = element.getAttribute("class"); - if (clsval) { - clsval = clsval.replace(regexp, ""); - element.setAttribute("class", clsval); - } - } - }, - add_class: function (element, name) { - if (!this.has_class(element, name)) { - if (typeof element.className != 'undefined') { - element.className += " " + name; - } else { - var clsval = element.getAttribute("class"); - clsval = clsval ? clsval + " " + name : name; - element.setAttribute("class", clsval); - } - } - }, - // HTML elements that can be used with class="incremental" - // note that you can also put the class on containers like - // up, ol, dl, and div to make their contents appear - // incrementally. Upper case is used since this is what - // browsers report for HTML node names (text/html). - incremental_elements: null, - okay_for_incremental: function (name) { - if (!this.incremental_elements) { - var inclist = new Array(); - inclist["p"] = true; - inclist["pre"] = true; - inclist["li"] = true; - inclist["blockquote"] = true; - inclist["dt"] = true; - inclist["dd"] = true; - inclist["h2"] = true; - inclist["h3"] = true; - inclist["h4"] = true; - inclist["h5"] = true; - inclist["h6"] = true; - inclist["span"] = true; - inclist["address"] = true; - inclist["table"] = true; - inclist["tr"] = true; - inclist["th"] = true; - inclist["td"] = true; - inclist["img"] = true; - inclist["object"] = true; - this.incremental_elements = inclist; - } - return this.incremental_elements[name.toLowerCase()]; - }, - next_incremental_item: function (node) { - var br = this.is_xhtml ? "br" : "BR"; - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - for (;;) { - node = w3c_slidy.next_node(slide, node); - if (node == null || node.parentNode == null) { - break; - } - if (node.nodeType == 1) { // ELEMENT - if (node.nodeName == br) { - continue; - } - if (w3c_slidy.has_class(node, "incremental") - && w3c_slidy.okay_for_incremental(node.nodeName)) { - return node; - } - if (w3c_slidy.has_class(node.parentNode, "incremental") - && !w3c_slidy.has_class(node, "non-incremental")) { - return node; - } - } - } - return node; - }, - previous_incremental_item: function (node) { - var br = this.is_xhtml ? "br" : "BR"; - var slide = w3c_slidy.slides[w3c_slidy.slide_number]; - for (;;) { - node = w3c_slidy.previous_node(slide, node); - if (node == null || node.parentNode == null) { - break; - } - if (node.nodeType == 1) { - if (node.nodeName == br) { - continue; - } - if (w3c_slidy.has_class(node, "incremental") - && w3c_slidy.okay_for_incremental(node.nodeName)) { - return node; - } - if (w3c_slidy.has_class(node.parentNode, "incremental") - && !w3c_slidy.has_class(node, "non-incremental")) { - return node; - } - } - } - return node; - }, - // set visibility for all elements on current slide with - // a parent element with attribute class="incremental" - set_visibility_all_incremental: function (value) { - var node = this.next_incremental_item(null); - if (value == "hidden") { - while (node) { - w3c_slidy.add_class(node, "invisible"); - node = w3c_slidy.next_incremental_item(node); - } - } else {// value == "visible" - while (node) { - w3c_slidy.remove_class(node, "invisible"); - node = w3c_slidy.next_incremental_item(node); - } - } - }, - // reveal the next hidden item on the slide - // node is null or the node that was last revealed - reveal_next_item: function (node) { - node = w3c_slidy.next_incremental_item(node); - if (node && node.nodeType == 1) { // an element - w3c_slidy.remove_class(node, "invisible"); - } - return node; - }, - // exact inverse of revealNextItem(node) - hide_previous_item: function (node) { - if (node && node.nodeType == 1) { // an element - w3c_slidy.add_class(node, "invisible"); - } - return this.previous_incremental_item(node); - }, - // left to right traversal of root's content - next_node: function (root, node) { - if (node == null) { - return root.firstChild; - } - if (node.firstChild) { - return node.firstChild; - } - if (node.nextSibling) { - return node.nextSibling; - } - for (;;) { - node = node.parentNode; - if (!node || node == root) { - break; - } - if (node && node.nextSibling) { - return node.nextSibling; - } - } - return null; - }, - // right to left traversal of root's content - previous_node: function (root, node) { - if (node == null) { - node = root.lastChild; - if (node) { - while (node.lastChild) { - node = node.lastChild; - } - } - return node; - } - if (node.previousSibling) { - node = node.previousSibling; - while (node.lastChild) - node = node.lastChild; - return node; - } - if (node.parentNode != root) { - return node.parentNode; - } - return null; - }, - previous_sibling_element: function (el) { - el = el.previousSibling; - while (el && el.nodeType != 1) { - el = el.previousSibling; - } - return el; - }, - next_sibling_element: function (el) { - el = el.nextSibling; - while (el && el.nodeType != 1) { - el = el.nextSibling; - } - return el; - }, - first_child_element: function (el) { - var node; - for (node = el.firstChild; node; node = node.nextSibling) { - if (node.nodeType == 1) { - break; - } - } - return node; - }, - first_tag: function (element, tag) { - var node; - if (!this.is_xhtml) { - tag = tag.toUpperCase(); - } - for (node = element.firstChild; node; node = node.nextSibling) { - if (node.nodeType == 1 && node.nodeName == tag) { - break; - } - } - return node; - }, - hide_selection: function () { - if (window.getSelection) {// Firefox, Chromium, Safari, Opera - var selection = window.getSelection(); - if (selection.rangeCount > 0) { - var range = selection.getRangeAt(0); - range.collapse (false); - } - } else {// Internet Explorer - var textRange = document.selection.createRange (); - textRange.collapse (false); - } - }, - get_selected_text: function () { - try { - if (window.getSelection) { - return window.getSelection().toString(); - } - if (document.getSelection) { - return document.getSelection().toString(); - } - if (document.selection) { - return document.selection.createRange().text; - } - } catch (e) { - } - return ""; - }, - // make note of length of selected text - // as this evaluates to zero in click event - mouse_button_up: function (e) { - w3c_slidy.selected_text_len = w3c_slidy.get_selected_text().length; - }, - mouse_button_down: function (e) { - w3c_slidy.selected_text_len = w3c_slidy.get_selected_text().length; - w3c_slidy.mouse_x = e.clientX; - w3c_slidy.mouse_y = e.clientY; - }, - // right mouse button click is reserved for context menus - // it is more reliable to detect rightclick than leftclick - mouse_button_click: function (e) { - if (!e) { - var e = window.event; - } - if (Math.abs(e.clientX -w3c_slidy.mouse_x) + - Math.abs(e.clientY -w3c_slidy.mouse_y) > 10) { - return true; - } - if (w3c_slidy.selected_text_len > 0) { - return true; - } - var rightclick = false; - var leftclick = false; - var middleclick = false; - var target; - if (!e) { - var e = window.event; - } - if (e.target) { - target = e.target; - } else if (e.srcElement) { - target = e.srcElement; - } - // work around Safari bug - if (target.nodeType == 3) { - target = target.parentNode; - } - if (e.which) { // all browsers except IE - leftclick = (e.which == 1); - middleclick = (e.which == 2); - rightclick = (e.which == 3); - } else if (e.button) { - // Konqueror gives 1 for left, 4 for middle - // IE6 gives 0 for left and not 1 as I expected - if (e.button == 4) - middleclick = true; - // all browsers agree on 2 for right button - rightclick = (e.button == 2); - } else { - leftclick = true; - } - if (w3c_slidy.selected_text_len > 0) { - w3c_slidy.stop_propagation(e); - e.cancel = true; - e.returnValue = false; - return false; - } - // dismiss table of contents - w3c_slidy.hide_table_of_contents(false); - // check if target is something that probably want's clicks - // e.g. a, embed, object, input, textarea, select, option - var tag = target.nodeName.toLowerCase(); - if (w3c_slidy.mouse_click_enabled && leftclick && - !w3c_slidy.special_element(target) && - !target.onclick) { - w3c_slidy.next_slide(true); - w3c_slidy.stop_propagation(e); - e.cancel = true; - e.returnValue = false; - return false; - } - return true; - }, - special_element: function (element) { - if (this.has_class(element, "non-interactive")) { - return false; - } - var tag = element.nodeName.toLowerCase(); - return element.onkeydown || - element.onclick || - tag == "a" || - tag == "embed" || - tag == "object" || - tag == "video" || - tag == "audio" || - tag == "svg" || - tag == "canvas" || - tag == "input" || - tag == "textarea" || - tag == "select" || - tag == "option"; - }, - slidy_chrome: function (el) { - while (el) { - if (el == w3c_slidy.toc || - el == w3c_slidy.toolbar || - w3c_slidy.has_class(el, "outline")) { - return true; - } - el = el.parentNode; - } - return false; - }, - get_key: function (e) { - var key; - // kludge around NS/IE differences - if (typeof window.event != "undefined") { - key = window.event.keyCode; - } else if (e.which) { - key = e.which; - } - return key; - }, - get_target: function (e) { - var target; - if (!e) { - e = window.event; - } - if (e.target) { - target = e.target; - } else if (e.srcElement) { - target = e.srcElement; - } - if (target.nodeType != 1) { - target = target.parentNode; - } - return target; - }, - // does display property provide correct defaults? - is_block: function (elem) { - var tag = elem.nodeName.toLowerCase(); - return tag == "ol" || tag == "ul" || tag == "p" || tag == "dl" || - tag == "li" || tag == "table" || tag == "pre" || - tag == "h1" || tag == "h2" || tag == "h3" || - tag == "h4" || tag == "h5" || tag == "h6" || - tag == "blockquote" || tag == "address"; - }, - add_listener: function (element, event, handler) { - if (window.addEventListener) { - element.addEventListener(event, handler, false); - } else { - element.attachEvent("on" + event, handler); - } - }, - // used to prevent event propagation from field controls - stop_propagation: function (event) { - event = event ? event : window.event; - event.cancelBubble = true; // for IE - if (event.stopPropagation) { - event.stopPropagation(); - } - return true; - }, - cancel: function (event) { - if (event) { - event.cancel = true; - event.returnValue = false; - if (event.preventDefault) { - event.preventDefault(); - } - } - w3c_slidy.key_wanted = false; - return false; - }, - // for each language define an associative array - // and also the help text which is longer - strings_es: { - "slide":"pág.", - "help?":"Ayuda", - "contents?":"Índice", - "table of contents":"tabla de contenidos", - "Table of Contents":"Tabla de Contenidos", - "restart presentation":"Reiniciar presentación", - "restart?":"Inicio" - }, - help_es: - "Utilice el ratón, barra espaciadora, teclas Izda/Dcha, " + - "o Re pág y Av pág. Use S y B para cambiar el tamaño de fuente.", - strings_ca: { - "slide":"pàg..", - "help?":"Ajuda", - "contents?":"Índex", - "table of contents":"taula de continguts", - "Table of Contents":"Taula de Continguts", - "restart presentation":"Reiniciar presentació", - "restart?":"Inici" - }, - help_ca: - "Utilitzi el ratolí, barra espaiadora, tecles Esq./Dta. " + - "o Re pàg y Av pàg. Usi S i B per canviar grandària de font.", - strings_cs: { - "slide":"snímek", - "help?":"nápověda", - "contents?":"obsah", - "table of contents":"obsah prezentace", - "Table of Contents":"Obsah prezentace", - "restart presentation":"znovu spustit prezentaci", - "restart?":"restart" - }, - help_cs: - "Prezentaci můžete procházet pomocí kliknutí myši, mezerníku, " + - "šipek vlevo a vpravo nebo kláves PageUp a PageDown. Písmo se " + - "dá zvětšit a zmenšit pomocí kláves B a S.", - strings_nl: { - "slide":"pagina", - "help?":"Help?", - "contents?":"Inhoud?", - "table of contents":"inhoudsopgave", - "Table of Contents":"Inhoudsopgave", - "restart presentation":"herstart presentatie", - "restart?":"Herstart?" - }, - help_nl: - "Navigeer d.m.v. het muis, spatiebar, Links/Rechts toetsen, " + - "of PgUp en PgDn. Gebruik S en B om de karaktergrootte te veranderen.", - strings_de: { - "slide":"Seite", - "help?":"Hilfe", - "contents?":"Übersicht", - "table of contents":"Inhaltsverzeichnis", - "Table of Contents":"Inhaltsverzeichnis", - "restart presentation":"Präsentation neu starten", - "restart?":"Neustart" - }, - help_de: - "Benutzen Sie die Maus, Leerschlag, die Cursortasten links/rechts " + - "oder Page up/Page Down zum Wechseln der Seiten und S und B für die " + - "Schriftgrösse.", - strings_pl: { - "slide":"slajd", - "help?":"pomoc?", - "contents?":"spis treści?", - "table of contents":"spis treści", - "Table of Contents":"Spis Treści", - "restart presentation":"Restartuj prezentację", - "restart?":"restart?" - }, - help_pl: - "Zmieniaj slajdy klikając myszą, naciskając spację, strzałki " + - "lewo/prawo lub PgUp / PgDn. Użyj klawiszy S i B, aby zmienić " + - "rozmiar czczionki.", - strings_fr: { - "slide":"page", - "help?":"Aide", - "contents?":"Index", - "table of contents":"table des matières", - "Table of Contents":"Table des matières", - "restart presentation":"Recommencer l'exposé", - "restart?":"Début" - }, - help_fr: - "Naviguez avec la souris, la barre d'espace, les flèches " + - "gauche/droite ou les touches Pg Up, Pg Dn. Utilisez " + - "les touches S et B pour modifier la taille de la police.", - strings_hu: { - "slide":"oldal", - "help?":"segítség", - "contents?":"tartalom", - "table of contents":"tartalomjegyzék", - "Table of Contents":"Tartalomjegyzék", - "restart presentation":"bemutató újraindítása", - "restart?":"újraindítás" - }, - help_hu: - "Az oldalak közti lépkedéshez kattintson az egérrel, vagy " + - "használja a szóköz, a bal, vagy a jobb nyíl, illetve a Page Down, " + - "Page Up billentyűket. Az S és a B billentyűkkel változtathatja " + - "a szöveg méretét.", - strings_it: { - "slide":"pag.", - "help?":"Aiuto", - "contents?":"Indice", - "table of contents":"indice", - "Table of Contents":"Indice", - "restart presentation":"Ricominciare la presentazione", - "restart?":"Inizio" - }, - help_it: - "Navigare con mouse, barra spazio, frecce sinistra/destra o " + - "PgUp e PgDn. Usare S e B per cambiare la dimensione dei caratteri.", - strings_el: { - "slide":"σελίδα", - "help?":"βοήθεια;", - "contents?":"περιεχόμενα;", - "table of contents":"πίνακας περιεχομένων", - "Table of Contents":"Πίνακας Περιεχομένων", - "restart presentation":"επανεκκίνηση παρουσίασης", - "restart?":"επανεκκίνηση;" - }, - help_el: - "Πλοηγηθείτε με το κλίκ του ποντικιού, το space, τα βέλη " + - "αριστερά/δεξιά, ή Page Up και Page Down. Χρησιμοποιήστε τα" + - " πλήκτρα S και B για να αλλάξετε το μέγεθος της γραμματοσειράς.", - strings_ja: { - "slide":"スライド", - "help?":"ヘルプ", - "contents?":"目次", - "table of contents":"目次を表示", - "Table of Contents":"目次", - "restart presentation":"最初から再生", - "restart?":"最初から" - }, - help_ja: - "マウス左クリック ・ スペース ・ 左右キー " + - "または Page Up ・ Page Downで操作, S ・ Bでフォントサイズ変更", - strings_zh: { - "slide":"幻灯片", - "help?":"帮助?", - "contents?":"内容?", - "table of contents":"目录", - "Table of Contents":"目录", - "restart presentation":"重新启动展示", - "restart?":"重新启动?" - }, - help_zh: - "用鼠标点击, 空格条, 左右箭头, Pg Up 和 Pg Dn 导航. " + - "用 S, B 改变字体大小.", - strings_ru: { - "slide":"слайд", - "help?":"помощь?", - "contents?":"содержание?", - "table of contents":"оглавление", - "Table of Contents":"Оглавление", - "restart presentation":"перезапустить презентацию", - "restart?":"перезапуск?" - }, - help_ru: - "Перемещайтесь кликая мышкой, используя клавишу пробел, стрелки" + - "влево/вправо или Pg Up и Pg Dn. Клавиши S и B меняют размер шрифта.", - strings_sv: { - "slide":"sida", - "help?":"hjälp", - "contents?":"innehåll", - "table of contents":"innehållsförteckning", - "Table of Contents":"Innehållsförteckning", - "restart presentation":"visa presentationen från början", - "restart?":"börja om" - }, - help_sv: - "Bläddra med ett klick med vänstra musknappen, mellanslagstangenten, " + - "vänster- och högerpiltangenterna eller tangenterna Pg Up, Pg Dn. " + - "Använd tangenterna S och B för att ändra textens storlek.", - strings: { }, - localize: function (src) { - if (src == "") { - return src; - } - // try full language code, e.g. en-US - var s, lookup = w3c_slidy.strings[w3c_slidy.lang]; - if (lookup) { - s = lookup[src]; - if (s) { - return s; - } - } - // strip country code suffix, e.g. - // try en if undefined for en-US - var lg = w3c_slidy.lang.split("-"); - if (lg.length > 1) { - lookup = w3c_slidy.strings[lg[0]]; - if (lookup) { - s = lookup[src]; - if (s) { - return s; - } - } - } - // otherwise string as is - return src; - }, - init_localization: function () { - var i18n = w3c_slidy; - var help_text = w3c_slidy.help_text; - // each such language array is declared in the localize array - // this is used as in w3c_slidy.localize("foo"); - this.strings = { - "es":this.strings_es, - "ca":this.strings_ca, - "cs":this.strings_cs, - "nl":this.strings_nl, - "de":this.strings_de, - "pl":this.strings_pl, - "fr":this.strings_fr, - "hu":this.strings_hu, - "it":this.strings_it, - "el":this.strings_el, - "jp":this.strings_ja, - "zh":this.strings_zh, - "ru":this.strings_ru, - "sv":this.strings_sv - }, - i18n.strings_es[help_text] = i18n.help_es; - i18n.strings_ca[help_text] = i18n.help_ca; - i18n.strings_cs[help_text] = i18n.help_cs; - i18n.strings_nl[help_text] = i18n.help_nl; - i18n.strings_de[help_text] = i18n.help_de; - i18n.strings_pl[help_text] = i18n.help_pl; - i18n.strings_fr[help_text] = i18n.help_fr; - i18n.strings_hu[help_text] = i18n.help_hu; - i18n.strings_it[help_text] = i18n.help_it; - i18n.strings_el[help_text] = i18n.help_el; - i18n.strings_ja[help_text] = i18n.help_ja; - i18n.strings_zh[help_text] = i18n.help_zh; - i18n.strings_ru[help_text] = i18n.help_ru; - i18n.strings_sv[help_text] = i18n.help_sv; - w3c_slidy.lang = document.body.parentNode.getAttribute("lang"); - if (!w3c_slidy.lang) { - w3c_slidy.lang = document.body.parentNode.getAttribute("xml:lang"); - } - if (!w3c_slidy.lang) { - w3c_slidy.lang = "en"; - } - } -}; -// attach event listeners for initialization -w3c_slidy.set_up(); -// hide the slides as soon as body element is available -// to reduce annoying screen mess before the onload event -setTimeout(w3c_slidy.hide_slides, 50); diff --git a/src/views/layouts/WebLayout.php b/src/views/layouts/WebLayout.php index 445899f13..5f48b2a41 100755 --- a/src/views/layouts/WebLayout.php +++ b/src/views/layouts/WebLayout.php @@ -319,6 +319,9 @@ class WebLayout extends Layout </head><?php $data['MOBILE'] = (!empty($_SERVER["MOBILE"])) ? 'mobile': ''; flush(); + $is_presentation = !empty($data["HEAD"]['page_type']) && + $data["HEAD"]['page_type'] == 'presentation' && + !empty($data['MODE']) && $data['MODE'] == 'read'; ?> <body class="html-<?= $data['LOCALE_DIR'] ?> html-<?= $data['WRITING_MODE'] . ' ' . $data['MOBILE'] ?>" ><?php @@ -327,13 +330,15 @@ class WebLayout extends Layout $body_view = ($view_class == 'MediadetailView') ? " media-detail-body " : (($view_class == 'SearchView') ? " search-body " : ""); - ?> - <div id="body-container" class="body-container <?= - $body_view ?>"> - <div id="message" ></div><?php - $this->view->renderView($data); - if (C\QUERY_STATISTICS && (!isset($this->presentation) || - !$this->presentation)) { ?> + if ($is_presentation) { + e($data['PAGE']); + } else { + ?><div id="body-container" class="body-container <?= + $body_view ?>"> + <div id="message" ></div><?php + $this->view->renderView($data); + } + if (C\QUERY_STATISTICS && !$is_presentation) { ?> <div class="query-statistics"><?php e("<h1>" . tl('web_layout_query_statistics')."</h1>"); e("<div><b>".