From 0707dce2189892f69839f97e8facddbdd74d4fc5 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Sun, 2 Apr 2023 15:23:32 +0200 Subject: [PATCH] [Client] Improve reaction images management on NPC admin page - Fix HTML and improve style of the feature; - Improve admin NPC's JavaScript and fix addition of a reaction image where there is no one already present; - Allow upload of images with image/jpg MIME type; - Fix attribute name of a hidden input; - Apply the hover effects of links and buttons on their focus, for all admin pages. --- truthinquiry/static/css/admin_ui.css | 9 +- truthinquiry/static/css/admin_ui_npc.css | 34 ++++- truthinquiry/static/js/admin_npc.js | 162 +++++++++++++++++------ truthinquiry/templates/admin/npc.html | 50 +++---- 4 files changed, 184 insertions(+), 71 deletions(-) diff --git a/truthinquiry/static/css/admin_ui.css b/truthinquiry/static/css/admin_ui.css index 36a37b7..f55e3b8 100644 --- a/truthinquiry/static/css/admin_ui.css +++ b/truthinquiry/static/css/admin_ui.css @@ -9,6 +9,7 @@ } :root { + --admin-black-color: #000000; --admin-grey-color: #5A5656; --admin-red-color: #FF0000; --admin-white-color: #FFFFFF; @@ -41,7 +42,7 @@ button { justify-content: center; } -button, input { +button, input, select { background-color: transparent; border-color: var(--admin-white-color); border-style: solid; @@ -71,12 +72,12 @@ header a { text-decoration: none; } -a:hover { +a:focus, a:hover { background-color: var(--admin-white-color); color: var(--admin-grey-color); } -a:hover > .action_icon, .action_button:hover > .action_icon { +a:focus > .action_icon, .action_button:focus > .action_icon, a:hover > .action_icon, .action_button:hover > .action_icon { fill: var(--admin-grey-color); } @@ -89,7 +90,7 @@ svg { fill: var(--admin-white-color); } -.action_button:hover { +.action_button:focus, .action_button:hover { background-color: var(--admin-white-color); color: var(--admin-grey-color); } diff --git a/truthinquiry/static/css/admin_ui_npc.css b/truthinquiry/static/css/admin_ui_npc.css index 9ec1079..25a8b78 100644 --- a/truthinquiry/static/css/admin_ui_npc.css +++ b/truthinquiry/static/css/admin_ui_npc.css @@ -1,16 +1,36 @@ +img[alt] { + font-size: 1em; +} + +img[alt], .info_item, .section_title { + text-align: center; +} + input[type="text"] { width: 20em; } -.action_buttons, .answer_groups { - align-content: center; - display: flex; +option { + background-color: var(--admin-white-color); + color: var(--admin-black-color); +} + +.action_buttons, .answer_groups, .reaction, #add_reactions { flex-wrap: wrap; +} + +.action_buttons, .answer_group, .answer_groups, .reaction, #add_reactions { + display: flex; justify-content: center; } -.character_image { - width: 15em; +.answer_group, .reaction, #add_reactions { + align-items: center; + flex-direction: column; +} + +.character_image, .reaction_image { + width: 20em; } .character_image, #npc_name { @@ -26,8 +46,8 @@ input[type="text"] { font-size: 1.25em; } -.info_item, .section_title { - text-align: center; +.reaction, #add_reactions { + align-content: center; } .section_title { diff --git a/truthinquiry/static/js/admin_npc.js b/truthinquiry/static/js/admin_npc.js index b495208..eede6a8 100644 --- a/truthinquiry/static/js/admin_npc.js +++ b/truthinquiry/static/js/admin_npc.js @@ -1,5 +1,5 @@ -const reactionsDelta = {} +const reactionsDelta = {}; async function createOrUpdateNpc() { const data = {}; @@ -9,7 +9,7 @@ async function createOrUpdateNpc() { const allAnswersJson = []; data["allAnswers"] = allAnswersJson; - for (let answerTypeNode of document.querySelector(".answer_groups").children) { + for (const answerTypeNode of document.querySelector(".answer_groups").children) { const answersJson = []; const answerTypeJson = {"answers": answersJson}; allAnswersJson.push(answerTypeJson); @@ -20,35 +20,33 @@ async function createOrUpdateNpc() { } await makeAPIRequest("admin/setNpc", {"npc": data, "lang": "FR"}, {"content": "json"}); - await uploadReactionsDelta(); alert("Opération effectuée avec succès"); } async function uploadReactionsDelta() { - let requests = []; + const requests = []; + const npcId = document.querySelector("#npc_id").value; - - for(const [traitId, reactionNode] of Object.entries(reactionsDelta)){ + for (const [traitId, reactionNode] of Object.entries(reactionsDelta)) { const formData = new FormData(); - formData.append("npc_id", npc_id.value); + formData.append("npc_id", npcId); formData.append("trait_id", traitId); - if(reactionNode === null) formData.append("file", "null"); - else{ - const file = reactionNode.querySelector(".img_input").files[0] + if (reactionNode === null) { + formData.append("file", "null"); + } else { + const file = reactionNode.querySelector(".img_input").files[0]; formData.append("file", file ? file : ""); } requests.push(makeAPIRequest("admin/setReaction", formData, {"content": "form"})); } - for(request of requests){ + for (const request of requests) { await request; } - - } async function deleteNpc() { @@ -62,51 +60,141 @@ async function deleteNpc() { document.location = "/admin"; } -function changeReaction(inputNode){ - const parentNode = inputNode.parentNode; +function changeImageReaction(imageInputElement) { + const parentNode = imageInputElement.parentNode; const imgNode = parentNode.querySelector('img'); const traitId = parentNode.querySelector('.trait_id').value; const reader = new FileReader(); - reader.onload = (e)=>{ - imgNode.src = e.target.result - } - reader.readAsDataURL(inputNode.files[0]); + reader.addEventListener("load", event => { + imgNode.src = event.target.result + }); + reader.readAsDataURL(imageInputElement.files[0]); reactionsDelta[traitId] = parentNode; } -function deleteReaction(node){ - const reactionNode = node.parentNode; +function deleteImageReaction(reactionDeletionButton) { + if (!confirm("Voulez-vous vraiment supprimer l'image de cette réaction ?")) { + return; + } + + const reactionNode = reactionDeletionButton.parentNode; const traitId = reactionNode.querySelector(".trait_id").value; - const reactionName = reactionNode.querySelector("p").innerText; + const reactionName = reactionNode.querySelector(".reaction_name").innerText; reactionNode.parentNode.removeChild(reactionNode); const option = document.createElement("option"); - option.value = traitId - option.innerText = reactionName + option.value = traitId; + option.innerText = reactionName; - reactions_to_add.appendChild(option); + const addReactionsSelectorElement = document.getElementById("add_reactions_selector"); + if (addReactionsSelectorElement === null) { + // No add_reactions_selector element, this should never happen + // Do nothing in this case + return; + } + + addReactionsSelectorElement.appendChild(option); reactionsDelta[traitId] = null; } -function addReaction(selectNode){ - const selectedOptionNode = selectNode.selectedOptions[0]; +function addReaction(addReactionsSelectorElement) { + const selectedOptionNode = addReactionsSelectorElement.selectedOptions[0]; const traitId = selectedOptionNode.value; const reactionName = selectedOptionNode.innerText; - selectNode.removeChild(selectedOptionNode); + addReactionsSelectorElement.removeChild(selectedOptionNode); - const newReaction = reactions.querySelector("div").cloneNode(true); - newReaction.querySelector("img").src = ""; - newReaction.querySelector(".img_input").value = null; - newReaction.querySelector(".trait_id").value = traitId - newReaction.querySelector("p").innerText = reactionName - - reactions.appendChild(newReaction); + const newReactionElement = document.createElement("section"); + newReactionElement.classList.add("reaction"); - reactionsDelta[traitId] = newReaction; -} \ No newline at end of file + const reactionNameElement = document.createElement("h3"); + reactionNameElement.classList.add("reaction_name"); + reactionNameElement.textContent = reactionName; + + newReactionElement.appendChild(reactionNameElement); + + const imageElement = document.createElement("img"); + imageElement.classList.add("reaction_image"); + imageElement.setAttribute("alt", "Image d'une réaction d'un personnage"); + imageElement.src = "/static/images/no_photography_white.svg"; + + newReactionElement.appendChild(imageElement); + + const imageInputElement = document.createElement("input"); + imageInputElement.classList.add("img_input"); + imageInputElement.setAttribute("type", "file"); + imageInputElement.setAttribute("accept", "image/png, image/jpg, image/jpeg"); + imageInputElement.addEventListener("change", () => changeImageReaction(imageInputElement)); + + newReactionElement.appendChild(imageInputElement); + + const traitIdInputElement = document.createElement("input"); + traitIdInputElement.classList.add("trait_id"); + traitIdInputElement.setAttribute("type", "hidden"); + traitIdInputElement.setAttribute("value", traitId); + + newReactionElement.appendChild(traitIdInputElement); + + const buttonElement = document.createElement("button"); + buttonElement.classList.add("delete_question_btn", "action_button", "short_color_transition"); + buttonElement.setAttribute("title", "Cliquez ici pour supprimer l'image de cette réaction"); + buttonElement.addEventListener("click", () => deleteImageReaction(buttonElement)); + + const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svgElement.classList.add("action_icon", "short_color_transition"); + svgElement.setAttribute("viewBox", "0 0 48 48"); + + const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path"); + pathElement.setAttribute("d", + "M12.45 38.7 9.3 35.55 20.85 24 9.3 12.5l3.15-3.2L24 20.8 35.55 9.3l3.15 3.2L27.2 24l11.5 11.55-3.15 3.15L24 27.2Z"); + + svgElement.appendChild(pathElement); + + buttonElement.appendChild(svgElement); + buttonElement.appendChild(document.createTextNode("Supprimer l'image de la réaction")); + + newReactionElement.appendChild(buttonElement); + + const reactionsElement = document.getElementById("reactions"); + if (reactionsElement === null) { + // No add_reactions_selector element, this should never happen + // Do nothing in this case + return; + } + + reactionsElement.appendChild(newReactionElement); + + reactionsDelta[traitId] = newReactionElement; +} + +function setListenersToImageInputs() { + for (const imageInput of document.getElementsByClassName("img_input")) { + imageInput.addEventListener("change", () => changeImageReaction(imageInput)); + } +} + +function setListenersToImageReactionsRemovalButtons() { + for (const imageReactionRemovalButton of document.getElementsByClassName("delete_image_reaction_btn")) { + imageReactionRemovalButton.addEventListener("click", () => deleteImageReaction(imageReactionRemovalButton)); + } +} + +function setListenersToAddReactionsSelector() { + const addReactionsSelectorElement = document.getElementById("add_reactions_selector"); + if (addReactionsSelectorElement === null) { + // No add_reactions_selector element, this should never happen + // Do nothing in this case + return; + } + + addReactionsSelectorElement.addEventListener("change", () => addReaction(addReactionsSelectorElement)); +} + +setListenersToImageReactionsRemovalButtons(); +setListenersToImageInputs(); +setListenersToAddReactionsSelector(); diff --git a/truthinquiry/templates/admin/npc.html b/truthinquiry/templates/admin/npc.html index b34edd7..d920958 100644 --- a/truthinquiry/templates/admin/npc.html +++ b/truthinquiry/templates/admin/npc.html @@ -26,37 +26,41 @@
Cliquez sur les champs pour éditer les informations. Dans les réponses aux questions lors de l'interrogation, utilisez « {NPC} » pour faire référence au nom d'un personnage et « {SALLE} » pour faire référence au nom d'une salle.
-Nom du personnage
Image du personnage
{{reaction.get('name')}}
-