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 {