mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-12 09:26:33 +01:00
Expand popup functionality
- Add "custom buttons" functionality, each with their own popup result - Handle 'Enter' by defining a default action - Using default action to style the default button to make the default action visible - Allow override of ok/cancel button on any popup type to display those - Allow multiple popups to overlay each other - Small styling changes for bottom spacing on non-input popups
This commit is contained in:
parent
26572458b6
commit
d9582062d2
@ -4856,8 +4856,8 @@
|
||||
</div>
|
||||
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
|
||||
<div class="dialogue_popup_controls">
|
||||
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</div>
|
||||
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel">Cancel</div>
|
||||
<div class="dialogue_popup_ok menu_button" data-i18n="Delete" data-result="1">Delete</div>
|
||||
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -4871,8 +4871,8 @@
|
||||
</div>
|
||||
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
|
||||
<div id="dialogue_popup_controls">
|
||||
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete">Delete</div>
|
||||
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel">Cancel</div>
|
||||
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete" data-result="1">Delete</div>
|
||||
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,25 +1,46 @@
|
||||
import { animation_duration, animation_easing } from '../script.js';
|
||||
import { delay } from './utils.js';
|
||||
|
||||
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
/** @readonly */
|
||||
/** @enum {Number} */
|
||||
export const POPUP_TYPE = {
|
||||
'TEXT': 1,
|
||||
'CONFIRM': 2,
|
||||
'INPUT': 3,
|
||||
};
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Boolean}*/
|
||||
/** @readonly */
|
||||
/** @enum {number} */
|
||||
export const POPUP_RESULT = {
|
||||
'AFFIRMATIVE': true,
|
||||
'NEGATIVE': false,
|
||||
'AFFIRMATIVE': 1,
|
||||
'NEGATIVE': 0,
|
||||
'CANCELLED': undefined,
|
||||
};
|
||||
|
||||
const POPUP_START_Z_INDEX = 9998;
|
||||
let currentPopupZIndex = POPUP_START_Z_INDEX;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @property {boolean?} [large] - Whether to display the popup in large mode
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CustomPopupButton
|
||||
* @property {string} text - The text of the button
|
||||
* @property {POPUP_RESULT|number?} result - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
|
||||
* @property {string[]|string?} [classes] - Optional custom CSS classes applied to the button
|
||||
* @property {()=>void?} [action] - Optional action to perform when the button is clicked
|
||||
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
|
||||
*/
|
||||
|
||||
export class Popup {
|
||||
/**@type {POPUP_TYPE}*/ type;
|
||||
@ -39,16 +60,15 @@ export class Popup {
|
||||
|
||||
/**@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<HTMLElement>|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.
|
||||
* Constructs a new Popup object with the given text, type, inputValue, and options
|
||||
*
|
||||
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup
|
||||
* @param {POPUP_TYPE} type - The type of the popup
|
||||
* @param {string} [inputValue=''] - The initial value of the input field
|
||||
* @param {PopupOptions} [options={}] - Additional options for the popup
|
||||
*/
|
||||
constructor(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
||||
constructor(text, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null } = {}) {
|
||||
this.type = type;
|
||||
|
||||
/**@type {HTMLTemplateElement}*/
|
||||
@ -60,6 +80,7 @@ export class Popup {
|
||||
this.dlg = dlg;
|
||||
this.text = this.dom.querySelector('.dialogue_popup_text');
|
||||
this.input = this.dom.querySelector('.dialogue_popup_input');
|
||||
this.controls = this.dom.querySelector('.dialogue_popup_controls');
|
||||
this.ok = this.dom.querySelector('.dialogue_popup_ok');
|
||||
this.cancel = this.dom.querySelector('.dialogue_popup_cancel');
|
||||
|
||||
@ -68,24 +89,53 @@ export class 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 ?? template.getAttribute('popup_text_cancel');
|
||||
// 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_text_cancel');
|
||||
|
||||
this.defaultResult = defaultResult;
|
||||
this.customButtons = customButtons;
|
||||
this.customButtonElements = this.customButtons?.map((x, index) => {
|
||||
/** @type {CustomPopupButton} */
|
||||
const button = typeof x === 'string' ? { text: x, result: index + 2 } : x;
|
||||
const buttonElement = document.createElement('button');
|
||||
|
||||
buttonElement.classList.add('menu_button', 'menu_button_custom');
|
||||
buttonElement.classList.add(...(button.classes ?? []));
|
||||
|
||||
buttonElement.textContent = button.text;
|
||||
if (button.action) buttonElement.addEventListener('click', button.action);
|
||||
if (button.result) buttonElement.addEventListener('click', () => this.completeCustom(button.result));
|
||||
|
||||
buttonElement.setAttribute('data-result', String(button.result ?? undefined));
|
||||
|
||||
if (button.appendAtEnd) {
|
||||
this.controls.appendChild(buttonElement);
|
||||
} else {
|
||||
this.controls.insertBefore(buttonElement, this.ok);
|
||||
}
|
||||
return buttonElement;
|
||||
});
|
||||
|
||||
// Set the default button class
|
||||
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
if (defaultButton) defaultButton.classList.add('menu_button_default');
|
||||
|
||||
switch (type) {
|
||||
case POPUP_TYPE.TEXT: {
|
||||
this.input.style.display = 'none';
|
||||
this.cancel.style.display = 'none';
|
||||
if (!cancelButton) this.cancel.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.CONFIRM: {
|
||||
this.input.style.display = 'none';
|
||||
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes');
|
||||
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no');
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup_text_yes');
|
||||
if (!cancelButton) this.cancel.textContent = template.getAttribute('popup_text_no');
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.style.display = 'block';
|
||||
this.ok.textContent = okButton ?? template.getAttribute('popup_text_save');
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup_text_save');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@ -107,13 +157,6 @@ export class Popup {
|
||||
// illegal argument
|
||||
}
|
||||
|
||||
this.input.addEventListener('keydown', (evt) => {
|
||||
if (evt.key != 'Enter' || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.completeAffirmative();
|
||||
});
|
||||
|
||||
this.ok.addEventListener('click', () => this.completeAffirmative());
|
||||
this.cancel.addEventListener('click', () => this.completeNegative());
|
||||
const keyListener = (evt) => {
|
||||
@ -129,13 +172,32 @@ export class Popup {
|
||||
break;
|
||||
}
|
||||
}
|
||||
case 'Enter': {
|
||||
// Only count enter if no modifier key is pressed
|
||||
if (!evt.altKey && !evt.ctrlKey && !evt.shiftKey) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.completeCustom(this.defaultResult);
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const keyListenerBound = keyListener.bind(this);
|
||||
window.addEventListener('keydown', keyListenerBound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously shows the popup element by appending it to the document body,
|
||||
* setting its display to 'block' and focusing on the input if the popup type is INPUT.
|
||||
*
|
||||
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed.
|
||||
*/
|
||||
async show() {
|
||||
// Set z-index, so popups can stack "on top" of each other
|
||||
this.dom.style.zIndex = String(++currentPopupZIndex);
|
||||
|
||||
document.body.append(this.dom);
|
||||
this.dom.style.display = 'block';
|
||||
switch (this.type) {
|
||||
@ -143,6 +205,9 @@ export class Popup {
|
||||
this.input.focus();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.ok.focus();
|
||||
break;
|
||||
}
|
||||
|
||||
$(this.dom).transition({
|
||||
@ -201,7 +266,40 @@ export class Popup {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Completes the popup with a custom result.
|
||||
* Calls into the default three delete states, if a valid `POPUP_RESULT` is provided.
|
||||
*
|
||||
* @param {POPUP_RESULT|number} result - The result of the custom action
|
||||
*/
|
||||
completeCustom(result) {
|
||||
switch (result) {
|
||||
case POPUP_RESULT.AFFIRMATIVE: {
|
||||
this.completeAffirmative();
|
||||
break;
|
||||
}
|
||||
case POPUP_RESULT.NEGATIVE: {
|
||||
this.completeNegative();
|
||||
break;
|
||||
}
|
||||
case POPUP_RESULT.CANCELLED: {
|
||||
this.completeCancelled();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.value = this.type === POPUP_TYPE.INPUT ? this.input.value : result;
|
||||
this.result = result ? POPUP_RESULT.AFFIRMATIVE : POPUP_RESULT.NEGATIVE;
|
||||
this.hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the popup, using the internal resolver to return the value to the original show promise
|
||||
*/
|
||||
hide() {
|
||||
--currentPopupZIndex;
|
||||
$(this.dom).transition({
|
||||
opacity: 0,
|
||||
duration: animation_duration,
|
||||
@ -215,22 +313,20 @@ export class Popup {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Displays a blocking popup with a given text and type.
|
||||
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup.
|
||||
* Displays a blocking popup with a given text and type
|
||||
* @param {JQuery<HTMLElement>|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
|
||||
* @param {string} inputValue - Value to set the input to
|
||||
* @param {PopupOptions} [popupOptions={}] - Options for the popup
|
||||
* @returns {Promise<POPUP_RESULT|string|boolean?>} The value for this popup, which can either be the popup retult or the input value if chosen
|
||||
*/
|
||||
export function callGenericPopup(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
||||
export function callGenericPopup(text, type, inputValue = '', popupOptions = {}) {
|
||||
const popup = new Popup(
|
||||
text,
|
||||
type,
|
||||
inputValue,
|
||||
{ okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling },
|
||||
popupOptions,
|
||||
);
|
||||
return popup.show();
|
||||
}
|
||||
|
@ -3092,6 +3092,7 @@ grammarly-extension {
|
||||
|
||||
#dialogue_popup_controls,
|
||||
.dialogue_popup_controls {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
gap: 20px;
|
||||
@ -3100,7 +3101,8 @@ grammarly-extension {
|
||||
#bulk_tag_popup_reset,
|
||||
#bulk_tag_popup_remove_mutual,
|
||||
#dialogue_popup_ok,
|
||||
.dialogue_popup_ok {
|
||||
.dialogue_popup_ok,
|
||||
.menu_button.dialogue_popup_ok {
|
||||
background-color: var(--crimson70a);
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -3108,7 +3110,8 @@ grammarly-extension {
|
||||
#bulk_tag_popup_reset:hover,
|
||||
#bulk_tag_popup_remove_mutual:hover,
|
||||
#dialogue_popup_ok:hover,
|
||||
.dialogue_popup_ok:hover {
|
||||
.dialogue_popup_ok:hover,
|
||||
.menu_button.dialogue_popup_ok:hover {
|
||||
background-color: var(--crimson-hover);
|
||||
}
|
||||
|
||||
@ -3118,7 +3121,7 @@ grammarly-extension {
|
||||
|
||||
#dialogue_popup_input,
|
||||
.dialogue_popup_input {
|
||||
margin: 10px 0;
|
||||
margin: 10px 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -3166,6 +3169,16 @@ grammarly-extension {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu_button.menu_button_default {
|
||||
border: 1px ridge var(--white30a);
|
||||
box-shadow: 0 0 5px var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
.menu_button.menu_button_custom {
|
||||
/** Custom buttons should not scale to smallest size, otherwise they will always break to multiline */
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.avatar_div .menu_button,
|
||||
.form_create_bottom_buttons_block .menu_button {
|
||||
font-weight: bold;
|
||||
|
Loading…
Reference in New Issue
Block a user