mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-15 11:30:09 +01:00
Make generic popups be modal dialogs
- Switch generic popups to actual <dialog> elements - Move toastr settings from html to JS - Add style variable for animation duration (to re-use in CSS) - Remember focus of popup on stacking pop-up close to switch back to the element you started out in - Fix keybinds of popups to only act on actual result-triggering controls - Fix toastr appearing behind popups by dynamically moving the container inside the currently open dialog - Improve autofocus on popup open - Make cleaner and prettier popup animations, and tie them to the animation speed -
This commit is contained in:
parent
311fb261a4
commit
6c3118549f
@ -4848,20 +4848,18 @@
|
||||
</div>
|
||||
<!-- various fullscreen popups -->
|
||||
<template id="shadow_popup_template" data-i18n="[popup_text_save]popup_text_save;[popup_text_yes]popup_text_yes;[popup_text_no]popup_text_no;[popup_text_cancel]popup_text_cancel;[popup_text_import]popup_text_import" popup_text_save="Save" popup_text_yes="Yes" popup_text_no="No" popup_text_cancel="Cancel" popup_text_import="Import"> <!-- localization data holder for popups -->
|
||||
<div class="shadow_popup">
|
||||
<div class="dialogue_popup">
|
||||
<div class="dialogue_popup_holder">
|
||||
<div class="dialogue_popup_text">
|
||||
<h3 class="margin0">text</h3>
|
||||
</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" data-result="1">Delete</div>
|
||||
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
|
||||
</div>
|
||||
<dialog class="dialogue_popup shadow_popup">
|
||||
|
||||
<div class="dialogue_popup_text">
|
||||
<h3 class="margin0">text</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="dialogue_popup_input text_pole result_control" rows="1" data-result="1"></textarea>
|
||||
<div class="dialogue_popup_controls">
|
||||
<div class="dialogue_popup_ok menu_button result_control" data-i18n="Delete" data-result="1" tabindex="0">Delete</div>
|
||||
<div class="dialogue_popup_cancel menu_button result_control" data-i18n="Cancel" data-result="0" tabindex="0">Cancel</div>
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
</template>
|
||||
<div id="shadow_popup">
|
||||
<div id="dialogue_popup">
|
||||
@ -5198,8 +5196,8 @@
|
||||
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder">
|
||||
<span class="tag_folder_indicator"></span>
|
||||
</div>
|
||||
<div class="tagColorPickerHolder"></div>
|
||||
<div class="tagColorPicker2Holder"></div>
|
||||
<div class="tag_view_color_picker" data-value="color"></div>
|
||||
<div class="tag_view_color_picker" data-value="color2"></div>
|
||||
<div class="tag_view_name" contenteditable="true"></div>
|
||||
<div class="tag_view_counter"><span class="tag_view_counter_value"></span> entries</div>
|
||||
<div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
|
||||
@ -6437,15 +6435,6 @@
|
||||
<script type="module" src="scripts/setting-search.js"></script>
|
||||
<script type="module" src="scripts/server-history.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<script>
|
||||
// Configure toast library:
|
||||
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
||||
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
|
||||
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
|
||||
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
|
||||
toastr.options.closeButton = true; // enable a close button
|
||||
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
|
||||
</script>
|
||||
<script>
|
||||
window.addEventListener('load', (event) => {
|
||||
const documentHeight = () => {
|
||||
|
@ -227,7 +227,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from './scripts/popup.js';
|
||||
import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||
import { ScraperManager } from './scripts/scrapers.js';
|
||||
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||
@ -263,6 +263,19 @@ showLoader();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
document.getElementById('preloader').remove();
|
||||
|
||||
// Configure toast library:
|
||||
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
||||
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
|
||||
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
|
||||
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
|
||||
toastr.options.closeButton = true; // enable a close button
|
||||
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
|
||||
toastr.options.onHidden = () => {
|
||||
// If we have any dialog still open, the last "hidden" toastr will remove the toastr-container. We need to keep it alive inside the dialog though
|
||||
// so the toasts still show up inside there.
|
||||
fixToastrForDialogs();
|
||||
}
|
||||
|
||||
// Allow target="_blank" in links
|
||||
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
||||
if ('target' in node) {
|
||||
@ -910,6 +923,8 @@ export function displayOnlineStatus() {
|
||||
*/
|
||||
export function setAnimationDuration(ms = null) {
|
||||
animation_duration = ms ?? ANIMATION_DURATION_DEFAULT;
|
||||
// Set CSS variable to document
|
||||
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
|
||||
}
|
||||
|
||||
export function setActiveCharacter(entityOrKey) {
|
||||
@ -5431,7 +5446,7 @@ export function setSendButtonState(value) {
|
||||
|
||||
async function renameCharacter() {
|
||||
const oldAvatar = characters[this_chid].avatar;
|
||||
const newValue = await callPopup('<h3>New name:</h3>', 'input', characters[this_chid].name);
|
||||
const newValue = await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
|
||||
|
||||
if (newValue && newValue !== characters[this_chid].name) {
|
||||
const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue });
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { animation_duration, animation_easing } from '../script.js';
|
||||
import { delay } from './utils.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { hasAnimation, removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
||||
|
||||
/** @readonly */
|
||||
/** @enum {Number} */
|
||||
@ -17,8 +17,8 @@ export const POPUP_RESULT = {
|
||||
'CANCELLED': undefined,
|
||||
};
|
||||
|
||||
const POPUP_START_Z_INDEX = 9998;
|
||||
let currentPopupZIndex = POPUP_START_Z_INDEX;
|
||||
/** @type {Popup[]} Remember all popups */
|
||||
export const popups = [];
|
||||
|
||||
/**
|
||||
* @typedef {object} PopupOptions
|
||||
@ -44,22 +44,26 @@ let currentPopupZIndex = POPUP_START_Z_INDEX;
|
||||
*/
|
||||
|
||||
export class Popup {
|
||||
/**@type {POPUP_TYPE}*/ type;
|
||||
/** @type {POPUP_TYPE} */ type;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ dlg;
|
||||
/**@type {HTMLElement}*/ text;
|
||||
/**@type {HTMLTextAreaElement}*/ input;
|
||||
/**@type {HTMLElement}*/ ok;
|
||||
/**@type {HTMLElement}*/ cancel;
|
||||
/** @type {string} */ id;
|
||||
|
||||
/**@type {POPUP_RESULT}*/ result;
|
||||
/**@type {any}*/ value;
|
||||
/** @type {HTMLDialogElement} */ dlg;
|
||||
/** @type {HTMLElement} */ text;
|
||||
/** @type {HTMLTextAreaElement} */ input;
|
||||
/** @type {HTMLElement} */ controls;
|
||||
/** @type {HTMLElement} */ ok;
|
||||
/** @type {HTMLElement} */ cancel;
|
||||
/** @type {POPUP_RESULT|number?} */ defaultResult;
|
||||
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
|
||||
|
||||
/**@type {Promise}*/ promise;
|
||||
/**@type {Function}*/ resolver;
|
||||
/** @type {POPUP_RESULT|number} */ result;
|
||||
/** @type {any} */ value;
|
||||
|
||||
/**@type {Function}*/ keyListenerBound;
|
||||
/** @type {HTMLElement} */ lastFocus;
|
||||
|
||||
/** @type {Promise<any>} */ promise;
|
||||
/** @type {(result: any) => any} */ resolver;
|
||||
|
||||
/**
|
||||
* Constructs a new Popup object with the given text, type, inputValue, and options
|
||||
@ -70,26 +74,28 @@ export class Popup {
|
||||
* @param {PopupOptions} [options={}] - Additional options for the popup
|
||||
*/
|
||||
constructor(text, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null } = {}) {
|
||||
popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
this.id = uuidv4();
|
||||
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.controls = this.dom.querySelector('.dialogue_popup_controls');
|
||||
this.ok = this.dom.querySelector('.dialogue_popup_ok');
|
||||
this.cancel = this.dom.querySelector('.dialogue_popup_cancel');
|
||||
this.dlg = template.content.cloneNode(true).querySelector('.dialogue_popup');
|
||||
this.text = this.dlg.querySelector('.dialogue_popup_text');
|
||||
this.input = this.dlg.querySelector('.dialogue_popup_input');
|
||||
this.controls = this.dlg.querySelector('.dialogue_popup_controls');
|
||||
this.ok = this.dlg.querySelector('.dialogue_popup_ok');
|
||||
this.cancel = this.dlg.querySelector('.dialogue_popup_cancel');
|
||||
|
||||
if (wide) dlg.classList.add('wide_dialogue_popup');
|
||||
if (wider) dlg.classList.add('wider_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.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 (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';
|
||||
@ -97,26 +103,25 @@ export class Popup {
|
||||
|
||||
this.defaultResult = defaultResult;
|
||||
this.customButtons = customButtons;
|
||||
this.customButtonElements = this.customButtons?.map((x, index) => {
|
||||
this.customButtons?.forEach((x, index) => {
|
||||
/** @type {CustomPopupButton} */
|
||||
const button = typeof x === 'string' ? { text: x, result: index + 2 } : x;
|
||||
|
||||
const buttonElement = document.createElement('div');
|
||||
|
||||
buttonElement.classList.add('menu_button', 'menu_button_custom');
|
||||
buttonElement.classList.add('menu_button', 'menu_button_custom', 'result_control');
|
||||
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));
|
||||
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);
|
||||
}
|
||||
return buttonElement;
|
||||
});
|
||||
|
||||
// Set the default button class
|
||||
@ -141,7 +146,8 @@ export class Popup {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// illegal argument
|
||||
console.warn('Unknown popup type.', type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,38 +162,57 @@ export class Popup {
|
||||
} else if (typeof text == 'string') {
|
||||
this.text.innerHTML = text;
|
||||
} else {
|
||||
// illegal argument
|
||||
console.warn('Unknown popup text type. Should be jQuery, HTMLElement or string.', text);
|
||||
}
|
||||
|
||||
this.ok.addEventListener('click', () => this.completeAffirmative());
|
||||
this.cancel.addEventListener('click', () => this.completeNegative());
|
||||
// Already prepare the auto-focus control by adding the "autofocus" attribute, this should be respected by showModal()
|
||||
this.setAutoFocus({ applyAutoFocus: true });
|
||||
|
||||
// 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));
|
||||
const keyListener = (evt) => {
|
||||
switch (evt.key) {
|
||||
case 'Escape': {
|
||||
// does it really matter where we check?
|
||||
const topModal = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)?.closest('.shadow_popup');
|
||||
if (topModal == this.dom) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.completeCancelled();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
break;
|
||||
}
|
||||
// Check if we are the currently active popup
|
||||
if (this.dlg != document.activeElement?.closest('.dialogue_popup'))
|
||||
return;
|
||||
|
||||
this.complete(POPUP_RESULT.CANCELLED);
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
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);
|
||||
}
|
||||
if (evt.altKey || evt.ctrlKey || evt.shiftKey)
|
||||
return;
|
||||
|
||||
// Check if we are the currently active popup
|
||||
if (this.dlg != document.activeElement?.closest('.dialogue_popup'))
|
||||
return;
|
||||
|
||||
// Check if the current focus is a result control. Only should we apply the compelete action
|
||||
const resultControl = document.activeElement?.closest('.result_control');
|
||||
if (!resultControl)
|
||||
return;
|
||||
|
||||
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
|
||||
this.complete(result);
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const keyListenerBound = keyListener.bind(this);
|
||||
window.addEventListener('keydown', keyListenerBound);
|
||||
this.dlg.addEventListener('keydown', keyListenerBound);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,26 +222,12 @@ export class Popup {
|
||||
* @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.dlg);
|
||||
|
||||
document.body.append(this.dom);
|
||||
this.dom.style.display = 'block';
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.focus();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.ok.focus();
|
||||
break;
|
||||
}
|
||||
this.dlg.showModal();
|
||||
|
||||
$(this.dom).transition({
|
||||
opacity: 1,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
});
|
||||
// We need to fix the toastr to be present inside this dialog
|
||||
fixToastrForDialogs();
|
||||
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolver = resolve;
|
||||
@ -224,91 +235,94 @@ export class Popup {
|
||||
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;
|
||||
setAutoFocus({ applyAutoFocus = false } = {}) {
|
||||
/** @type {HTMLElement} */
|
||||
let control;
|
||||
|
||||
// Try to find if we have an autofocus control already present
|
||||
control = this.dlg.querySelector('[autofocus]');
|
||||
|
||||
// If not, find the default control for this popup type
|
||||
if (!control) {
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.INPUT: {
|
||||
control = this.input;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Select default button
|
||||
control = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
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;
|
||||
}
|
||||
if (applyAutoFocus) {
|
||||
control.setAttribute('autofocus', '');
|
||||
} else {
|
||||
control.focus();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Completes the popup with a custom result.
|
||||
* Calls into the default three delete states, if a valid `POPUP_RESULT` is provided.
|
||||
* Completes the popup and sets its result and value
|
||||
*
|
||||
* @param {POPUP_RESULT|number} result - The result of the custom action
|
||||
* The completion handling will make the popup return the result to the original show promise.
|
||||
*
|
||||
* There will be two different types of result values:
|
||||
* - popup with `POPUP_TYPE.INPUT` will return the input value - or `false` on negative and `null` on cancelled
|
||||
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
|
||||
*
|
||||
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
complete(result) {
|
||||
// In all cases besides INPUT the popup value should be the result
|
||||
/** @type {POPUP_RESULT|number|boolean|string?} */
|
||||
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.NEGATIVE) value = false;
|
||||
if (result === POPUP_RESULT.CANCELLED) value = null;
|
||||
else value = false; // Might a custom negative value?
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.result = result;
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the popup, using the internal resolver to return the value to the original show promise
|
||||
* @private
|
||||
*/
|
||||
hide() {
|
||||
--currentPopupZIndex;
|
||||
$(this.dom).transition({
|
||||
opacity: 0,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
});
|
||||
delay(animation_duration).then(() => {
|
||||
this.dom.remove();
|
||||
// We close the dialog, first running the animation
|
||||
this.dlg.setAttribute('closing', '');
|
||||
|
||||
// Once the hiding starts, we need to fix the toastr to the layer below
|
||||
fixToastrForDialogs();
|
||||
|
||||
// After the dialog is actually completely closed, remove it from the DOM
|
||||
runAfterAnimation(this.dlg, () => {
|
||||
// Call the close on the dialog
|
||||
this.dlg.close();
|
||||
|
||||
// Remove it from the dom
|
||||
this.dlg.remove();
|
||||
|
||||
// Remove it from the popup references
|
||||
removeFromArray(popups, this);
|
||||
|
||||
// If there is any popup below this one, see if we can set the focus
|
||||
if (popups.length > 0) {
|
||||
const activeDialog = document.activeElement?.closest('.dialogue_popup');
|
||||
const id = activeDialog?.getAttribute('data-id');
|
||||
const popup = popups.find(x => x.id == id);
|
||||
if (popup) {
|
||||
if (popup.lastFocus) popup.lastFocus.focus();
|
||||
else popup.setAutoFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.resolver(this.value);
|
||||
@ -332,3 +346,32 @@ export function callGenericPopup(text, type, inputValue = '', popupOptions = {})
|
||||
);
|
||||
return popup.show();
|
||||
}
|
||||
|
||||
export function fixToastrForDialogs() {
|
||||
// Hacky way of getting toastr to actually display on top of the popup...
|
||||
|
||||
const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop();
|
||||
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
const isAlreadyPresent = !!toastContainer;
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.setAttribute('id', 'toast-container');
|
||||
if (toastr.options.positionClass) toastContainer.classList.add(toastr.options.positionClass);
|
||||
}
|
||||
|
||||
// Check if toastr is already a child. If not, we need to move it inside this dialog.
|
||||
// This is either the existing toastr container or the newly created one.
|
||||
if (dlg && !dlg.contains(toastContainer)) {
|
||||
dlg?.appendChild(toastContainer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now another case is if we only have one popup and that is currently closing. In that case the toastr container exists,
|
||||
// but we don't have an open dialog to move it into. It's just inside the existing one that will be gone in milliseconds.
|
||||
// To prevent new toasts from being showing up in there and then vanish in an instant,
|
||||
// we move the toastr back to the main body
|
||||
if (!dlg && isAlreadyPresent) {
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
}
|
||||
|
@ -1492,7 +1492,7 @@ function onTagCreateClick() {
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
toastr.success('Tag created', 'Create Tag', { showDuration: 60000 });
|
||||
toastr.success('Tag created', 'Create Tag');
|
||||
}
|
||||
|
||||
function appendViewTagToList(list, tag, everything) {
|
||||
|
108
public/style.css
108
public/style.css
@ -105,6 +105,9 @@
|
||||
--avatar-base-border-radius: 2px;
|
||||
--avatar-base-border-radius-round: 50%;
|
||||
--inline-avatar-small-factor: 0.6;
|
||||
|
||||
--animation-duration: 125ms;
|
||||
--animation-duration-slow: calc(var(--animation-duration) * 3);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -3020,8 +3023,7 @@ grammarly-extension {
|
||||
/* Focus */
|
||||
|
||||
#bulk_tag_popup,
|
||||
#dialogue_popup,
|
||||
.dialogue_popup {
|
||||
#dialogue_popup {
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
max-width: 90svw;
|
||||
@ -3047,6 +3049,88 @@ grammarly-extension {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
dialog {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
}
|
||||
|
||||
/* Closed state of the dialog */
|
||||
.dialogue_popup {
|
||||
width: 500px;
|
||||
text-align: center;
|
||||
box-shadow: 0px 0px 14px var(--black70a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
padding: 4px 14px;
|
||||
background-color: var(--SmartThemeBlurTintColor);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
/* Fix weird animation issue with font-scaling during popup open */
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
/* Open state of the dialog */
|
||||
.dialogue_popup[open] {
|
||||
animation: pop-in var(--animation-duration-slow) ease-in-out;
|
||||
}
|
||||
|
||||
.dialogue_popup[closing] {
|
||||
animation: pop-out var(--animation-duration-slow) ease-in-out;
|
||||
}
|
||||
|
||||
.dialogue_popup[open]::backdrop {
|
||||
animation: fade-in var(--animation-duration-slow) ease-in-out;
|
||||
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||
background-color: var(--black30a);
|
||||
}
|
||||
|
||||
.dialogue_popup[closing]::backdrop {
|
||||
animation: fade-out var(--animation-duration-slow) ease-in-out;
|
||||
}
|
||||
|
||||
/* Animation keyframes */
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes pop-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
/** Make the scaling faster on pop-in, otherwise it looks a bit weird **/
|
||||
33% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.rm_stat_block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -3078,8 +3162,7 @@ grammarly-extension {
|
||||
}
|
||||
|
||||
#bulk_tag_popup_holder,
|
||||
#dialogue_popup_holder,
|
||||
.dialogue_popup_holder {
|
||||
#dialogue_popup_holder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@ -3174,8 +3257,7 @@ grammarly-extension {
|
||||
}
|
||||
|
||||
.menu_button.menu_button_default {
|
||||
border: 1px ridge var(--white30a);
|
||||
box-shadow: 0 0 5px var(--SmartThemeBorderColor);
|
||||
box-shadow: 0 0 5px var(--white20a);
|
||||
}
|
||||
|
||||
.menu_button.menu_button_custom {
|
||||
@ -3200,14 +3282,18 @@ grammarly-extension {
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
#dialogue_del_mes .menu_button {
|
||||
dialog.dialogue_popup .menu_button:focus,
|
||||
dialog input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):focus {
|
||||
/** For dialogs, mimic the outline of keyboard navigation for all button focus */
|
||||
outline: 1px solid white;
|
||||
}
|
||||
|
||||
#dialogue_del_mes .menu_button {
|
||||
margin-left: 25px;
|
||||
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);
|
||||
@ -3219,10 +3305,6 @@ grammarly-extension {
|
||||
height: 100svh;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
|
||||
&.shadow_popup {
|
||||
z-index: 9998;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogue_popup.dragover {
|
||||
|
Loading…
x
Reference in New Issue
Block a user