Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions internal/template/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,26 @@ func TestQueryString(t *testing.T) {
}
}

func TestConfirmationModalDefaultsToCancelAction(t *testing.T) {
contents, err := commonTemplateFiles.ReadFile("templates/common/layout.html")
if err != nil {
t.Fatalf(`Unable to read layout template: %v`, err)
}

layout := string(contents)
if !strings.Contains(layout, `<dialog id="confirmation-modal">`) {
t.Fatal(`The confirmation modal should be present in the base layout`)
}

if !strings.Contains(layout, `<button value="no" id="confirmation-modal-no" class="button" autofocus>`) {
t.Fatal(`The confirmation modal should focus the cancel action by default`)
}

if strings.Contains(layout, `<button value="yes" id="confirmation-modal-yes" class="button button-primary" autofocus>`) {
t.Fatal(`The confirmation modal should not focus the confirm action by default`)
}
}

func TestCSPExternalFont(t *testing.T) {
want := []string{
`default-src 'none';`,
Expand Down
9 changes: 9 additions & 0 deletions internal/template/templates/common/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ <h3 tabindex="-1" id="dialog-title">{{ t "page.keyboard_shortcuts.title" }}</h3>
</ul>
</div>
</dialog>
<dialog id="confirmation-modal">
<form method="dialog">
<p id="confirmation-modal-question"></p>
<div class="confirmation-modal-actions">
<button value="yes" id="confirmation-modal-yes" class="button button-primary"></button>
<button value="no" id="confirmation-modal-no" class="button" autofocus></button>
</div>
</form>
</dialog>

<template id="icon-read">{{ icon "read" }}</template>
<template id="icon-unread">{{ icon "unread" }}</template>
Expand Down
30 changes: 18 additions & 12 deletions internal/ui/static/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -675,10 +675,18 @@ template {
}

dialog {
max-height: none;
height: 100vh;
box-sizing: border-box;
inline-size: fit-content;
max-inline-size: calc(100vw - 2rem);
max-block-size: 80vh;
border: none;
padding: 1%;
margin: 10vh auto auto;
padding: 1rem;
overflow: auto;
}

dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}

.btn-close-modal {
Expand Down Expand Up @@ -1291,18 +1299,16 @@ details.entry-enclosures {
word-wrap: break-word;
}

/* Confirmation */
.confirm {
#confirmation-modal-question {
margin: 0 0 0.75rem;
font-weight: 500;
color: #ed2d04;
text-align: center;
}

.confirm button {
color: #ed2d04;
border: none;
background-color: transparent;
cursor: pointer;
font-size: inherit;
.confirmation-modal-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}

.loading {
Expand Down
63 changes: 24 additions & 39 deletions internal/ui/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -901,68 +901,53 @@ function updateUnreadCounterValue(delta) {
}

/**
* Handle confirmation messages for actions that require user confirmation.
* Handle confirmation dialogs for actions that require user confirmation.
*
* This function modifies the link element to show a confirmation question with "Yes" and "No" buttons.
* If the user clicks "Yes", it calls the provided callback with the URL and redirect URL.
* If the user clicks "No", it either redirects to a no-action URL or restores the link element.
* If the user clicks "No", it either redirects to a no-action URL or cancels the action.
*
* @param {Element} linkElement - The link or button element that triggered the confirmation.
* @param {function} callback - The callback function to execute if the user confirms the action.
* @returns {void}
*/
function handleConfirmationMessage(linkElement, callback) {
if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") {
linkElement = linkElement.parentNode;
}

linkElement.style.display = "none";

const containerElement = linkElement.parentNode;
const questionElement = document.createElement("span");
linkElement = linkElement.closest("a, button");
if (!linkElement) return;

function createLoadingElement() {
const loadingElement = document.createElement("span");
loadingElement.className = "loading";
loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));

questionElement.remove();
containerElement.appendChild(loadingElement);
linkElement.style.display = "none";
linkElement.parentNode.appendChild(loadingElement);
}

const yesElement = document.createElement("button");
yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));
yesElement.onclick = (event) => {
event.preventDefault();
const dialogElement = document.getElementById("confirmation-modal");
const questionElement = document.getElementById("confirmation-modal-question");
const yesElement = document.getElementById("confirmation-modal-yes");
const noElement = document.getElementById("confirmation-modal-no");

createLoadingElement();
questionElement.textContent = linkElement.dataset.labelQuestion;
yesElement.textContent = linkElement.dataset.labelYes;
noElement.textContent = linkElement.dataset.labelNo;
dialogElement.returnValue = "";

callback(linkElement.dataset.url, linkElement.dataset.redirectUrl);
};

const noElement = document.createElement("button");
noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));
noElement.onclick = (event) => {
event.preventDefault();

const noActionUrl = linkElement.dataset.noActionUrl;
if (noActionUrl) {
dialogElement.onclose = () => {
dialogElement.onclose = null;
if (dialogElement.returnValue === "yes") {
createLoadingElement();
callback(linkElement.dataset.url, linkElement.dataset.redirectUrl);
return;
}

callback(noActionUrl, linkElement.dataset.redirectUrl);
} else {
linkElement.style.display = "inline";
questionElement.remove();
if (dialogElement.returnValue === "no" && linkElement.dataset.noActionUrl) {
createLoadingElement();
callback(linkElement.dataset.noActionUrl, linkElement.dataset.redirectUrl);
}
};

questionElement.className = "confirm";
questionElement.appendChild(document.createTextNode(`${linkElement.dataset.labelQuestion} `));
questionElement.appendChild(yesElement);
questionElement.appendChild(document.createTextNode(", "));
questionElement.appendChild(noElement);

containerElement.appendChild(questionElement);
dialogElement.showModal();
}

/**
Expand Down