mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02: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:
@ -4856,8 +4856,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
|
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
|
||||||
<div class="dialogue_popup_controls">
|
<div class="dialogue_popup_controls">
|
||||||
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</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">Cancel</div>
|
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -4871,8 +4871,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
|
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
|
||||||
<div id="dialogue_popup_controls">
|
<div id="dialogue_popup_controls">
|
||||||
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete">Delete</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">Cancel</div>
|
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,25 +1,46 @@
|
|||||||
import { animation_duration, animation_easing } from '../script.js';
|
import { animation_duration, animation_easing } from '../script.js';
|
||||||
import { delay } from './utils.js';
|
import { delay } from './utils.js';
|
||||||
|
|
||||||
|
/** @readonly */
|
||||||
|
/** @enum {Number} */
|
||||||
/**@readonly*/
|
|
||||||
/**@enum {Number}*/
|
|
||||||
export const POPUP_TYPE = {
|
export const POPUP_TYPE = {
|
||||||
'TEXT': 1,
|
'TEXT': 1,
|
||||||
'CONFIRM': 2,
|
'CONFIRM': 2,
|
||||||
'INPUT': 3,
|
'INPUT': 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**@readonly*/
|
/** @readonly */
|
||||||
/**@enum {Boolean}*/
|
/** @enum {number} */
|
||||||
export const POPUP_RESULT = {
|
export const POPUP_RESULT = {
|
||||||
'AFFIRMATIVE': true,
|
'AFFIRMATIVE': 1,
|
||||||
'NEGATIVE': false,
|
'NEGATIVE': 0,
|
||||||
'CANCELLED': undefined,
|
'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 {
|
export class Popup {
|
||||||
/**@type {POPUP_TYPE}*/ type;
|
/**@type {POPUP_TYPE}*/ type;
|
||||||
@ -39,16 +60,15 @@ export class Popup {
|
|||||||
|
|
||||||
/**@type {Function}*/ keyListenerBound;
|
/**@type {Function}*/ keyListenerBound;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{okButton?: string, cancelButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - 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 - One of Popup.TYPE
|
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup
|
||||||
* @param {string} inputValue - Value to set the input to.
|
* @param {POPUP_TYPE} type - The type of the popup
|
||||||
* @param {PopupOptions} options - Options for 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;
|
this.type = type;
|
||||||
|
|
||||||
/**@type {HTMLTemplateElement}*/
|
/**@type {HTMLTemplateElement}*/
|
||||||
@ -60,6 +80,7 @@ export class Popup {
|
|||||||
this.dlg = dlg;
|
this.dlg = dlg;
|
||||||
this.text = this.dom.querySelector('.dialogue_popup_text');
|
this.text = this.dom.querySelector('.dialogue_popup_text');
|
||||||
this.input = this.dom.querySelector('.dialogue_popup_input');
|
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.ok = this.dom.querySelector('.dialogue_popup_ok');
|
||||||
this.cancel = this.dom.querySelector('.dialogue_popup_cancel');
|
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 (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup');
|
||||||
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup');
|
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup');
|
||||||
|
|
||||||
this.ok.textContent = okButton ?? 'OK';
|
// If custom button captions are provided, we set them beforehand
|
||||||
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_cancel');
|
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) {
|
switch (type) {
|
||||||
case POPUP_TYPE.TEXT: {
|
case POPUP_TYPE.TEXT: {
|
||||||
this.input.style.display = 'none';
|
this.input.style.display = 'none';
|
||||||
this.cancel.style.display = 'none';
|
if (!cancelButton) this.cancel.style.display = 'none';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case POPUP_TYPE.CONFIRM: {
|
case POPUP_TYPE.CONFIRM: {
|
||||||
this.input.style.display = 'none';
|
this.input.style.display = 'none';
|
||||||
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes');
|
if (!okButton) this.ok.textContent = template.getAttribute('popup_text_yes');
|
||||||
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no');
|
if (!cancelButton) this.cancel.textContent = template.getAttribute('popup_text_no');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case POPUP_TYPE.INPUT: {
|
case POPUP_TYPE.INPUT: {
|
||||||
this.input.style.display = 'block';
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@ -107,13 +157,6 @@ export class Popup {
|
|||||||
// illegal argument
|
// 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.ok.addEventListener('click', () => this.completeAffirmative());
|
||||||
this.cancel.addEventListener('click', () => this.completeNegative());
|
this.cancel.addEventListener('click', () => this.completeNegative());
|
||||||
const keyListener = (evt) => {
|
const keyListener = (evt) => {
|
||||||
@ -129,13 +172,32 @@ export class Popup {
|
|||||||
break;
|
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);
|
const keyListenerBound = keyListener.bind(this);
|
||||||
window.addEventListener('keydown', keyListenerBound);
|
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() {
|
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);
|
document.body.append(this.dom);
|
||||||
this.dom.style.display = 'block';
|
this.dom.style.display = 'block';
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
@ -143,6 +205,9 @@ export class Popup {
|
|||||||
this.input.focus();
|
this.input.focus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
this.ok.focus();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this.dom).transition({
|
$(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() {
|
hide() {
|
||||||
|
--currentPopupZIndex;
|
||||||
$(this.dom).transition({
|
$(this.dom).transition({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: animation_duration,
|
duration: animation_duration,
|
||||||
@ -215,22 +313,20 @@ export class Popup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a blocking popup with a given text and type.
|
* Displays a blocking popup with a given text and type
|
||||||
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup.
|
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup
|
||||||
* @param {POPUP_TYPE} type
|
* @param {POPUP_TYPE} type
|
||||||
* @param {string} inputValue - Value to set the input to.
|
* @param {string} inputValue - Value to set the input to
|
||||||
* @param {PopupOptions} options - Options for the popup.
|
* @param {PopupOptions} [popupOptions={}] - Options for the popup
|
||||||
* @returns
|
* @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(
|
const popup = new Popup(
|
||||||
text,
|
text,
|
||||||
type,
|
type,
|
||||||
inputValue,
|
inputValue,
|
||||||
{ okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling },
|
popupOptions,
|
||||||
);
|
);
|
||||||
return popup.show();
|
return popup.show();
|
||||||
}
|
}
|
||||||
|
@ -3092,6 +3092,7 @@ grammarly-extension {
|
|||||||
|
|
||||||
#dialogue_popup_controls,
|
#dialogue_popup_controls,
|
||||||
.dialogue_popup_controls {
|
.dialogue_popup_controls {
|
||||||
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@ -3100,7 +3101,8 @@ grammarly-extension {
|
|||||||
#bulk_tag_popup_reset,
|
#bulk_tag_popup_reset,
|
||||||
#bulk_tag_popup_remove_mutual,
|
#bulk_tag_popup_remove_mutual,
|
||||||
#dialogue_popup_ok,
|
#dialogue_popup_ok,
|
||||||
.dialogue_popup_ok {
|
.dialogue_popup_ok,
|
||||||
|
.menu_button.dialogue_popup_ok {
|
||||||
background-color: var(--crimson70a);
|
background-color: var(--crimson70a);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -3108,7 +3110,8 @@ grammarly-extension {
|
|||||||
#bulk_tag_popup_reset:hover,
|
#bulk_tag_popup_reset:hover,
|
||||||
#bulk_tag_popup_remove_mutual:hover,
|
#bulk_tag_popup_remove_mutual:hover,
|
||||||
#dialogue_popup_ok:hover,
|
#dialogue_popup_ok:hover,
|
||||||
.dialogue_popup_ok:hover {
|
.dialogue_popup_ok:hover,
|
||||||
|
.menu_button.dialogue_popup_ok:hover {
|
||||||
background-color: var(--crimson-hover);
|
background-color: var(--crimson-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3118,7 +3121,7 @@ grammarly-extension {
|
|||||||
|
|
||||||
#dialogue_popup_input,
|
#dialogue_popup_input,
|
||||||
.dialogue_popup_input {
|
.dialogue_popup_input {
|
||||||
margin: 10px 0;
|
margin: 10px 0 0 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3166,6 +3169,16 @@ grammarly-extension {
|
|||||||
text-align: center;
|
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,
|
.avatar_div .menu_button,
|
||||||
.form_create_bottom_buttons_block .menu_button {
|
.form_create_bottom_buttons_block .menu_button {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -5134,4 +5147,4 @@ body:not(.movingUI) .drawer-content.maximized {
|
|||||||
color: #FAF8F6;
|
color: #FAF8F6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pastel White */
|
/* Pastel White */
|
||||||
|
Reference in New Issue
Block a user