<!--
	NOTE: You need to build the notes plugin after making changes to this file.
-->
<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>reveal.js - Speaker View</title>

    <style>
      body {
        font-family: Helvetica;
        font-size: 18px;
      }

      #current-slide,
      #upcoming-slide,
      #speaker-controls {
        padding: 6px;
        box-sizing: border-box;
        -moz-box-sizing: border-box;
      }

      #current-slide iframe,
      #upcoming-slide iframe {
        width: 100%;
        height: 100%;
        border: 1px solid #ddd;
      }

      #current-slide .label,
      #upcoming-slide .label {
        position: absolute;
        top: 10px;
        left: 10px;
        z-index: 2;
      }

      #connection-status {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 20;
        padding: 30% 20% 20% 20%;
        font-size: 18px;
        color: #222;
        background: #fff;
        text-align: center;
        box-sizing: border-box;
        line-height: 1.4;
      }

      .overlay-element {
        height: 34px;
        line-height: 34px;
        padding: 0 10px;
        text-shadow: none;
        background: rgba(220, 220, 220, 0.8);
        color: #222;
        font-size: 14px;
      }

      .overlay-element.interactive:hover {
        background: rgba(220, 220, 220, 1);
      }

      #current-slide {
        position: absolute;
        width: 60%;
        height: 100%;
        top: 0;
        left: 0;
        padding-right: 0;
      }

      #upcoming-slide {
        position: absolute;
        width: 40%;
        height: 40%;
        right: 0;
        top: 0;
      }

      /* Speaker controls */
      #speaker-controls {
        position: absolute;
        top: 40%;
        right: 0;
        width: 40%;
        height: 60%;
        overflow: auto;
        font-size: 18px;
      }

      .speaker-controls-time.hidden,
      .speaker-controls-notes.hidden {
        display: none;
      }

      .speaker-controls-time .label,
      .speaker-controls-pace .label,
      .speaker-controls-notes .label {
        text-transform: uppercase;
        font-weight: normal;
        font-size: 0.66em;
        color: #666;
        margin: 0;
      }

      .speaker-controls-time,
      .speaker-controls-pace {
        border-bottom: 1px solid rgba(200, 200, 200, 0.5);
        margin-bottom: 10px;
        padding: 10px 16px;
        padding-bottom: 20px;
        cursor: pointer;
      }

      .speaker-controls-time .reset-button {
        opacity: 0;
        float: right;
        color: #666;
        text-decoration: none;
      }
      .speaker-controls-time:hover .reset-button {
        opacity: 1;
      }

      .speaker-controls-time .timer,
      .speaker-controls-time .clock {
        width: 50%;
      }

      .speaker-controls-time .timer,
      .speaker-controls-time .clock,
      .speaker-controls-time .pacing .hours-value,
      .speaker-controls-time .pacing .minutes-value,
      .speaker-controls-time .pacing .seconds-value {
        font-size: 1.9em;
      }

      .speaker-controls-time .timer {
        float: left;
      }

      .speaker-controls-time .clock {
        float: right;
        text-align: right;
      }

      .speaker-controls-time span.mute {
        opacity: 0.3;
      }

      .speaker-controls-time .pacing-title {
        margin-top: 5px;
      }

      .speaker-controls-time .pacing.ahead {
        color: blue;
      }

      .speaker-controls-time .pacing.on-track {
        color: green;
      }

      .speaker-controls-time .pacing.behind {
        color: red;
      }

      .speaker-controls-notes {
        padding: 10px 16px;
      }

      .speaker-controls-notes .value {
        margin-top: 5px;
        line-height: 1.4;
        font-size: 1.2em;
      }

      /* Layout selector */
      #speaker-layout {
        position: absolute;
        top: 10px;
        right: 10px;
        color: #222;
        z-index: 10;
      }
      #speaker-layout select {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        border: 0;
        box-shadow: 0;
        cursor: pointer;
        opacity: 0;

        font-size: 1em;
        background-color: transparent;

        -moz-appearance: none;
        -webkit-appearance: none;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
      }

      #speaker-layout select:focus {
        outline: none;
        box-shadow: none;
      }

      .clear {
        clear: both;
      }

      /* Speaker layout: Wide */
      body[data-speaker-layout="wide"] #current-slide,
      body[data-speaker-layout="wide"] #upcoming-slide {
        width: 50%;
        height: 45%;
        padding: 6px;
      }

      body[data-speaker-layout="wide"] #current-slide {
        top: 0;
        left: 0;
      }

      body[data-speaker-layout="wide"] #upcoming-slide {
        top: 0;
        left: 50%;
      }

      body[data-speaker-layout="wide"] #speaker-controls {
        top: 45%;
        left: 0;
        width: 100%;
        height: 50%;
        font-size: 1.25em;
      }

      /* Speaker layout: Tall */
      body[data-speaker-layout="tall"] #current-slide,
      body[data-speaker-layout="tall"] #upcoming-slide {
        width: 45%;
        height: 50%;
        padding: 6px;
      }

      body[data-speaker-layout="tall"] #current-slide {
        top: 0;
        left: 0;
      }

      body[data-speaker-layout="tall"] #upcoming-slide {
        top: 50%;
        left: 0;
      }

      body[data-speaker-layout="tall"] #speaker-controls {
        padding-top: 40px;
        top: 0;
        left: 45%;
        width: 55%;
        height: 100%;
        font-size: 1.25em;
      }

      /* Speaker layout: Notes only */
      body[data-speaker-layout="notes-only"] #current-slide,
      body[data-speaker-layout="notes-only"] #upcoming-slide {
        display: none;
      }

      body[data-speaker-layout="notes-only"] #speaker-controls {
        padding-top: 40px;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        font-size: 1.25em;
      }

      @media screen and (max-width: 1080px) {
        body[data-speaker-layout="default"] #speaker-controls {
          font-size: 16px;
        }
      }

      @media screen and (max-width: 900px) {
        body[data-speaker-layout="default"] #speaker-controls {
          font-size: 14px;
        }
      }

      @media screen and (max-width: 800px) {
        body[data-speaker-layout="default"] #speaker-controls {
          font-size: 12px;
        }
      }
    </style>
  </head>

  <body>
    <div id="connection-status">Loading speaker view...</div>

    <div id="current-slide"></div>
    <div id="upcoming-slide">
      <span class="overlay-element label">Upcoming</span>
    </div>
    <div id="speaker-controls">
      <div class="speaker-controls-time">
        <h4 class="label">
          Time <span class="reset-button">Click to Reset</span>
        </h4>
        <div class="clock">
          <span class="clock-value">0:00 AM</span>
        </div>
        <div class="timer">
          <span class="hours-value">00</span
          ><span class="minutes-value">:00</span
          ><span class="seconds-value">:00</span>
        </div>
        <div class="clear"></div>

        <h4 class="label pacing-title" style="display: none">
          Pacing – Time to finish current slide
        </h4>
        <div class="pacing" style="display: none">
          <span class="hours-value">00</span
          ><span class="minutes-value">:00</span
          ><span class="seconds-value">:00</span>
        </div>
      </div>

      <div class="speaker-controls-notes hidden">
        <h4 class="label">Notes</h4>
        <div class="value"></div>
      </div>
    </div>
    <div id="speaker-layout" class="overlay-element interactive">
      <span class="speaker-layout-label"></span>
      <select class="speaker-layout-dropdown"></select>
    </div>

    <script>
      (function () {
        var notes,
          notesValue,
          currentState,
          currentSlide,
          upcomingSlide,
          layoutLabel,
          layoutDropdown,
          pendingCalls = {},
          lastRevealApiCallId = 0,
          connected = false;

        var connectionStatus = document.querySelector("#connection-status");

        var SPEAKER_LAYOUTS = {
          default: "Default",
          wide: "Wide",
          tall: "Tall",
          "notes-only": "Notes only",
        };

        setupLayout();

        let openerOrigin;

        try {
          openerOrigin = window.opener.location.origin;
        } catch (error) {
          console.warn(error);
        }

        // In order to prevent XSS, the speaker view will only run if its
        // opener has the same origin as itself
        if (window.location.origin !== openerOrigin) {
          connectionStatus.innerHTML =
            "Cross origin error.<br>The speaker window can only be opened from the same origin.";
          return;
        }

        var connectionTimeout = setTimeout(function () {
          connectionStatus.innerHTML =
            "Error connecting to main window.<br>Please try closing and reopening the speaker view.";
        }, 5000);

        window.addEventListener("message", function (event) {
          clearTimeout(connectionTimeout);
          connectionStatus.style.display = "none";

          var data = JSON.parse(event.data);

          // The overview mode is only useful to the reveal.js instance
          // where navigation occurs so we don't sync it
          if (data.state) delete data.state.overview;

          // Messages sent by the notes plugin inside of the main window
          if (data && data.namespace === "reveal-notes") {
            if (data.type === "connect") {
              handleConnectMessage(data);
            } else if (data.type === "state") {
              handleStateMessage(data);
            } else if (data.type === "return") {
              pendingCalls[data.callId](data.result);
              delete pendingCalls[data.callId];
            }
          }
          // Messages sent by the reveal.js inside of the current slide preview
          else if (data && data.namespace === "reveal") {
            if (/ready/.test(data.eventName)) {
              // Send a message back to notify that the handshake is complete
              window.opener.postMessage(
                JSON.stringify({
                  namespace: "reveal-notes",
                  type: "connected",
                }),
                "*"
              );
            } else if (
              /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test(
                data.eventName
              ) &&
              currentState !== JSON.stringify(data.state)
            ) {
              dispatchStateToMainWindow(data.state);
            }
          }
        });

        /**
         * Updates the presentation in the main window to match the state
         * of the presentation in the notes window.
         */
        const dispatchStateToMainWindow = debounce((state) => {
          window.opener.postMessage(
            JSON.stringify({ method: "setState", args: [state] }),
            "*"
          );
        }, 500);

        /**
         * Asynchronously calls the Reveal.js API of the main frame.
         */
        function callRevealApi(methodName, methodArguments, callback) {
          var callId = ++lastRevealApiCallId;
          pendingCalls[callId] = callback;
          window.opener.postMessage(
            JSON.stringify({
              namespace: "reveal-notes",
              type: "call",
              callId: callId,
              methodName: methodName,
              arguments: methodArguments,
            }),
            "*"
          );
        }

        /**
         * Called when the main window is trying to establish a
         * connection.
         */
        function handleConnectMessage(data) {
          if (connected === false) {
            connected = true;

            setupIframes(data);
            setupKeyboard();
            setupNotes();
            setupTimer();
            setupHeartbeat();
          }
        }

        /**
         * Called when the main window sends an updated state.
         */
        function handleStateMessage(data) {
          // Store the most recently set state to avoid circular loops
          // applying the same state
          currentState = JSON.stringify(data.state);

          // No need for updating the notes in case of fragment changes
          if (data.notes) {
            notes.classList.remove("hidden");
            notesValue.style.whiteSpace = data.whitespace;
            if (data.markdown) {
              notesValue.innerHTML = marked(data.notes);
            } else {
              notesValue.innerHTML = data.notes;
            }
          } else {
            notes.classList.add("hidden");
          }

          // Update the note slides
          currentSlide.contentWindow.postMessage(
            JSON.stringify({ method: "setState", args: [data.state] }),
            "*"
          );
          upcomingSlide.contentWindow.postMessage(
            JSON.stringify({ method: "setState", args: [data.state] }),
            "*"
          );
          upcomingSlide.contentWindow.postMessage(
            JSON.stringify({ method: "next" }),
            "*"
          );
        }

        // Limit to max one state update per X ms
        handleStateMessage = debounce(handleStateMessage, 200);

        /**
         * Forward keyboard events to the current slide window.
         * This enables keyboard events to work even if focus
         * isn't set on the current slide iframe.
         *
         * Block F5 default handling, it reloads and disconnects
         * the speaker notes window.
         */
        function setupKeyboard() {
          document.addEventListener("keydown", function (event) {
            if (
              event.keyCode === 116 ||
              (event.metaKey && event.keyCode === 82)
            ) {
              event.preventDefault();
              return false;
            }
            currentSlide.contentWindow.postMessage(
              JSON.stringify({ method: "triggerKey", args: [event.keyCode] }),
              "*"
            );
          });
        }

        /**
         * Creates the preview iframes.
         */
        function setupIframes(data) {
          var params = [
            "receiver",
            "progress=false",
            "history=false",
            "transition=none",
            "autoSlide=0",
            "backgroundTransition=none",
          ].join("&");

          var urlSeparator = /\?/.test(data.url) ? "&" : "?";
          var hash = "#/" + data.state.indexh + "/" + data.state.indexv;
          var currentURL =
            data.url + urlSeparator + params + "&postMessageEvents=true" + hash;
          var upcomingURL =
            data.url + urlSeparator + params + "&controls=false" + hash;

          currentSlide = document.createElement("iframe");
          currentSlide.setAttribute("width", 1280);
          currentSlide.setAttribute("height", 1024);
          currentSlide.setAttribute("src", currentURL);
          document.querySelector("#current-slide").appendChild(currentSlide);

          upcomingSlide = document.createElement("iframe");
          upcomingSlide.setAttribute("width", 640);
          upcomingSlide.setAttribute("height", 512);
          upcomingSlide.setAttribute("src", upcomingURL);
          document.querySelector("#upcoming-slide").appendChild(upcomingSlide);
        }

        /**
         * Setup the notes UI.
         */
        function setupNotes() {
          notes = document.querySelector(".speaker-controls-notes");
          notesValue = document.querySelector(".speaker-controls-notes .value");
        }

        /**
         * We send out a heartbeat at all times to ensure we can
         * reconnect with the main presentation window after reloads.
         */
        function setupHeartbeat() {
          setInterval(() => {
            window.opener.postMessage(
              JSON.stringify({ namespace: "reveal-notes", type: "heartbeat" }),
              "*"
            );
          }, 1000);
        }

        function getTimings(callback) {
          callRevealApi("getSlidesAttributes", [], function (slideAttributes) {
            callRevealApi("getConfig", [], function (config) {
              var totalTime = config.totalTime;
              var minTimePerSlide = config.minimumTimePerSlide || 0;
              var defaultTiming = config.defaultTiming;
              if (defaultTiming == null && totalTime == null) {
                callback(null);
                return;
              }
              // Setting totalTime overrides defaultTiming
              if (totalTime) {
                defaultTiming = 0;
              }
              var timings = [];
              for (var i in slideAttributes) {
                var slide = slideAttributes[i];
                var timing = defaultTiming;
                if (slide.hasOwnProperty("data-timing")) {
                  var t = slide["data-timing"];
                  timing = parseInt(t);
                  if (isNaN(timing)) {
                    console.warn(
                      "Could not parse timing '" +
                        t +
                        "' of slide " +
                        i +
                        "; using default of " +
                        defaultTiming
                    );
                    timing = defaultTiming;
                  }
                }
                timings.push(timing);
              }
              if (totalTime) {
                // After we've allocated time to individual slides, we summarize it and
                // subtract it from the total time
                var remainingTime =
                  totalTime -
                  timings.reduce(function (a, b) {
                    return a + b;
                  }, 0);
                // The remaining time is divided by the number of slides that have 0 seconds
                // allocated at the moment, giving the average time-per-slide on the remaining slides
                var remainingSlides = timings.filter(function (x) {
                  return x == 0;
                }).length;
                var timePerSlide = Math.round(
                  remainingTime / remainingSlides,
                  0
                );
                // And now we replace every zero-value timing with that average
                timings = timings.map(function (x) {
                  return x == 0 ? timePerSlide : x;
                });
              }
              var slidesUnderMinimum = timings.filter(function (x) {
                return x < minTimePerSlide;
              }).length;
              if (slidesUnderMinimum) {
                message =
                  "The pacing time for " +
                  slidesUnderMinimum +
                  " slide(s) is under the configured minimum of " +
                  minTimePerSlide +
                  " seconds. Check the data-timing attribute on individual slides, or consider increasing the totalTime or minimumTimePerSlide configuration options (or removing some slides).";
                alert(message);
              }
              callback(timings);
            });
          });
        }

        /**
         * Return the number of seconds allocated for presenting
         * all slides up to and including this one.
         */
        function getTimeAllocated(timings, callback) {
          callRevealApi("getSlidePastCount", [], function (currentSlide) {
            var allocated = 0;
            for (var i in timings.slice(0, currentSlide + 1)) {
              allocated += timings[i];
            }
            callback(allocated);
          });
        }

        /**
         * Create the timer and clock and start updating them
         * at an interval.
         */
        function setupTimer() {
          var start = new Date(),
            timeEl = document.querySelector(".speaker-controls-time"),
            clockEl = timeEl.querySelector(".clock-value"),
            hoursEl = timeEl.querySelector(".hours-value"),
            minutesEl = timeEl.querySelector(".minutes-value"),
            secondsEl = timeEl.querySelector(".seconds-value"),
            pacingTitleEl = timeEl.querySelector(".pacing-title"),
            pacingEl = timeEl.querySelector(".pacing"),
            pacingHoursEl = pacingEl.querySelector(".hours-value"),
            pacingMinutesEl = pacingEl.querySelector(".minutes-value"),
            pacingSecondsEl = pacingEl.querySelector(".seconds-value");

          var timings = null;
          getTimings(function (_timings) {
            timings = _timings;
            if (_timings !== null) {
              pacingTitleEl.style.removeProperty("display");
              pacingEl.style.removeProperty("display");
            }

            // Update once directly
            _updateTimer();

            // Then update every second
            setInterval(_updateTimer, 1000);
          });

          function _resetTimer() {
            if (timings == null) {
              start = new Date();
              _updateTimer();
            } else {
              // Reset timer to beginning of current slide
              getTimeAllocated(timings, function (slideEndTimingSeconds) {
                var slideEndTiming = slideEndTimingSeconds * 1000;
                callRevealApi("getSlidePastCount", [], function (currentSlide) {
                  var currentSlideTiming = timings[currentSlide] * 1000;
                  var previousSlidesTiming =
                    slideEndTiming - currentSlideTiming;
                  var now = new Date();
                  start = new Date(now.getTime() - previousSlidesTiming);
                  _updateTimer();
                });
              });
            }
          }

          timeEl.addEventListener("click", function () {
            _resetTimer();
            return false;
          });

          function _displayTime(hrEl, minEl, secEl, time) {
            var sign = Math.sign(time) == -1 ? "-" : "";
            time = Math.abs(Math.round(time / 1000));
            var seconds = time % 60;
            var minutes = Math.floor(time / 60) % 60;
            var hours = Math.floor(time / (60 * 60));
            hrEl.innerHTML = sign + zeroPadInteger(hours);
            if (hours == 0) {
              hrEl.classList.add("mute");
            } else {
              hrEl.classList.remove("mute");
            }
            minEl.innerHTML = ":" + zeroPadInteger(minutes);
            if (hours == 0 && minutes == 0) {
              minEl.classList.add("mute");
            } else {
              minEl.classList.remove("mute");
            }
            secEl.innerHTML = ":" + zeroPadInteger(seconds);
          }

          function _updateTimer() {
            var diff,
              hours,
              minutes,
              seconds,
              now = new Date();

            diff = now.getTime() - start.getTime();

            clockEl.innerHTML = now.toLocaleTimeString("en-US", {
              hour12: true,
              hour: "2-digit",
              minute: "2-digit",
            });
            _displayTime(hoursEl, minutesEl, secondsEl, diff);
            if (timings !== null) {
              _updatePacing(diff);
            }
          }

          function _updatePacing(diff) {
            getTimeAllocated(timings, function (slideEndTimingSeconds) {
              var slideEndTiming = slideEndTimingSeconds * 1000;

              callRevealApi("getSlidePastCount", [], function (currentSlide) {
                var currentSlideTiming = timings[currentSlide] * 1000;
                var timeLeftCurrentSlide = slideEndTiming - diff;
                if (timeLeftCurrentSlide < 0) {
                  pacingEl.className = "pacing behind";
                } else if (timeLeftCurrentSlide < currentSlideTiming) {
                  pacingEl.className = "pacing on-track";
                } else {
                  pacingEl.className = "pacing ahead";
                }
                _displayTime(
                  pacingHoursEl,
                  pacingMinutesEl,
                  pacingSecondsEl,
                  timeLeftCurrentSlide
                );
              });
            });
          }
        }

        /**
         * Sets up the speaker view layout and layout selector.
         */
        function setupLayout() {
          layoutDropdown = document.querySelector(".speaker-layout-dropdown");
          layoutLabel = document.querySelector(".speaker-layout-label");

          // Render the list of available layouts
          for (var id in SPEAKER_LAYOUTS) {
            var option = document.createElement("option");
            option.setAttribute("value", id);
            option.textContent = SPEAKER_LAYOUTS[id];
            layoutDropdown.appendChild(option);
          }

          // Monitor the dropdown for changes
          layoutDropdown.addEventListener(
            "change",
            function (event) {
              setLayout(layoutDropdown.value);
            },
            false
          );

          // Restore any currently persisted layout
          setLayout(getLayout());
        }

        /**
         * Sets a new speaker view layout. The layout is persisted
         * in local storage.
         */
        function setLayout(value) {
          var title = SPEAKER_LAYOUTS[value];

          layoutLabel.innerHTML = "Layout" + (title ? ": " + title : "");
          layoutDropdown.value = value;

          document.body.setAttribute("data-speaker-layout", value);

          // Persist locally
          if (supportsLocalStorage()) {
            window.localStorage.setItem("reveal-speaker-layout", value);
          }
        }

        /**
         * Returns the ID of the most recently set speaker layout
         * or our default layout if none has been set.
         */
        function getLayout() {
          if (supportsLocalStorage()) {
            var layout = window.localStorage.getItem("reveal-speaker-layout");
            if (layout) {
              return layout;
            }
          }

          // Default to the first record in the layouts hash
          for (var id in SPEAKER_LAYOUTS) {
            return id;
          }
        }

        function supportsLocalStorage() {
          try {
            localStorage.setItem("test", "test");
            localStorage.removeItem("test");
            return true;
          } catch (e) {
            return false;
          }
        }

        function zeroPadInteger(num) {
          var str = "00" + parseInt(num);
          return str.substring(str.length - 2);
        }

        /**
         * Limits the frequency at which a function can be called.
         */
        function debounce(fn, ms) {
          var lastTime = 0,
            timeout;

          return function () {
            var args = arguments;
            var context = this;

            clearTimeout(timeout);

            var timeSinceLastCall = Date.now() - lastTime;
            if (timeSinceLastCall > ms) {
              fn.apply(context, args);
              lastTime = Date.now();
            } else {
              timeout = setTimeout(function () {
                fn.apply(context, args);
                lastTime = Date.now();
              }, ms - timeSinceLastCall);
            }
          };
        }
      })();
    </script>
  </body>
</html>