diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88ef3da..2e5bef7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@ Changelog
------------
-
+[v2.3.0] - 2021-08-16
+---------------------
+[GitHub Release Page](https://github.com/ToranSharma/Xporcle-Extension/releases/tag/v2.3.0)
+### Added
+- Quiz Queue
+ - Button for hosts to add the quiz they are on to the queue.
+ - Draggable (for hosts) list of quizzes in the queue.
+ - Remove button for hosts to remove quizzes from the queue.
+ - Go to quiz button for hosts to visit the page of quizzes in the queue.
+ - Controls for hosts to toggle and customise interval for auto changing to
+ next quiz.
+ - Option to set default interval for auto chaning.
+ - Countdown under leaderboard displaying time left until change to next
+ quiz.
+ - Button to cancel the countdown for hosts.
+- Option to set default duration for next quiz polls.
+
[v2.2.0] - 2021-08-16
---------------------
[GitHub Release Page](https://github.com/ToranSharma/Xporcle-Extension/releases/tag/v2.2.0)
@@ -15,7 +32,6 @@ Changelog
### Changed
- Xporcle interface now scrollable if longer than available space.
-
[v2.1.0] - 2021-08-14
---------------------
[GitHub Release Page](https://github.com/ToranSharma/Xporcle-Extension/releases/tag/v2.1.0)
@@ -149,6 +165,7 @@ Changelog
- Options page popup placeholder.
[Unreleased]: https://github.com/ToranSharma/Xporcle-Extension/compare/master...develop
+[v2.3.0]: https://github.com/ToranSharma/Xporcle-Extension/compare/v2.2.0...v2.3.0
[v2.2.0]: https://github.com/ToranSharma/Xporcle-Extension/compare/v2.1.0...v2.2.0
[v2.1.0]: https://github.com/ToranSharma/Xporcle-Extension/compare/v2.0.0...v2.1.0
[v2.0.0]: https://github.com/ToranSharma/Xporcle-Extension/compare/v1.2.1...v2.0.0
diff --git a/background.js b/background.js
index 1fdf80f..3e52288 100644
--- a/background.js
+++ b/background.js
@@ -41,6 +41,8 @@ let pollData = {};
let voteData = {};
let voted = false;
let saveName = null;
+let queue = [];
+let queueInterval;
chrome.runtime.onConnect.addListener(
(port) =>
@@ -70,7 +72,9 @@ chrome.runtime.onConnect.addListener(
poll_data: pollData,
vote_data: voteData,
saveName: saveName,
- voted: voted
+ voted: voted,
+ queue: queue,
+ queue_interval: queueInterval
}
);
ws.send(JSON.stringify({type: "url_update", url: message["url"]}))
@@ -212,6 +216,8 @@ function forwardMessage(event)
if (message["success"])
{
hosts = message["hosts"];
+ queue = message["queue"];
+ queueInterval = message["queue_interval"];
}
else
{
@@ -241,7 +247,9 @@ function forwardMessage(event)
{
host = true;
urls = message["urls"];
- poll_data = message["poll_data"] ?? {}
+ poll_data = message["poll_data"] ?? {};
+ queue = message["queue"];
+ queueInterval = message["queue_interval"];
}
else if (
messageType === "users_update"
@@ -319,6 +327,11 @@ function forwardMessage(event)
{
voteData = message["vote_data"];
}
+ else if (messageType === "queue_update")
+ {
+ queue = message["queue"];
+ queueInterval = message["queue_interval"];
+ }
if (messagePort !== null)
{
@@ -340,4 +353,6 @@ function reset()
voteData = {};
saveName = null;
voted = false;
+ queue = [];
+ queueInterval = null;
}
diff --git a/manifest.json b/manifest.json
index 2ab9c3c..e530ad7 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"name" : "Xporcle",
"description" : "Adds real-time multiplayer abilities to Sporcle.com",
- "version" : "2.2.0",
+ "version" : "2.3.0",
"manifest_version" : 2,
"icons" : {
diff --git a/options.html b/options.html
index 7ca2524..4a6ea48 100644
--- a/options.html
+++ b/options.html
@@ -6,7 +6,7 @@
- Xporcle v2.2.0
+ Xporcle v2.3.0
Enable or Disable Features Here
diff --git a/options.js b/options.js
index c1809cc..814a207 100644
--- a/options.js
+++ b/options.js
@@ -101,6 +101,7 @@ function getOptions(firstLoad=false)
document.getElementById(option).checked = options[option];
break;
case "string":
+ case "number":
document.getElementById(option).value = options[option];
break;
}
@@ -120,6 +121,10 @@ function saveOptions()
{
options[element.id] = element.checked;
}
+ else if (element.type === "number")
+ {
+ options[element.id] = Number(element.value);
+ }
else
{
options[element.id] = element.value;
diff --git a/stylesheets/interface.css b/stylesheets/interface.css
index 46c9d3b..6279d3d 100644
--- a/stylesheets/interface.css
+++ b/stylesheets/interface.css
@@ -15,20 +15,6 @@
max-width: 400px;
overflow: auto;
}
- #interfaceContainer form
- {
- display: flex;
- flex-direction: column;
- }
- #interfaceContainer form input,
- #interfaceContainer form button,
- #interfaceContainer form select
- {
- width: min(100%, 10em);
- box-sizing: border-box;
- margin: 0 auto;
-
- }
.interfaceSection
{
@@ -52,6 +38,31 @@
margin: 0;
}
+#interfaceBox .toggle
+{
+ top: 0.5rem;
+}
+#interfaceBox form
+{
+ display: flex;
+ flex-direction: column;
+}
+ #interfaceBox form input,
+ #interfaceBox form button,
+ #interfaceBox form select
+ {
+ width: min(100%, 10em);
+ box-sizing: border-box;
+ margin: 0 auto;
+
+ }
+#interfaceBox #changeQuizCountdown
+{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ flex-wrap: wrap;
+}
#leaderboard,
#liveScores
@@ -328,7 +339,7 @@
margin: 0;
border: 0;
position: absolute;
- right: 2rem;
+ right: 1rem;
top: 1rem;
color: grey;
cursor: pointer;
@@ -338,6 +349,10 @@
transform: rotate(0deg);
transition: transform 0.5s;
}
+.closeButton ~ .toggle
+{
+ right: 2rem;
+}
.toggle::after
{
content: "\02228";
@@ -349,8 +364,8 @@
}
.toggle:checked ~ *
{
- display: none;
- visibility: hidden;
+ display: none !important;
+ visibility: hidden !important;
}
.closeButton::after,
@@ -366,3 +381,82 @@
text-align: center;
}
+.goTo
+{
+ appearance: none;
+ border: none;
+ padding: 0;
+ position: absolute;
+ top: 0;
+ right: 2rem;
+ width: 1rem;
+ height: 1rem;
+ background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ij48cGF0aCBkPSJNOSAyTDkgMyAxMi4zIDMgNiA5LjMgNi43IDEwIDEzIDMuNyAxMyA3IDE0IDcgMTQgMlpNNCA0QzIuOSA0IDIgNC45IDIgNkwyIDEyQzIgMTMuMSAyLjkgMTQgNCAxNEwxMCAxNEMxMS4xIDE0IDEyIDEzLjEgMTIgMTJMMTIgNyAxMSA4IDExIDEyQzExIDEyLjYgMTAuNiAxMyAxMCAxM0w0IDEzQzMuNCAxMyAzIDEyLjYgMyAxMkwzIDZDMyA1LjQgMy40IDUgNCA1TDggNSA5IDRaIi8+PC9zdmc+) no-repeat;
+ background-size: contain;
+ cursor: pointer;
+}
+
+#quizQueueBox ol
+{
+ display: grid;
+ grid-auto-rows: 1fr;
+ margin: 0;
+}
+ #quizQueueBox li[draggable="true"]
+ {
+ cursor: move;
+ position: relative;
+ padding-right: 3rem;
+ }
+ #quizQueueBox li.moving
+ {
+ color: transparent;
+ }
+ #quizQueueBox li.moving::marker
+ {
+ color: black;
+ }
+ #quizQueueBox li .closeButton,
+ #quizQueueBox li .goTo
+ {
+ display: none;
+ visibility: hidden;
+ top: 0;
+ }
+ #quizQueueBox li:hover .closeButton,
+ #quizQueueBox li:hover .goTo
+ {
+ display: unset;
+ visibility: visible;
+ }
+ #quizQueueBox li:active .closeButton,
+ #quizQueueBox li.moving .closeButton,
+ #quizQueueBox li.moving:hover .closeButton,
+ #quizQueueBox li.moving ~ li .closeButton,
+ #quizQueueBox li:active .goTo,
+ #quizQueueBox li.moving .goTo,
+ #quizQueueBox li.moving:hover .goTo,
+ #quizQueueBox li.moving ~ li .goTo
+ {
+ display: none;
+ visibility: hidden;
+ }
+ #quizQueueBox li .closeButton:hover,
+ #quizQueueBox li .goTo:hover
+ {
+ display: unset;
+ visibility: visible;
+ }
+#quizQueueBox form
+{
+ display: block;
+}
+ #quizQueueBox form input
+ {
+ display: inline;
+ vertical-align: baseline;
+ }
+ #quizQueueBox form input[type="number"]
+ {
+ width: 3.5em;
+ }
diff --git a/stylesheets/options.css b/stylesheets/options.css
index 49d0cb2..832fe84 100644
--- a/stylesheets/options.css
+++ b/stylesheets/options.css
@@ -78,17 +78,21 @@ ul
{
color: grey;
}
-#optionTree .labelFirst
+#optionTree li.labelFirst
+{
+ grid-template-columns: max-content 4em;
+}
+#optionTree ul li.labelFirst
{
grid-template-columns: 3em max-content 3em;
}
- #optionTree .labelFirst > label
- {
- padding-left: 0;
- padding-right: 0.5em;
- z-index: 3;
- background-color: white;
- }
+#optionTree .labelFirst > label
+{
+ padding-left: 0;
+ padding-right: 0.5em;
+ z-index: 3;
+ background-color: white;
+}
#optionTree .labelFirst.containsTextInput
{
@@ -119,6 +123,17 @@ ul
line-height: 1.5em;
}
+#optionTree .containsTime::after
+{
+ content:"s";
+ height: 1.5em;
+ line-height: 1.5em;
+}
+#optionTree > li.labelFirst.containsTime
+{
+ grid-template-columns: max-content 4em auto;
+}
+
button
{
width: 6em;
diff --git a/xporcle.js b/xporcle.js
index ce8f820..865bc98 100644
--- a/xporcle.js
+++ b/xporcle.js
@@ -143,7 +143,9 @@ async function init()
const pollData = Object.keys(statusResponse["poll_data"]).length !== 0 ? statusResponse["poll_data"] : undefined;
const voteData = Object.keys(statusResponse["vote_data"]).length !== 0 ? statusResponse["vote_data"] : undefined;
- onRoomConnect(statusResponse["scores"], pollData, voteData);
+ const queue = Object.keys(statusResponse["queue"]).length !== 0 ? statusResponse["queue"] : undefined;
+ const queueInterval = statusResponse["queue_interval"];
+ onRoomConnect(statusResponse["scores"], pollData, voteData, queue, queueInterval);
}
else
{
@@ -203,7 +205,9 @@ function retrieveOptions()
{
useDefaultUsername: false,
defaultUsername: "",
- blurRoomCode: false
+ blurRoomCode: false,
+ defaultPollDuration: 30,
+ defaultQuizQueueInterval: 60
};
if (Object.entries(data).length === 0)
@@ -260,11 +264,11 @@ function applyOptions()
// Blur Room Code
if (options.blurRoomCode)
{
- document.body.classList.add(".blurRoomCode");
+ document.body.classList.add("blurRoomCode");
}
else
{
- document.body.classList.remove(".blurRoomCode");
+ document.body.classList.remove("blurRoomCode");
}
}
@@ -353,9 +357,7 @@ function resetInterface(errorElement, lastUsername, lastCode)
setQuizStartProvention(false);
document.querySelectorAll("#startCountdown").forEach(elm => elm.remove());
- document.querySelector("#pollBox")?.remove();
- document.querySelector("#voteInfoBox")?.remove();
- document.querySelector("#ballotPopout")?.remove();
+ document.querySelectorAll(".interfaceSection:not(#interfaceBox)").forEach(section => section.remove());
init().then(
() =>
@@ -442,7 +444,16 @@ function processMessage(message)
updateLeaderboardUrls();
suggestions = [];
- document.querySelectorAll(`#changeQuizButton, #suggestionsHeader, #suggestionsList, #saveButton, #createPollButton`).forEach(element => element.remove());
+ document.querySelectorAll(
+ `
+ #changeQuizButton,
+ #suggestionsHeader, #suggestionsList,
+ #saveButton,
+ #createPollButton, #pollBox,
+ #addQuizToQueueButton, #quizQueueBox form, #quizQueueBox ol button
+ `
+ ).forEach(element => element.remove());
+ document.querySelectorAll(`#quizQueueBox ol li`).forEach(li => li.removeAttribute("draggable"));
// Add non host features
if (onQuizPage)
@@ -466,6 +477,16 @@ function processMessage(message)
updateLeaderboardUrls();
updateContextMenuHandling();
addSaveRoomButton();
+ if (onQuizPage)
+ {
+ interfaceBox.append(addQuizToQueueButton());
+ }
+ if (message["queue"] !== null)
+ {
+ document.querySelector("#quizQueueBox")?.remove();
+ addQuizQueueBox(message["queue"], message["queue_interval"]);
+ }
+
if (message["poll_data"] !== null)
{
addCreatePollBox(message["poll_data"]);
@@ -528,8 +549,51 @@ function processMessage(message)
case "vote_update":
updateVoteInfoBox(message["vote_data"]);
break;
+ case "queue_update":
+ updateQuizQueue(message["queue"], message["queue_interval"]);
+ break;
+ case "start_change_quiz_countdown":
+ {
+ const endTime = (new Date()).getTime() + message["countdown_length"]*1000;
+ const timeLeft = () => (Math.max(0, endTime - (new Date()).getTime())/1000).toFixed(1);
+ const countdown = document.createElement("div");
+ countdown.id = "changeQuizCountdown";
+ countdown.append(document.createElement("span"));
+ countdown.firstChild.textContent = `Changing to next quiz in ${timeLeft()}s`;
+ setInterval(
+ () =>
+ {
+ if (countdown.firstChild.textContent !== "Change quiz countdown cancelled")
+ {
+ countdown.firstChild.textContent = `Changing to next quiz in ${timeLeft()}s`;
+ }
+ }
+ , 100
+ );
+ if (host)
+ {
+ const cancelButton = document.createElement("button");
+ cancelButton.textContent = "Cancel Countdown";
+ cancelButton.addEventListener("click",
+ (event) =>
+ {
+ port.postMessage({type: "change_queue_interval", queue_interval: null});
+ }
+ );
+ countdown.append(cancelButton);
+ }
+ document.querySelector("#interfaceBox")?.insertBefore(countdown, document.querySelector("#leaderboard").nextElementSibling);
+ break;
+ }
+ case "cancel_change_quiz_countdown":
+ {
+ const countdownMessage = document.querySelector("#changeQuizCountdown span");
+ countdownMessage.textContent = "Change quiz countdown cancelled";
+ document.querySelector("#changeQuizCountdown button")?.remove();
+ setTimeout(() => countdownMessage.parentNode.remove(), 2000);
+ break;
+ }
}
-
}
function updateHostsInLeaderboard()
@@ -639,7 +703,7 @@ async function joinRoom(event)
username: username,
code: roomCode,
};
-
+ let queue = undefined;
try
{
port.postMessage({type: "startConnection", initialMessage: message});
@@ -656,6 +720,7 @@ async function joinRoom(event)
{
hosts = message["hosts"];
port.postMessage({type: "url_update", url: window.location.pathname})
+ queue = message["queue"] ?? undefined;
// Set up message handing
port.onMessage.addListener(processMessage);
@@ -689,7 +754,7 @@ async function joinRoom(event)
return;
}
- onRoomConnect();
+ onRoomConnect(undefined, undefined, undefined, queue);
}
async function loadRoom(event, form)
@@ -774,7 +839,7 @@ async function loadRoom(event, form)
onRoomConnect();
}
-function onRoomConnect(existingScores, existingPollData, currentVoteData)
+function onRoomConnect(existingScores, existingPollData, currentVoteData, queue, queueInterval)
{
// Set up message handing if not done already.
if (!port.onMessage.hasListener(processMessage))
@@ -795,6 +860,8 @@ function onRoomConnect(existingScores, existingPollData, currentVoteData)
roomCodeHeader.lastChild.textContent = roomCode;
interfaceBox.insertBefore(roomCodeHeader, interfaceBox.firstElementChild);
+ interfaceBox.insertBefore(collapseToggle(),roomCodeHeader.nextElementSibling);
+
// If the user is a host and is on a quiz,
// add a button to send the quiz to the rest of the room
if (host && onQuizPage)
@@ -833,6 +900,8 @@ function onRoomConnect(existingScores, existingPollData, currentVoteData)
if (host)
{
addSaveRoomButton();
+
+ // Poll
if (existingPollData)
{
addCreatePollBox(existingPollData);
@@ -841,6 +910,12 @@ function onRoomConnect(existingScores, existingPollData, currentVoteData)
{
addCreatePollButton();
}
+
+ // Add Quiz to Queue
+ if (onQuizPage)
+ {
+ interfaceBox.append(addQuizToQueueButton());
+ }
}
// Add vote info for current poll
@@ -853,6 +928,13 @@ function onRoomConnect(existingScores, existingPollData, currentVoteData)
}
}
+ // Quiz Queue
+ if (queue)
+ {
+ addQuizQueueBox(queue, queueInterval);
+ }
+
+
// If on a quiz page, observe for the start of the quiz
if (onQuizPage)
{
@@ -926,14 +1008,22 @@ function addChangeQuizButton()
);
// The button goes just after the room code header
- interfaceBox.insertBefore(changeQuizButton, interfaceBox.querySelector(`#roomCodeHeader`).nextElementSibling);
+ interfaceBox.insertBefore(changeQuizButton, interfaceBox.querySelector(`.toggle`).nextElementSibling);
}
-function addSuggestionQuizButton()
+function currentQuizInfo()
{
+ const url = window.location.href;
const shortTitle = document.querySelector(`title`).textContent;
const longTitle = document.querySelector("#gameMeta>h2").textContent;
+ return {url: url, short_title: shortTitle, long_title: longTitle};
+}
+
+function addSuggestionQuizButton()
+{
+ const quizInfo = currentQuizInfo();
+
const suggestQuizButton = document.createElement("button");
suggestQuizButton.id = "suggestQuizButton";
suggestQuizButton.textContent = "Suggest Quiz to Hosts";
@@ -943,9 +1033,9 @@ function addSuggestionQuizButton()
port.postMessage(
{
type: "suggest_quiz",
- url: window.location.href,
- short_title: shortTitle,
- long_title: longTitle
+ url: quizInfo.url,
+ short_title: quizInfo.short_title,
+ long_title: quizInfo.long_title
}
);
}
@@ -962,7 +1052,7 @@ function addCreatePollButton()
createPollButton.addEventListener("click",
(event) =>
{
- const pollData = {duration: 30, entries: []};
+ const pollData = {duration: options.defaultPollDuration, entries: []};
addCreatePollBox(pollData);
port.postMessage({type: "poll_create", poll_data: pollData});
createPollButton.remove();
@@ -989,12 +1079,7 @@ function addCreatePollBox(pollData)
if (onQuizPage)
{
// Get info about quiz from page
- const currentQuiz =
- {
- url: window.location.href,
- short_title: document.querySelector(`title`).textContent,
- long_title: document.querySelector("#gameMeta>h2").textContent
- };
+ const quizInfo = currentQuizInfo();
const addCurrentQuizToPollButton = document.createElement("button");
addCurrentQuizToPollButton.textContent = "Add Quiz to Poll";
@@ -1007,7 +1092,7 @@ function addCreatePollBox(pollData)
pollData.entries.push(currentQuiz);
const newEntryListItem = document.createElement("li");
- newEntryListItem.textContent = currentQuiz.short_title;
+ newEntryListItem.textContent = quizInfo.short_title;
removeEntryButton = document.createElement("div");
removeEntryButton.textContent = "×";
@@ -1015,7 +1100,7 @@ function addCreatePollBox(pollData)
(event) =>
{
newEntryListItem.remove();
- pollData.entries = pollData.entries.filter(existingEntry => existingEntry.url !== currentQuiz.url);
+ pollData.entries = pollData.entries.filter(existingEntry => existingEntry.url !== quizInfo.url);
port.postMessage({type:"poll_data_update", poll_data: pollData});
}
);
@@ -1062,7 +1147,7 @@ function addCreatePollBox(pollData)
pollDurationInput.addEventListener("change",
(event) =>
{
- pollData.duration = Math.max(10, Math.min(pollDurationInput.value, 60));
+ pollData.duration = Math.max(10, Math.min(Number(pollDurationInput.value), 60));
// Send updated data
port.postMessage({type:"poll_data_update", poll_data: pollData});
}
@@ -2345,3 +2430,214 @@ function collapseToggle()
return toggle;
}
+
+function addQuizToQueueButton()
+{
+ const button = document.createElement("button");
+ button.textContent = "Add Quiz to Queue";
+ button.addEventListener("click",
+ (event) =>
+ {
+ const quizInfo = currentQuizInfo();
+ const existingQueuedQuizes = Array.from(document.querySelectorAll("#quizQueueBox ol li"));
+
+ if (!existingQueuedQuizes.find(queuedQuiz => queuedQuiz.short_title === quizInfo.short_title))
+ {
+ port.postMessage({type: "add_to_queue", quiz: quizInfo});
+ }
+ }
+ );
+ return button;
+}
+
+function addQuizQueueBox(queue = [], queueInterval)
+{
+ const quizQueueBox = document.createElement("div");
+ quizQueueBox.classList.add("interfaceSection");
+ quizQueueBox.id = "quizQueueBox";
+
+ quizQueueBox.append(closeButton(quizQueueBox));
+
+ const header = document.createElement("h2");
+ header.textContent = "Quiz Queue";
+ quizQueueBox.append(header);
+
+ quizQueueBox.append(collapseToggle());
+
+ const queueList = document.createElement("ol");
+ quizQueueBox.append(queueList);
+
+ if (host)
+ {
+ const queueAutoChangeForm = document.createElement("form");
+
+ const autoChangeToggleInput = document.createElement("input");
+ autoChangeToggleInput.type = "checkbox";
+ autoChangeToggleInput.checked = !!queueInterval;
+ const intervalLabel = document.createElement("label");
+ intervalLabel.textContent = "auto change quiz after ";
+ const intervalInput = document.createElement("input");
+ intervalInput.type = "number";
+ intervalInput.min = 10;
+ intervalInput.max = 60*5;
+ intervalInput.value = queueInterval ?? options.defaultQuizQueueInterval;
+ intervalInput.disabled = !autoChangeToggleInput.checked;
+ [autoChangeToggleInput, intervalInput].forEach(
+ (input) =>
+ {
+ input.addEventListener("change",
+ (event) =>
+ {
+ intervalInput.disabled = !autoChangeToggleInput.checked;
+ intervalInput.value = Math.max(10, Math.min(Number(intervalInput.value), 60*5));
+ const newQueueInterval = autoChangeToggleInput.checked ? Number(intervalInput.value) : null;
+ port.postMessage({type: "change_queue_interval", queue_interval: newQueueInterval});
+ }
+ );
+ }
+ );
+ queueAutoChangeForm.append(
+ autoChangeToggleInput, intervalLabel, intervalInput, document.createTextNode("s")
+ );
+ quizQueueBox.append(queueAutoChangeForm);
+ }
+
+ sectionContainer.append(quizQueueBox);
+
+ updateQuizQueue(queue, queueInterval);
+}
+
+function updateQuizQueue(queue, queueInterval)
+{
+ let quizQueueBox = document.querySelector("#quizQueueBox");
+ if (queue.length === 0)
+ {
+ return quizQueueBox?.remove();
+ }
+ if (quizQueueBox === null)
+ {
+ return addQuizQueueBox(queue, queueInterval);
+ }
+
+
+ const queueList = quizQueueBox.querySelector("ol");
+ Array.from(queueList.querySelectorAll("li"))
+ .filter(li => !queue.some(queuedQuiz => queuedQuiz.short_title === li.textContent))
+ .forEach(li => li.remove());
+
+ queue.forEach(
+ (queuedQuiz) =>
+ {
+ let li = Array.from(queueList.querySelectorAll("li")).find(li => li.textContent === queuedQuiz.short_title);
+ if (li === undefined)
+ {
+ li = document.createElement("li");
+ li.textContent = queuedQuiz.short_title;
+ if (host)
+ {
+ // Add go to button
+ const goToButton = document.createElement("button");
+ goToButton.classList.add("goTo");
+ goToButton.addEventListener("click", (event) => {window.location = queuedQuiz.url});
+ li.append(goToButton);
+ // Add Remove Button
+ li.append(
+ closeButton(li,
+ (event) =>
+ {
+ port.postMessage({type: "remove_from_queue", quiz: queuedQuiz});
+ }
+ )
+ );
+ // Make list draggable to reorder queue.
+ li.setAttribute("draggable", "true");
+ li.addEventListener("dragstart",
+ (event) =>
+ {
+ const quizInfoString = JSON.stringify(queuedQuiz);
+ event.dataTransfer.setData("text/quizinfo", quizInfoString);
+ const dragImage = document.createElement("div");
+ dragImage.id = "tempDragImage";
+ dragImage.style =
+ `
+ position: absolute;
+ top: -2000vh;
+ left: -2000vw;
+ width: calc(${event.target.clientWidth}px - ${window.getComputedStyle(event.target).paddingRight});
+ `;
+ dragImage.textContent = queuedQuiz.short_title;
+ document.body.append(dragImage);
+
+ event.dataTransfer.setDragImage(dragImage, 30, dragImage.clientHeight/2);
+ event.dataTransfer.effectAllowed = "move";
+ event.target.classList.add("moving");
+ }
+ );
+ li.addEventListener("dragend",
+ (event) =>
+ {
+ event.target.classList.remove("moving");
+ event.target.style = "";
+ document.querySelector("#tempDragImage")?.remove();
+ }
+ );
+ li.addEventListener("dragenter",
+ (event) =>
+ {
+ if (event.dataTransfer.types.includes("text/quizinfo"))
+ {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ const beingDragged = queueList.querySelector(".moving");
+
+ const indexOffset =
+ Array.from(queueList.childNodes).indexOf(event.target)
+ - Array.from(queueList.childNodes).indexOf(beingDragged);
+ if (indexOffset > 0)
+ {
+ // Element being dragged is above the target.
+ // So we want to move the dragged element to just below/after it.
+ queueList.insertBefore(beingDragged, event.target.nextElementSibling);
+ }
+ else
+ {
+ // Element being dragged is below the target
+ // So we want to move the dragged element to just above/before it.
+ queueList.insertBefore(beingDragged, event.target);
+ }
+ }
+ }
+ );
+ li.addEventListener("dragover",
+ (event) =>
+ {
+ if (event.dataTransfer.types.includes("text/quizinfo"))
+ {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }
+ }
+ );
+ li.addEventListener("drop",
+ (event) =>
+ {
+ const newIndex = Array.from(queueList.children).indexOf(document.querySelector(".moving"));
+ port.postMessage({type: "reorder_queue", quiz: queuedQuiz, index: newIndex});
+ }
+ );
+ }
+ }
+ queueList.append(li);
+ }
+ );
+
+ if (host)
+ {
+ // Update Queue Interval
+ const autoChangeToggleInput = quizQueueBox.querySelector(`form input[type="checkbox"]`);
+ const intervalInput = quizQueueBox.querySelector(`form input[type="number"]`);
+ autoChangeToggleInput.checked = !!queueInterval;
+ intervalInput.disabled = !queueInterval;
+ intervalInput.value = queueInterval ?? intervalInput.value;
+ }
+}