diff --git a/public/index.html b/public/index.html index 98170d1a8..b2916a859 100644 --- a/public/index.html +++ b/public/index.html @@ -4511,6 +4511,22 @@ +
diff --git a/public/script.js b/public/script.js index 18e15e8e2..ee4bb3dbe 100644 --- a/public/script.js +++ b/public/script.js @@ -211,6 +211,7 @@ import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermati import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; +import { callGenericPopup } from './scripts/popup.js'; //exporting functions and vars for mods export { @@ -821,7 +822,7 @@ let create_save = { //animation right menu export const ANIMATION_DURATION_DEFAULT = 125; export let animation_duration = ANIMATION_DURATION_DEFAULT; -let animation_easing = 'ease-in-out'; +export let animation_easing = 'ease-in-out'; let popup_type = ''; let chat_file_for_del = ''; let online_status = 'no_connection'; @@ -7808,6 +7809,7 @@ window['SillyTavern'].getContext = function () { registedDebugFunction: registerDebugFunction, renderExtensionTemplate: renderExtensionTemplate, callPopup: callPopup, + callGenericPopup: callGenericPopup, mainApi: main_api, extensionSettings: extension_settings, ModuleWorkerWrapper: ModuleWorkerWrapper, diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 3e6ea982b..33a1d05f6 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,4 +1,4 @@ -import { callPopup } from '../../../../script.js'; +import { POPUP_TYPE, Popup } from '../../../popup.js'; import { getSortableDelay } from '../../../utils.js'; import { log, warn } from '../index.js'; import { QuickReplyContextLink } from './QuickReplyContextLink.js'; @@ -44,6 +44,8 @@ export class QuickReply { /**@type {HTMLInputElement}*/ settingsDomLabel; /**@type {HTMLTextAreaElement}*/ settingsDomMessage; + /**@type {Popup}*/ editorPopup; + /**@type {HTMLElement}*/ editorExecuteBtn; /**@type {HTMLElement}*/ editorExecuteErrors; /**@type {HTMLInputElement}*/ editorExecuteHide; @@ -197,7 +199,8 @@ export class QuickReply { /**@type {HTMLElement} */ // @ts-ignore const dom = this.template.cloneNode(true); - const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); + this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); + const popupResult = this.editorPopup.show(); // basics /**@type {HTMLInputElement}*/ @@ -435,7 +438,7 @@ export class QuickReply { this.editorExecuteBtn.classList.add('qr--busy'); this.editorExecuteErrors.innerHTML = ''; if (this.editorExecuteHide.checked) { - document.querySelector('#shadow_popup').classList.add('qr--hide'); + this.editorPopup.dom.classList.add('qr--hide'); } try { this.editorExecutePromise = this.execute(); @@ -445,7 +448,7 @@ export class QuickReply { } this.editorExecutePromise = null; this.editorExecuteBtn.classList.remove('qr--busy'); - document.querySelector('#shadow_popup').classList.remove('qr--hide'); + this.editorPopup.dom.classList.remove('qr--hide'); } diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 132fe008a..bff6ee067 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -216,60 +216,60 @@ align-items: baseline; } @media screen and (max-width: 750px) { - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex-direction: column; } - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { flex-direction: column; } - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { min-height: 90svh; } } -#dialogue_popup:has(#qr--modalEditor) { +.dialogue_popup:has(#qr--modalEditor) { aspect-ratio: unset; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text { display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex: 1 1 auto; display: flex; flex-direction: row; gap: 1em; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { flex: 1 1 auto; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { flex: 0 0 auto; display: flex; flex-direction: row; gap: 0.5em; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { flex: 1 1 1px; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { flex: 1 1 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { flex: 1 1 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { flex: 0 0 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { flex: 1 1 auto; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { display: flex; flex-direction: row; gap: 1em; @@ -277,24 +277,24 @@ font-size: smaller; align-items: baseline; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label { white-space: nowrap; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input { font-size: inherit; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { flex: 1 1 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute { display: flex; flex-direction: row; gap: 0.5em; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy { opacity: 0.5; cursor: wait; } -#shadow_popup.qr--hide { +.shadow_popup.qr--hide { opacity: 0 !important; } diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 725cf97e2..b91d7b444 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -242,7 +242,7 @@ @media screen and (max-width: 750px) { - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex-direction: column; > #qr--main > .qr--labels { flex-direction: column; @@ -252,10 +252,10 @@ } } } -#dialogue_popup:has(#qr--modalEditor) { +.dialogue_popup:has(#qr--modalEditor) { aspect-ratio: unset; - #dialogue_popup_text { + .dialogue_popup_text { display: flex; flex-direction: column; @@ -326,6 +326,6 @@ } } -#shadow_popup.qr--hide { +.shadow_popup.qr--hide { opacity: 0 !important; } diff --git a/public/scripts/popup.js b/public/scripts/popup.js new file mode 100644 index 000000000..b793f3a66 --- /dev/null +++ b/public/scripts/popup.js @@ -0,0 +1,225 @@ +import { animation_duration, animation_easing } from '../script.js'; +import { delay } from './utils.js'; + + + +/**@readonly*/ +/**@enum {Number}*/ +export const POPUP_TYPE = { + 'TEXT': 1, + 'CONFIRM': 2, + 'INPUT': 3, +}; + +/**@readonly*/ +/**@enum {Boolean}*/ +export const POPUP_RESULT = { + 'AFFIRMATIVE': true, + 'NEGATIVE': false, + 'CANCELLED': undefined, +}; + + + +export class Popup { + /**@type {POPUP_TYPE}*/ type; + + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ dlg; + /**@type {HTMLElement}*/ text; + /**@type {HTMLTextAreaElement}*/ input; + /**@type {HTMLElement}*/ ok; + /**@type {HTMLElement}*/ cancel; + + /**@type {POPUP_RESULT}*/ result; + /**@type {any}*/ value; + + /**@type {Promise}*/ promise; + /**@type {Function}*/ resolver; + + /**@type {Function}*/ keyListenerBound; + + + + /** + * @typedef {{okButton?: string, cancelButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup. + * @param {JQuery|string|Element} text - Text to display in the popup. + * @param {POPUP_TYPE} type - One of Popup.TYPE + * @param {string} inputValue - Value to set the input to. + * @param {PopupOptions} options - Options for the popup. + */ + constructor(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { + this.type = type; + + /**@type {HTMLTemplateElement}*/ + const template = document.querySelector('#shadow_popup_template'); + // @ts-ignore + this.dom = template.content.cloneNode(true).querySelector('.shadow_popup'); + const dlg = this.dom.querySelector('.dialogue_popup'); + // @ts-ignore + this.dlg = dlg; + this.text = this.dom.querySelector('.dialogue_popup_text'); + this.input = this.dom.querySelector('.dialogue_popup_input'); + this.ok = this.dom.querySelector('.dialogue_popup_ok'); + this.cancel = this.dom.querySelector('.dialogue_popup_cancel'); + + if (wide) dlg.classList.add('wide_dialogue_popup'); + if (large) dlg.classList.add('large_dialogue_popup'); + if (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup'); + if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup'); + + this.ok.textContent = okButton ?? 'OK'; + this.cancel.textContent = cancelButton ?? 'Cancel'; + + switch(type) { + case POPUP_TYPE.TEXT: { + this.input.style.display = 'none'; + this.cancel.style.display = 'none'; + break; + } + case POPUP_TYPE.CONFIRM: { + this.input.style.display = 'none'; + this.ok.textContent = okButton ?? 'Yes'; + this.cancel.textContent = cancelButton ?? 'No'; + break; + } + case POPUP_TYPE.INPUT: { + this.input.style.display = 'block'; + this.ok.textContent = okButton ?? 'Save'; + break; + } + default: { + // illegal argument + } + } + + this.input.value = inputValue; + this.input.rows = rows ?? 1; + + this.text.innerHTML = ''; + if (text instanceof jQuery) { + $(this.text).append(text); + } else if (text instanceof HTMLElement) { + this.text.append(text); + } else if (typeof text == 'string') { + this.text.innerHTML = text; + } else { + // illegal argument + } + + this.ok.addEventListener('click', ()=>this.completeAffirmative()); + this.cancel.addEventListener('click', ()=>this.completeNegative()); + const keyListener = (evt)=>{ + switch (evt.key) { + case 'Escape': { + evt.preventDefault(); + evt.stopPropagation(); + this.completeCancelled(); + window.removeEventListener('keydown', keyListenerBound); + break; + } + } + }; + const keyListenerBound = keyListener.bind(this); + window.addEventListener('keydown', keyListenerBound); + } + + async show() { + document.body.append(this.dom); + this.dom.style.display = 'block'; + switch(this.type) { + case POPUP_TYPE.INPUT: { + this.input.focus(); + break; + } + } + + $(this.dom).transition({ + opacity: 1, + duration: animation_duration, + easing: animation_easing, + }); + + this.promise = new Promise((resolve) => { + this.resolver = resolve; + }); + return this.promise; + } + + completeAffirmative() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: { + this.value = true; + break; + } + case POPUP_TYPE.INPUT: { + this.value = this.input.value; + break; + } + } + this.result = POPUP_RESULT.AFFIRMATIVE; + this.hide(); + } + + completeNegative() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: + case POPUP_TYPE.INPUT: { + this.value = false; + break; + } + } + this.result = POPUP_RESULT.NEGATIVE; + this.hide(); + } + + completeCancelled() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: + case POPUP_TYPE.INPUT: { + this.value = null; + break; + } + } + this.result = POPUP_RESULT.CANCELLED; + this.hide(); + } + + + + hide() { + $(this.dom).transition({ + opacity: 0, + duration: animation_duration, + easing: animation_easing, + }); + delay(animation_duration).then(()=>{ + this.dom.remove(); + }); + + this.resolver(this.value); + } +} + + + +/** + * Displays a blocking popup with a given text and type. + * @param {JQuery|string|Element} text - Text to display in the popup. + * @param {POPUP_TYPE} type + * @param {string} inputValue - Value to set the input to. + * @param {PopupOptions} options - Options for the popup. + * @returns + */ +export function callGenericPopup(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { + const popup = new Popup( + text, + type, + inputValue, + { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling }, + ); + return popup.show(); +} diff --git a/public/style.css b/public/style.css index f6516beed..2ff9ef394 100644 --- a/public/style.css +++ b/public/style.css @@ -2059,7 +2059,8 @@ grammarly-extension { /* Focus */ #bulk_tag_popup, -#dialogue_popup { +#dialogue_popup, +.dialogue_popup { width: 500px; max-width: 90vw; max-width: 90svw; @@ -2112,7 +2113,8 @@ grammarly-extension { } #bulk_tag_popup_holder, -#dialogue_popup_holder { +#dialogue_popup_holder, +.dialogue_popup_holder { display: flex; flex-direction: column; height: 100%; @@ -2120,13 +2122,15 @@ grammarly-extension { padding: 0 10px; } -#dialogue_popup_text { +#dialogue_popup_text, +.dialogue_popup_text { flex-grow: 1; overflow-y: auto; height: 100%; } -#dialogue_popup_controls { +#dialogue_popup_controls, +.dialogue_popup_controls { display: flex; align-self: center; gap: 20px; @@ -2134,14 +2138,16 @@ grammarly-extension { #bulk_tag_popup_reset, #bulk_tag_popup_remove_mutual, -#dialogue_popup_ok { +#dialogue_popup_ok, +.dialogue_popup_ok { background-color: var(--crimson70a); cursor: pointer; } #bulk_tag_popup_reset:hover, #bulk_tag_popup_remove_mutual:hover, -#dialogue_popup_ok:hover { +#dialogue_popup_ok:hover, +.dialogue_popup_ok:hover { background-color: var(--crimson-hover); } @@ -2149,13 +2155,15 @@ grammarly-extension { max-height: 70vh; } -#dialogue_popup_input { +#dialogue_popup_input, +.dialogue_popup_input { margin: 10px 0; width: 100%; } #bulk_tag_popup_cancel, -#dialogue_popup_cancel { +#dialogue_popup_cancel, +.dialogue_popup_cancel { cursor: pointer; } @@ -2220,7 +2228,7 @@ grammarly-extension { margin-right: 25px; } -#shadow_popup { +#shadow_popup, .shadow_popup { backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); background-color: var(--black30a); @@ -2232,6 +2240,9 @@ grammarly-extension { height: 100svh; z-index: 9999; top: 0; + &.shadow_popup { + z-index: 9998; + } } #bgtest {