mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into parser-followup-2
This commit is contained in:
@ -37,6 +37,7 @@ export const POPUP_RESULT = {
|
||||
* @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 {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.
|
||||
* @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
|
||||
* @property {number?} [cropAspect=null] - Aspect ratio for the crop popup
|
||||
@ -52,6 +53,14 @@ export const POPUP_RESULT = {
|
||||
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CustomPopupInput
|
||||
* @property {string} id - The id for the html element
|
||||
* @property {string} label - The label text for the input
|
||||
* @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label
|
||||
* @property {boolean?} [defaultState=false] - The default state when opening the popup (false if not set)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ShowPopupHelper
|
||||
* Local implementation of the helper functionality to show several popups.
|
||||
@ -78,8 +87,8 @@ const showPopupHelper = {
|
||||
/**
|
||||
* Asynchronously displays a confirmation popup with the given header and text, returning the clicked result button value.
|
||||
*
|
||||
* @param {string} header - The header text for the popup.
|
||||
* @param {string} text - The main text for the popup.
|
||||
* @param {string?} header - The header text for the popup.
|
||||
* @param {string?} text - The main text for the popup.
|
||||
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
|
||||
* @return {Promise<POPUP_RESULT>} A Promise that resolves with the result of the user's interaction.
|
||||
*/
|
||||
@ -93,34 +102,37 @@ const showPopupHelper = {
|
||||
};
|
||||
|
||||
export class Popup {
|
||||
/** @type {POPUP_TYPE} */ type;
|
||||
/** @readonly @type {POPUP_TYPE} */ type;
|
||||
|
||||
/** @type {string} */ id;
|
||||
/** @readonly @type {string} */ id;
|
||||
|
||||
/** @type {HTMLDialogElement} */ dlg;
|
||||
/** @type {HTMLElement} */ body;
|
||||
/** @type {HTMLElement} */ content;
|
||||
/** @type {HTMLTextAreaElement} */ input;
|
||||
/** @type {HTMLElement} */ controls;
|
||||
/** @type {HTMLElement} */ okButton;
|
||||
/** @type {HTMLElement} */ cancelButton;
|
||||
/** @type {HTMLElement} */ closeButton;
|
||||
/** @type {HTMLElement} */ cropWrap;
|
||||
/** @type {HTMLImageElement} */ cropImage;
|
||||
/** @type {POPUP_RESULT|number?} */ defaultResult;
|
||||
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
|
||||
/** @readonly @type {HTMLDialogElement} */ dlg;
|
||||
/** @readonly @type {HTMLDivElement} */ body;
|
||||
/** @readonly @type {HTMLDivElement} */ content;
|
||||
/** @readonly @type {HTMLTextAreaElement} */ mainInput;
|
||||
/** @readonly @type {HTMLDivElement} */ inputControls;
|
||||
/** @readonly @type {HTMLDivElement} */ buttonControls;
|
||||
/** @readonly @type {HTMLDivElement} */ okButton;
|
||||
/** @readonly @type {HTMLDivElement} */ cancelButton;
|
||||
/** @readonly @type {HTMLDivElement} */ closeButton;
|
||||
/** @readonly @type {HTMLDivElement} */ cropWrap;
|
||||
/** @readonly @type {HTMLImageElement} */ cropImage;
|
||||
/** @readonly @type {POPUP_RESULT|number?} */ defaultResult;
|
||||
/** @readonly @type {CustomPopupButton[]|string[]?} */ customButtons;
|
||||
/** @readonly @type {CustomPopupInput[]} */ customInputs;
|
||||
|
||||
/** @type {(popup: Popup) => boolean?} */ onClosing;
|
||||
/** @type {(popup: Popup) => void?} */ onClose;
|
||||
|
||||
/** @type {POPUP_RESULT|number} */ result;
|
||||
/** @type {any} */ value;
|
||||
/** @type {Map<string,boolean>?} */ inputResults;
|
||||
/** @type {any} */ cropData;
|
||||
|
||||
/** @type {HTMLElement} */ lastFocus;
|
||||
|
||||
/** @type {Promise<any>} */ promise;
|
||||
/** @type {(result: any) => any} */ resolver;
|
||||
/** @type {Promise<any>} */ #promise;
|
||||
/** @type {(result: any) => any} */ #resolver;
|
||||
|
||||
/**
|
||||
* Constructs a new Popup object with the given text content, type, inputValue, and options
|
||||
@ -130,7 +142,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, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null, cropAspect = null, cropImage = 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, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
Popup.util.popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
@ -147,8 +159,9 @@ export class Popup {
|
||||
this.dlg = template.content.cloneNode(true).querySelector('.popup');
|
||||
this.body = this.dlg.querySelector('.popup-body');
|
||||
this.content = this.dlg.querySelector('.popup-content');
|
||||
this.input = this.dlg.querySelector('.popup-input');
|
||||
this.controls = this.dlg.querySelector('.popup-controls');
|
||||
this.mainInput = this.dlg.querySelector('.popup-input');
|
||||
this.inputControls = this.dlg.querySelector('.popup-inputs');
|
||||
this.buttonControls = this.dlg.querySelector('.popup-controls');
|
||||
this.okButton = this.dlg.querySelector('.popup-button-ok');
|
||||
this.cancelButton = this.dlg.querySelector('.popup-button-cancel');
|
||||
this.closeButton = this.dlg.querySelector('.popup-button-close');
|
||||
@ -165,7 +178,9 @@ export class Popup {
|
||||
|
||||
// If custom button captions are provided, we set them beforehand
|
||||
this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
this.okButton.dataset.i18n = this.okButton.textContent;
|
||||
this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
|
||||
this.cancelButton.dataset.i18n = this.cancelButton.textContent;
|
||||
|
||||
this.defaultResult = defaultResult;
|
||||
this.customButtons = customButtons;
|
||||
@ -178,12 +193,13 @@ export class Popup {
|
||||
buttonElement.classList.add(...(button.classes ?? []));
|
||||
buttonElement.dataset.result = String(button.result ?? undefined);
|
||||
buttonElement.textContent = button.text;
|
||||
buttonElement.dataset.i18n = buttonElement.textContent;
|
||||
buttonElement.tabIndex = 0;
|
||||
|
||||
if (button.appendAtEnd) {
|
||||
this.controls.appendChild(buttonElement);
|
||||
this.buttonControls.appendChild(buttonElement);
|
||||
} else {
|
||||
this.controls.insertBefore(buttonElement, this.okButton);
|
||||
this.buttonControls.insertBefore(buttonElement, this.okButton);
|
||||
}
|
||||
|
||||
if (typeof button.action === 'function') {
|
||||
@ -191,13 +207,45 @@ export class Popup {
|
||||
}
|
||||
});
|
||||
|
||||
this.customInputs = customInputs;
|
||||
this.customInputs?.forEach(input => {
|
||||
if (!input.id || !(typeof input.id === 'string')) {
|
||||
console.warn('Given custom input does not have a valid id set')
|
||||
return;
|
||||
}
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.classList.add('checkbox_label', 'justifyCenter');
|
||||
label.setAttribute('for', input.id);
|
||||
const inputElement = document.createElement('input');
|
||||
inputElement.type = 'checkbox';
|
||||
inputElement.id = input.id;
|
||||
inputElement.checked = input.defaultState ?? false;
|
||||
label.appendChild(inputElement);
|
||||
const labelText = document.createElement('span');
|
||||
labelText.innerText = input.label;
|
||||
labelText.dataset.i18n = input.label;
|
||||
label.appendChild(labelText);
|
||||
|
||||
if (input.tooltip) {
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
|
||||
tooltip.title = input.tooltip;
|
||||
tooltip.dataset.i18n = '[title]' + input.tooltip;
|
||||
label.appendChild(tooltip);
|
||||
}
|
||||
|
||||
this.inputControls.appendChild(label);
|
||||
});
|
||||
|
||||
// Set the default button class
|
||||
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
const defaultButton = this.buttonControls.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.mainInput.style.display = 'none';
|
||||
this.inputControls.style.display = customInputs ? 'block' : 'none';
|
||||
this.closeButton.style.display = 'none';
|
||||
this.cropWrap.style.display = 'none';
|
||||
|
||||
@ -212,12 +260,12 @@ export class Popup {
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.style.display = 'block';
|
||||
this.mainInput.style.display = 'block';
|
||||
if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save');
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.DISPLAY: {
|
||||
this.controls.style.display = 'none';
|
||||
this.buttonControls.style.display = 'none';
|
||||
this.closeButton.style.display = 'block';
|
||||
break;
|
||||
}
|
||||
@ -243,8 +291,8 @@ export class Popup {
|
||||
}
|
||||
}
|
||||
|
||||
this.input.value = inputValue;
|
||||
this.input.rows = rows ?? 1;
|
||||
this.mainInput.value = inputValue;
|
||||
this.mainInput.rows = rows ?? 1;
|
||||
|
||||
this.content.innerHTML = '';
|
||||
if (content instanceof jQuery) {
|
||||
@ -335,10 +383,10 @@ export class Popup {
|
||||
this.dlg.removeAttribute('opening');
|
||||
});
|
||||
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolver = resolve;
|
||||
this.#promise = new Promise((resolve) => {
|
||||
this.#resolver = resolve;
|
||||
});
|
||||
return this.promise;
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
setAutoFocus({ applyAutoFocus = false } = {}) {
|
||||
@ -352,12 +400,12 @@ export class Popup {
|
||||
if (!control) {
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.INPUT: {
|
||||
control = this.input;
|
||||
control = this.mainInput;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Select default button
|
||||
control = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
control = this.buttonControls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -389,7 +437,7 @@ export class Popup {
|
||||
let value = result;
|
||||
// Input type have special results, so the input can be accessed directly without the need to save the popup and access both result and value
|
||||
if (this.type === POPUP_TYPE.INPUT) {
|
||||
if (result >= POPUP_RESULT.AFFIRMATIVE) value = this.input.value;
|
||||
if (result >= POPUP_RESULT.AFFIRMATIVE) value = this.mainInput.value;
|
||||
else if (result === POPUP_RESULT.NEGATIVE) value = false;
|
||||
else if (result === POPUP_RESULT.CANCELLED) value = null;
|
||||
else value = false; // Might a custom negative value?
|
||||
@ -402,6 +450,14 @@ export class Popup {
|
||||
: null;
|
||||
}
|
||||
|
||||
if (this.customInputs?.length) {
|
||||
this.inputResults = new Map(this.customInputs.map(input => {
|
||||
/** @type {HTMLInputElement} */
|
||||
const inputControl = this.dlg.querySelector(`#${input.id}`);
|
||||
return [inputControl.id, inputControl.checked];
|
||||
}));
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.result = result;
|
||||
|
||||
@ -410,8 +466,8 @@ export class Popup {
|
||||
if (!shouldClose) return;
|
||||
}
|
||||
|
||||
Popup.util.lastResult = { value, result };
|
||||
this.hide();
|
||||
Popup.util.lastResult = { value, result, inputResults: this.inputResults };
|
||||
this.#hide();
|
||||
}
|
||||
completeAffirmative() {
|
||||
return this.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
@ -425,9 +481,8 @@ export class Popup {
|
||||
|
||||
/**
|
||||
* Hides the popup, using the internal resolver to return the value to the original show promise
|
||||
* @private
|
||||
*/
|
||||
hide() {
|
||||
#hide() {
|
||||
// We close the dialog, first running the animation
|
||||
this.dlg.setAttribute('closing', '');
|
||||
|
||||
@ -460,9 +515,9 @@ export class Popup {
|
||||
else popup.setAutoFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.resolver(this.value);
|
||||
this.#resolver(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -476,10 +531,10 @@ export class Popup {
|
||||
* Contains the list of all currently open popups, and it'll remember the result of the last closed popup.
|
||||
*/
|
||||
static util = {
|
||||
/** @type {Popup[]} Remember all popups */
|
||||
/** @readonly @type {Popup[]} Remember all popups */
|
||||
popups: [],
|
||||
|
||||
/** @type {{value: any, result: POPUP_RESULT|number?}?} Last popup result */
|
||||
/** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, boolean>?}?} Last popup result */
|
||||
lastResult: null,
|
||||
|
||||
/** @returns {boolean} Checks if any modal popup dialog is open */
|
||||
@ -500,9 +555,17 @@ export class Popup {
|
||||
}
|
||||
|
||||
class PopupUtils {
|
||||
/**
|
||||
* Builds popup content with header and text below
|
||||
*
|
||||
* @param {string} header - The header to be added to the text
|
||||
* @param {string} text - The main text content
|
||||
*/
|
||||
static BuildTextWithHeader(header, text) {
|
||||
return `
|
||||
<h3>${header}</h1>
|
||||
if (!header) {
|
||||
return text;
|
||||
}
|
||||
return `<h3>${header}</h3>
|
||||
${text}`;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user