Merge pull request #2410 from SillyTavern/inline-image-enlarge-rework

Inline image enlarge rework
This commit is contained in:
Cohee 2024-06-23 14:59:24 +03:00 committed by GitHub
commit 3a15e44d0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 183 additions and 65 deletions

View File

@ -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,17 @@ dialog {
filter: brightness(1.3) saturate(1.3);
}
.popup .popup-button-close {
position: absolute;
top: -6px;
right: -6px;
width: 24px;
height: 24px;
font-size: 20px;
padding: 2px 3px 3px 2px;
filter: brightness(0.8);
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
}

View File

@ -4855,10 +4855,11 @@
</div>
<textarea class="popup-input text_pole result-control" rows="1" data-result="1"></textarea>
<div class="popup-controls">
<div class="popup-button-ok menu_button result-control" data-i18n="Delete" data-result="1" tabindex="0">Delete</div>
<div class="popup-button-cancel menu_button result-control" data-i18n="Cancel" data-result="0" tabindex="0">Cancel</div>
<div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
<div class="popup-button-cancel menu_button result-control" data-result="0" data-i18n="Cancel">Cancel</div>
</div>
</div>
<div class="popup-button-close right_menu_button fa-solid fa-circle-xmark" data-result="0" title="Close popup" data-i18n="[title]Close popup"></div>
</dialog>
</template>
<div id="shadow_popup">

View File

@ -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) {

View File

@ -35,7 +35,7 @@ import {
extractTextFromOffice,
} from './utils.js';
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
@ -566,7 +566,7 @@ export function isExternalMediaAllowed() {
return !power_user.forbid_external_media;
}
function enlargeMessageImage() {
async function enlargeMessageImage() {
const mesBlock = $(this).closest('.mes');
const mesId = mesBlock.attr('mesid');
const message = chat[mesId];
@ -580,14 +580,28 @@ function enlargeMessageImage() {
const img = document.createElement('img');
img.classList.add('img_enlarged');
img.src = imgSrc;
const imgHolder = document.createElement('div');
imgHolder.classList.add('img_enlarged_holder');
imgHolder.append(img);
const imgContainer = $('<div><pre><code></code></pre></div>');
imgContainer.prepend(img);
imgContainer.prepend(imgHolder);
imgContainer.addClass('img_enlarged_container');
imgContainer.find('code').addClass('txt').text(title);
const titleEmpty = !title || title.trim().length === 0;
imgContainer.find('pre').toggle(!titleEmpty);
addCopyToCodeBlocks(imgContainer);
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
const popup = new Popup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true });
popup.dlg.style.width = 'unset';
popup.dlg.style.height = 'unset';
img.addEventListener('click', () => {
const shouldZoom = !img.classList.contains('zoomed');
img.classList.toggle('zoomed', shouldZoom);
});
await popup.show();
}
async function deleteMessageImage() {

View File

@ -3,33 +3,39 @@ 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,
};
/**
* @typedef {object} PopupOptions
* @property {string|boolean?} [okButton] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {string|boolean?} [cancelButton] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {number?} [rows] - The number of rows for the input field
* @property {boolean?} [wide] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
* @property {boolean?} [wider] - Whether to display the popup in wider mode (just wider, no height scaling)
* @property {boolean?} [large] - Whether to display the popup in large mode (90% of screen)
* @property {boolean?} [allowHorizontalScrolling] - Whether to allow horizontal scrolling in the popup
* @property {boolean?} [allowVerticalScrolling] - Whether to allow vertical scrolling in the popup
* @property {POPUP_RESULT|number?} [defaultResult] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
* @property {CustomPopupButton[]|string[]?} [customButtons] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
* @property {(popup: Popup) => boolean?} [onClosing] - Handler called before the popup closes, return `false` to cancel the close
* @property {(popup: Popup) => void?} [onClose] - Handler called after the popup closes, but before the DOM is cleaned up
* @property {string|boolean?} [okButton=null] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {number?} [rows=1] - The number of rows for the input field
* @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
* @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling)
* @property {boolean?} [large=false] - Whether to display the popup in large mode (90% of screen)
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
* @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
* @property {(popup: Popup) => boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close
* @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
*/
/**
@ -75,8 +81,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;
@ -99,7 +106,7 @@ export class Popup {
* @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) {
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
@ -118,19 +125,21 @@ 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');
if (wider) this.dlg.classList.add('wider_dialogue_popup');
if (large) this.dlg.classList.add('large_dialogue_popup');
if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_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 +150,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 +165,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 +215,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 +315,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();
}

View File

@ -366,16 +366,6 @@ input[type='checkbox']:focus-visible {
font-weight: bold;
}
.img_enlarged_container {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 10px;
height: 100%;
width: 100%;
}
.img_enlarged_container pre code,
.mes_text pre code {
position: relative;
display: block;
@ -3144,6 +3134,16 @@ grammarly-extension {
min-width: 750px;
}
.transparent_dialogue_popup {
background-color: transparent;
box-shadow: none;
border: none;
}
.transparent_dialogue_popup:focus-visible {
outline: none;
}
#dialogue_popup .horizontal_scrolling_dialogue_popup {
overflow-x: unset !important;
}
@ -4457,38 +4457,106 @@ 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 {
display: flex;
}
.img_enlarged_holder {
/* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */
min-height: 120px;
}
.img_enlarged_holder:has(.zoomed) {
overflow: auto;
}
.img_enlarged {
max-width: 100%;
max-height: 100%;
border-radius: 2px;
border: 1px solid transparent;
object-fit: contain;
width: 100%;
height: 100%;
cursor: zoom-in
}
.img_enlarged.zoomed {
object-fit: cover;
width: auto;
height: auto;
cursor: zoom-out;
}
.img_enlarged_container {
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px;
height: 100%;
width: 100%;
}
.img_enlarged_holder::-webkit-scrollbar-corner {
background-color: transparent;
}
.img_enlarged_container pre code {
position: relative;
display: block;
overflow-x: auto;
padding: 1em;
}
.popup:has(.img_enlarged.zoomed).large_dialogue_popup {
height: 100vh !important;
height: 100svh !important;
max-height: 100vh !important;
max-height: 100svh !important;
max-width: 100vw !important;
max-width: 100svw !important;
padding: 0;
}
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-content {
margin: 0;
padding: 0;
}
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .img_enlarged_container pre {
display: none;
}
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-button-close {
display: none !important;
}
.cropper-container {