diff --git a/public/css/popup.css b/public/css/popup.css
index 62c29fcf9..0ce4af1ce 100644
--- a/public/css/popup.css
+++ b/public/css/popup.css
@@ -109,7 +109,6 @@ dialog {
.menu_button.popup-button-ok {
background-color: var(--crimson70a);
- cursor: pointer;
}
.menu_button.popup-button-ok:hover {
@@ -132,3 +131,13 @@ dialog {
filter: brightness(1.3) saturate(1.3);
}
+.popup .popup-button-close {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ width: 20px;
+ height: 20px;
+ font-size: 19px;
+ filter: brightness(0.4);
+}
+
diff --git a/public/index.html b/public/index.html
index b2fc3783d..0fc7f5b36 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4855,10 +4855,11 @@
+
diff --git a/public/script.js b/public/script.js
index 29df8a38e..c1e199885 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2122,7 +2122,7 @@ export function addCopyToCodeBlocks(messageElement) {
hljs.highlightElement(codeBlocks.get(i));
if (navigator.clipboard !== undefined) {
const copyButton = document.createElement('i');
- copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy');
+ copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
copyButton.title = 'Copy code';
codeBlocks.get(i).appendChild(copyButton);
copyButton.addEventListener('pointerup', function (event) {
diff --git a/public/scripts/chats.js b/public/scripts/chats.js
index 32a188c20..3362189fa 100644
--- a/public/scripts/chats.js
+++ b/public/scripts/chats.js
@@ -587,7 +587,7 @@ function enlargeMessageImage() {
const titleEmpty = !title || title.trim().length === 0;
imgContainer.find('pre').toggle(!titleEmpty);
addCopyToCodeBlocks(imgContainer);
- callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
+ callGenericPopup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true });
}
async function deleteMessageImage() {
diff --git a/public/scripts/popup.js b/public/scripts/popup.js
index 6e388839c..7222a9a28 100644
--- a/public/scripts/popup.js
+++ b/public/scripts/popup.js
@@ -3,17 +3,22 @@ import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
/** @readonly */
/** @enum {Number} */
export const POPUP_TYPE = {
- 'TEXT': 1,
- 'CONFIRM': 2,
- 'INPUT': 3,
+ /** Main popup type. Containing any content displayed, with buttons below. Can also contain additional input controls. */
+ TEXT: 1,
+ /** Popup mainly made to confirm something, answering with a simple Yes/No or similar. Focus on the button controls. */
+ CONFIRM: 2,
+ /** Popup who's main focus is the input text field, which is displayed here. Can contain additional content above. Return value for this is the input string. */
+ INPUT: 3,
+ /** Popup without any button controls. Used to simply display content, with a small X in the corner. */
+ DISPLAY: 4,
};
/** @readonly */
/** @enum {number?} */
export const POPUP_RESULT = {
- 'AFFIRMATIVE': 1,
- 'NEGATIVE': 0,
- 'CANCELLED': null,
+ AFFIRMATIVE: 1,
+ NEGATIVE: 0,
+ CANCELLED: null,
};
/**
@@ -75,8 +80,9 @@ export class Popup {
/** @type {HTMLElement} */ content;
/** @type {HTMLTextAreaElement} */ input;
/** @type {HTMLElement} */ controls;
- /** @type {HTMLElement} */ ok;
- /** @type {HTMLElement} */ cancel;
+ /** @type {HTMLElement} */ okButton;
+ /** @type {HTMLElement} */ cancelButton;
+ /** @type {HTMLElement} */ closeButton;
/** @type {POPUP_RESULT|number?} */ defaultResult;
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
@@ -118,8 +124,9 @@ export class Popup {
this.content = this.dlg.querySelector('.popup-content');
this.input = this.dlg.querySelector('.popup-input');
this.controls = this.dlg.querySelector('.popup-controls');
- this.ok = this.dlg.querySelector('.popup-button-ok');
- this.cancel = this.dlg.querySelector('.popup-button-cancel');
+ this.okButton = this.dlg.querySelector('.popup-button-ok');
+ this.cancelButton = this.dlg.querySelector('.popup-button-cancel');
+ this.closeButton = this.dlg.querySelector('.popup-button-close');
this.dlg.setAttribute('data-id', this.id);
if (wide) this.dlg.classList.add('wide_dialogue_popup');
@@ -129,8 +136,8 @@ export class Popup {
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
// If custom button captions are provided, we set them beforehand
- this.ok.textContent = typeof okButton === 'string' ? okButton : 'OK';
- this.cancel.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
+ this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
+ this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
this.defaultResult = defaultResult;
this.customButtons = customButtons;
@@ -141,17 +148,14 @@ export class Popup {
const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control');
buttonElement.classList.add(...(button.classes ?? []));
- buttonElement.setAttribute('data-result', String(button.result ?? undefined));
+ buttonElement.dataset.result = String(button.result ?? undefined);
buttonElement.textContent = button.text;
buttonElement.tabIndex = 0;
- if (button.action) buttonElement.addEventListener('click', button.action);
- if (button.result) buttonElement.addEventListener('click', () => this.complete(button.result));
-
if (button.appendAtEnd) {
this.controls.appendChild(buttonElement);
} else {
- this.controls.insertBefore(buttonElement, this.ok);
+ this.controls.insertBefore(buttonElement, this.okButton);
}
});
@@ -159,23 +163,30 @@ export class Popup {
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
if (defaultButton) defaultButton.classList.add('menu_button_default');
+ // Styling differences depending on the popup type
+ // General styling for all types first, that might be overriden for specific types below
+ this.input.style.display = 'none';
+ this.closeButton.style.display = 'none';
+
switch (type) {
case POPUP_TYPE.TEXT: {
- this.input.style.display = 'none';
- if (!cancelButton) this.cancel.style.display = 'none';
+ if (!cancelButton) this.cancelButton.style.display = 'none';
break;
}
case POPUP_TYPE.CONFIRM: {
- this.input.style.display = 'none';
- if (!okButton) this.ok.textContent = template.getAttribute('popup-button-yes');
- if (!cancelButton) this.cancel.textContent = template.getAttribute('popup-button-no');
+ if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes');
+ if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no');
break;
}
case POPUP_TYPE.INPUT: {
this.input.style.display = 'block';
- if (!okButton) this.ok.textContent = template.getAttribute('popup-button-save');
+ if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save');
break;
}
+ case POPUP_TYPE.DISPLAY: {
+ this.controls.style.display = 'none';
+ this.closeButton.style.display = 'block';
+ }
default: {
console.warn('Unknown popup type.', type);
break;
@@ -202,8 +213,14 @@ export class Popup {
// Set focus event that remembers the focused element
this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; });
- this.ok.addEventListener('click', () => this.complete(POPUP_RESULT.AFFIRMATIVE));
- this.cancel.addEventListener('click', () => this.complete(POPUP_RESULT.NEGATIVE));
+ // Bind event listeners for all result controls to their defined event type
+ this.dlg.querySelectorAll(`[data-result]`).forEach(resultControl => {
+ if (!(resultControl instanceof HTMLElement)) return;
+ const result = Number(resultControl.dataset.result);
+ if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
+ const type = resultControl.dataset.resultEvent || 'click';
+ resultControl.addEventListener(type, () => this.complete(result));
+ });
// Bind dialog listeners manually, so we can be sure context is preserved
const cancelListener = (evt) => {
@@ -296,6 +313,9 @@ export class Popup {
if (applyAutoFocus) {
control.setAttribute('autofocus', '');
+ // Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus
+ // interactable only gets applied when inserted into the DOM
+ control.tabIndex = 0;
} else {
control.focus();
}
diff --git a/public/style.css b/public/style.css
index 96a1ef69e..3df89b441 100644
--- a/public/style.css
+++ b/public/style.css
@@ -4457,26 +4457,36 @@ a {
.mes_img_controls {
position: absolute;
- top: 0.5em;
+ top: 0.1em;
left: 0;
width: 100%;
- display: none;
+ display: flex;
+ opacity: 0;
flex-direction: row;
justify-content: space-between;
padding: 1em;
}
.mes_img_controls .right_menu_button {
- padding: 0;
filter: brightness(80%);
+ padding: 1px;
+ height: 1.25em;
+ width: 1.25em;
+}
+
+.mes_img_controls .right_menu_button::before {
+ /* Fix weird alignment with this font-awesome icons on focus */
+ position: relative;
+ top: 0.6125em;
}
.mes_img_controls .right_menu_button:hover {
filter: brightness(150%);
}
-.mes_img_container:hover .mes_img_controls {
- display: flex;
+.mes_img_container:hover .mes_img_controls,
+.mes_img_container:focus-within .mes_img_controls {
+ opacity: 1;
}
.mes .mes_img_container.img_extra {