2024-06-25 01:05:35 +02:00
import { power _user } from './power-user.js' ;
2024-06-09 22:02:51 +02:00
import { removeFromArray , runAfterAnimation , uuidv4 } from './utils.js' ;
2024-04-09 00:42:33 +02:00
2024-05-25 00:44:09 +02:00
/** @readonly */
/** @enum {Number} */
2024-04-09 00:42:33 +02:00
export const POPUP _TYPE = {
2024-06-23 02:32:06 +02:00
/** Main popup type. Containing any content displayed, with buttons below. Can also contain additional input controls. */
TEXT : 1 ,
/** Popup mainly made to confirm something, answering with a simple Yes/No or similar. Focus on the button controls. */
CONFIRM : 2 ,
/** Popup who's main focus is the input text field, which is displayed here. Can contain additional content above. Return value for this is the input string. */
INPUT : 3 ,
/** Popup without any button controls. Used to simply display content, with a small X in the corner. */
DISPLAY : 4 ,
2024-06-25 01:05:35 +02:00
/** Popup that displays an image to crop. Returns a cropped image in result. */
CROP : 5 ,
2024-04-09 00:42:33 +02:00
} ;
2024-05-25 00:44:09 +02:00
/** @readonly */
2024-06-09 22:02:51 +02:00
/** @enum {number?} */
2024-04-09 00:42:33 +02:00
export const POPUP _RESULT = {
2024-06-23 02:32:06 +02:00
AFFIRMATIVE : 1 ,
NEGATIVE : 0 ,
CANCELLED : null ,
2024-04-09 00:42:33 +02:00
} ;
2024-05-25 00:44:09 +02:00
/ * *
* @ typedef { object } PopupOptions
2024-06-23 12:26:52 +02:00
* @ property { string | boolean ? } [ okButton = null ] - 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 = null ] - 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 = 1 ] - The number of rows for the input field
* @ property { boolean ? } [ wide = false ] - Whether to display the popup in wide mode ( wide screen , 1 / 1 aspect ratio )
* @ property { boolean ? } [ wider = false ] - Whether to display the popup in wider mode ( just wider , no height scaling )
* @ property { boolean ? } [ large = false ] - Whether to display the popup in large mode ( 90 % of screen )
* @ property { boolean ? } [ transparent = false ] - Whether to display the popup in transparent mode ( no background , border , shadow or anything , only its content )
* @ property { boolean ? } [ allowHorizontalScrolling = false ] - Whether to allow horizontal scrolling in the popup
* @ 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 { ( 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
2024-06-25 01:05:35 +02:00
* @ property { number ? } [ cropAspect = null ] - Aspect ratio for the crop popup
* @ property { string ? } [ cropImage = null ] - Image URL to display in the crop popup
2024-05-25 00:44:09 +02:00
* /
2024-04-09 00:42:33 +02:00
2024-05-25 00:44:09 +02:00
/ * *
* @ typedef { object } CustomPopupButton
* @ property { string } text - The text of the button
2024-06-25 10:54:59 +02:00
* @ 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 .
2024-05-25 00:44:09 +02:00
* @ 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
* /
2024-04-09 00:42:33 +02:00
2024-06-09 22:02:51 +02:00
/ * *
* @ typedef { object } ShowPopupHelper
* Local implementation of the helper functionality to show several popups .
*
* Should be called via ` Popup.show.xxxx() ` .
* /
const showPopupHelper = {
/ * *
* Asynchronously displays an input popup with the given header and text , and returns the user ' s input .
*
* @ param { string } header - The header text for the popup .
* @ param { string } text - The main text for the popup .
* @ param { string } [ defaultValue = '' ] - The default value for the input field .
* @ param { PopupOptions } [ popupOptions = { } ] - Options for the popup .
* @ return { Promise < string ? > } A Promise that resolves with the user ' s input .
* /
input : async ( header , text , defaultValue = '' , popupOptions = { } ) => {
const content = PopupUtils . BuildTextWithHeader ( header , text ) ;
const popup = new Popup ( content , POPUP _TYPE . INPUT , defaultValue , popupOptions ) ;
const value = await popup . show ( ) ;
return value ? String ( value ) : null ;
} ,
2024-06-26 05:29:08 +02:00
/ * *
* 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 { PopupOptions } [ popupOptions = { } ] - Options for the popup .
* @ return { Promise < POPUP _RESULT > } A Promise that resolves with the result of the user ' s interaction .
* /
confirm : async ( header , text , popupOptions = { } ) => {
const content = PopupUtils . BuildTextWithHeader ( header , text ) ;
const popup = new Popup ( content , POPUP _TYPE . CONFIRM , null , popupOptions ) ;
const result = await popup . show ( ) ;
if ( typeof result === 'string' || typeof result === 'boolean' ) throw new Error ( ` Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${ result } ` ) ;
return result ;
}
2024-06-23 22:09:22 +02:00
} ;
2024-06-09 22:02:51 +02:00
2024-04-09 00:42:33 +02:00
export class Popup {
2024-05-30 05:11:23 +02:00
/** @type {POPUP_TYPE} */ type ;
2024-04-09 00:42:33 +02:00
2024-05-30 05:11:23 +02:00
/** @type {string} */ id ;
2024-04-09 00:42:33 +02:00
2024-05-30 05:11:23 +02:00
/** @type {HTMLDialogElement} */ dlg ;
2024-06-09 22:02:51 +02:00
/** @type {HTMLElement} */ body ;
2024-05-30 21:03:52 +02:00
/** @type {HTMLElement} */ content ;
2024-05-30 05:11:23 +02:00
/** @type {HTMLTextAreaElement} */ input ;
/** @type {HTMLElement} */ controls ;
2024-06-23 02:32:06 +02:00
/** @type {HTMLElement} */ okButton ;
/** @type {HTMLElement} */ cancelButton ;
/** @type {HTMLElement} */ closeButton ;
2024-06-25 01:05:35 +02:00
/** @type {HTMLElement} */ cropWrap ;
/** @type {HTMLImageElement} */ cropImage ;
2024-05-30 05:11:23 +02:00
/** @type {POPUP_RESULT|number?} */ defaultResult ;
/** @type {CustomPopupButton[]|string[]?} */ customButtons ;
2024-04-09 00:42:33 +02:00
2024-06-22 04:54:13 +02:00
/** @type {(popup: Popup) => boolean?} */ onClosing ;
/** @type {(popup: Popup) => void?} */ onClose ;
2024-05-30 05:11:23 +02:00
/** @type {POPUP_RESULT|number} */ result ;
/** @type {any} */ value ;
2024-06-25 01:05:35 +02:00
/** @type {any} */ cropData ;
2024-04-09 00:42:33 +02:00
2024-05-30 05:11:23 +02:00
/** @type {HTMLElement} */ lastFocus ;
/** @type {Promise<any>} */ promise ;
/** @type {(result: any) => any} */ resolver ;
2024-04-09 00:42:33 +02:00
/ * *
2024-06-09 22:02:51 +02:00
* Constructs a new Popup object with the given text content , type , inputValue , and options
2024-05-25 00:44:09 +02:00
*
2024-06-09 22:02:51 +02:00
* @ param { JQuery < HTMLElement > | string | Element } content - Text content to display in the popup
2024-05-25 00:44:09 +02:00
* @ 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
2024-04-09 00:42:33 +02:00
* /
2024-06-25 01:05:35 +02:00
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 } = { } ) {
2024-06-09 22:02:51 +02:00
Popup . util . popups . push ( this ) ;
2024-05-30 05:11:23 +02:00
// Make this popup uniquely identifiable
this . id = uuidv4 ( ) ;
2024-04-09 00:42:33 +02:00
this . type = type ;
2024-06-22 04:54:13 +02:00
// Utilize event handlers being passed in
this . onClosing = onClosing ;
this . onClose = onClose ;
2024-04-09 00:42:33 +02:00
/**@type {HTMLTemplateElement}*/
2024-05-31 21:59:26 +02:00
const template = document . querySelector ( '#popup_template' ) ;
2024-04-09 00:42:33 +02:00
// @ts-ignore
2024-05-31 21:59:26 +02:00
this . dlg = template . content . cloneNode ( true ) . querySelector ( '.popup' ) ;
2024-06-09 22:02:51 +02:00
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' ) ;
2024-06-23 02:32:06 +02:00
this . okButton = this . dlg . querySelector ( '.popup-button-ok' ) ;
this . cancelButton = this . dlg . querySelector ( '.popup-button-cancel' ) ;
this . closeButton = this . dlg . querySelector ( '.popup-button-close' ) ;
2024-06-25 01:05:35 +02:00
this . cropWrap = this . dlg . querySelector ( '.popup-crop-wrap' ) ;
this . cropImage = this . dlg . querySelector ( '.popup-crop-image' ) ;
2024-05-30 05:11:23 +02:00
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' ) ;
2024-06-23 12:26:52 +02:00
if ( transparent ) this . dlg . classList . add ( 'transparent_dialogue_popup' ) ;
2024-05-30 05:11:23 +02:00
if ( allowHorizontalScrolling ) this . dlg . classList . add ( 'horizontal_scrolling_dialogue_popup' ) ;
if ( allowVerticalScrolling ) this . dlg . classList . add ( 'vertical_scrolling_dialogue_popup' ) ;
2024-04-09 00:42:33 +02:00
2024-05-25 00:44:09 +02:00
// If custom button captions are provided, we set them beforehand
2024-06-23 02:32:06 +02:00
this . okButton . textContent = typeof okButton === 'string' ? okButton : 'OK' ;
this . cancelButton . textContent = typeof cancelButton === 'string' ? cancelButton : template . getAttribute ( 'popup-button-cancel' ) ;
2024-05-25 00:44:09 +02:00
this . defaultResult = defaultResult ;
this . customButtons = customButtons ;
2024-05-30 05:11:23 +02:00
this . customButtons ? . forEach ( ( x , index ) => {
2024-05-25 00:44:09 +02:00
/** @type {CustomPopupButton} */
const button = typeof x === 'string' ? { text : x , result : index + 2 } : x ;
2024-05-30 05:11:23 +02:00
const buttonElement = document . createElement ( 'div' ) ;
2024-06-09 22:02:51 +02:00
buttonElement . classList . add ( 'menu_button' , 'popup-button-custom' , 'result-control' ) ;
2024-05-25 00:44:09 +02:00
buttonElement . classList . add ( ... ( button . classes ? ? [ ] ) ) ;
2024-06-23 02:32:06 +02:00
buttonElement . dataset . result = String ( button . result ? ? undefined ) ;
2024-05-25 00:44:09 +02:00
buttonElement . textContent = button . text ;
2024-05-30 05:11:23 +02:00
buttonElement . tabIndex = 0 ;
2024-05-25 00:44:09 +02:00
if ( button . appendAtEnd ) {
this . controls . appendChild ( buttonElement ) ;
} else {
2024-06-23 02:32:06 +02:00
this . controls . insertBefore ( buttonElement , this . okButton ) ;
2024-05-25 00:44:09 +02:00
}
2024-06-25 10:54:59 +02:00
if ( typeof button . action === 'function' ) {
buttonElement . addEventListener ( 'click' , button . action ) ;
}
2024-05-25 00:44:09 +02:00
} ) ;
// Set the default button class
const defaultButton = this . controls . querySelector ( ` [data-result=" ${ this . defaultResult } "] ` ) ;
if ( defaultButton ) defaultButton . classList . add ( 'menu_button_default' ) ;
2024-05-20 20:58:45 +02:00
2024-06-23 02:32:06 +02:00
// 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 . closeButton . style . display = 'none' ;
2024-06-25 01:05:35 +02:00
this . cropWrap . style . display = 'none' ;
2024-06-23 02:32:06 +02:00
2024-04-13 14:24:49 +02:00
switch ( type ) {
2024-04-09 00:42:33 +02:00
case POPUP _TYPE . TEXT : {
2024-06-23 02:32:06 +02:00
if ( ! cancelButton ) this . cancelButton . style . display = 'none' ;
2024-04-09 00:42:33 +02:00
break ;
}
case POPUP _TYPE . CONFIRM : {
2024-06-23 02:32:06 +02:00
if ( ! okButton ) this . okButton . textContent = template . getAttribute ( 'popup-button-yes' ) ;
if ( ! cancelButton ) this . cancelButton . textContent = template . getAttribute ( 'popup-button-no' ) ;
2024-04-09 00:42:33 +02:00
break ;
}
case POPUP _TYPE . INPUT : {
this . input . style . display = 'block' ;
2024-06-23 02:32:06 +02:00
if ( ! okButton ) this . okButton . textContent = template . getAttribute ( 'popup-button-save' ) ;
2024-04-09 00:42:33 +02:00
break ;
}
2024-06-23 02:32:06 +02:00
case POPUP _TYPE . DISPLAY : {
this . controls . style . display = 'none' ;
this . closeButton . style . display = 'block' ;
2024-06-23 22:09:22 +02:00
break ;
2024-06-23 02:32:06 +02:00
}
2024-06-25 01:05:35 +02:00
case POPUP _TYPE . CROP : {
this . cropWrap . style . display = 'block' ;
this . cropImage . src = cropImage ;
if ( ! okButton ) this . okButton . textContent = template . getAttribute ( 'popup-button-crop' ) ;
$ ( this . cropImage ) . cropper ( {
aspectRatio : cropAspect ? ? 2 / 3 ,
autoCropArea : 1 ,
viewMode : 2 ,
rotatable : false ,
2024-06-25 01:18:10 +02:00
crop : ( event ) => {
2024-06-25 01:05:35 +02:00
this . cropData = event . detail ;
this . cropData . want _resize = ! power _user . never _resize _avatars ;
} ,
} ) ;
break ;
}
2024-04-09 00:42:33 +02:00
default : {
2024-05-30 05:11:23 +02:00
console . warn ( 'Unknown popup type.' , type ) ;
break ;
2024-04-09 00:42:33 +02:00
}
}
this . input . value = inputValue ;
this . input . rows = rows ? ? 1 ;
2024-06-09 22:02:51 +02:00
this . content . innerHTML = '' ;
if ( content instanceof jQuery ) {
$ ( this . content ) . append ( content ) ;
} else if ( content instanceof HTMLElement ) {
this . content . append ( content ) ;
} else if ( typeof content == 'string' ) {
this . content . innerHTML = content ;
2024-04-09 00:42:33 +02:00
} else {
2024-06-09 22:02:51 +02:00
console . warn ( 'Unknown popup text type. Should be jQuery, HTMLElement or string.' , content ) ;
2024-04-09 00:42:33 +02:00
}
2024-05-30 05:11:23 +02:00
// 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 ; } ) ;
2024-06-23 02:32:06 +02:00
// Bind event listeners for all result controls to their defined event type
2024-06-23 22:09:22 +02:00
this . dlg . querySelectorAll ( '[data-result]' ) . forEach ( resultControl => {
2024-06-23 02:32:06 +02:00
if ( ! ( resultControl instanceof HTMLElement ) ) return ;
const result = Number ( resultControl . dataset . result ) ;
2024-06-25 10:54:59 +02:00
if ( String ( undefined ) === String ( resultControl . dataset . result ) ) return ;
2024-06-23 02:32:06 +02:00
if ( isNaN ( result ) ) throw new Error ( 'Invalid result control. Result must be a number. ' + resultControl . dataset . result ) ;
const type = resultControl . dataset . resultEvent || 'click' ;
resultControl . addEventListener ( type , ( ) => this . complete ( result ) ) ;
} ) ;
2024-06-20 21:43:13 +02:00
// Bind dialog listeners manually, so we can be sure context is preserved
const cancelListener = ( evt ) => {
this . complete ( POPUP _RESULT . CANCELLED ) ;
evt . preventDefault ( ) ;
evt . stopPropagation ( ) ;
window . removeEventListener ( 'cancel' , cancelListenerBound ) ;
} ;
const cancelListenerBound = cancelListener . bind ( this ) ;
this . dlg . addEventListener ( 'cancel' , cancelListenerBound ) ;
2024-04-13 14:24:49 +02:00
const keyListener = ( evt ) => {
2024-04-09 00:42:33 +02:00
switch ( evt . key ) {
2024-05-25 00:44:09 +02:00
case 'Enter' : {
2024-06-03 02:52:54 +02:00
// CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
if ( evt . altKey || evt . shiftKey )
2024-05-30 05:11:23 +02:00
return ;
// Check if we are the currently active popup
2024-05-31 21:59:26 +02:00
if ( this . dlg != document . activeElement ? . closest ( '.popup' ) )
2024-05-30 05:11:23 +02:00
return ;
// Check if the current focus is a result control. Only should we apply the compelete action
2024-06-09 22:02:51 +02:00
const resultControl = document . activeElement ? . closest ( '.result-control' ) ;
2024-05-30 05:11:23 +02:00
if ( ! resultControl )
return ;
const result = Number ( document . activeElement . getAttribute ( 'data-result' ) ? ? this . defaultResult ) ;
this . complete ( result ) ;
evt . preventDefault ( ) ;
evt . stopPropagation ( ) ;
window . removeEventListener ( 'keydown' , keyListenerBound ) ;
2024-05-25 00:44:09 +02:00
break ;
}
2024-04-09 00:42:33 +02:00
}
2024-05-30 05:11:23 +02:00
2024-04-09 00:42:33 +02:00
} ;
const keyListenerBound = keyListener . bind ( this ) ;
2024-05-30 05:11:23 +02:00
this . dlg . addEventListener ( 'keydown' , keyListenerBound ) ;
2024-04-09 00:42:33 +02:00
}
2024-05-25 00:44:09 +02:00
/ * *
* 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 .
* /
2024-04-09 00:42:33 +02:00
async show ( ) {
2024-05-30 05:11:23 +02:00
document . body . append ( this . dlg ) ;
2024-05-25 00:44:09 +02:00
2024-06-02 05:54:41 +02:00
// Run opening animation
this . dlg . setAttribute ( 'opening' , '' ) ;
2024-05-30 05:11:23 +02:00
this . dlg . showModal ( ) ;
2024-04-09 00:42:33 +02:00
2024-05-30 05:11:23 +02:00
// We need to fix the toastr to be present inside this dialog
fixToastrForDialogs ( ) ;
2024-04-09 00:42:33 +02:00
2024-06-02 05:54:41 +02:00
runAfterAnimation ( this . dlg , ( ) => {
this . dlg . removeAttribute ( 'opening' ) ;
2024-06-23 22:09:22 +02:00
} ) ;
2024-06-02 05:54:41 +02:00
2024-04-09 00:42:33 +02:00
this . promise = new Promise ( ( resolve ) => {
this . resolver = resolve ;
} ) ;
return this . promise ;
}
2024-05-30 05:11:23 +02:00
setAutoFocus ( { applyAutoFocus = false } = { } ) {
/** @type {HTMLElement} */
let control ;
2024-04-09 00:42:33 +02:00
2024-05-30 05:11:23 +02:00
// 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 ;
2024-04-09 00:42:33 +02:00
}
}
2024-05-30 05:11:23 +02:00
if ( applyAutoFocus ) {
control . setAttribute ( 'autofocus' , '' ) ;
2024-06-23 02:32:06 +02:00
// Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus
// interactable only gets applied when inserted into the DOM
control . tabIndex = 0 ;
2024-05-30 05:11:23 +02:00
} else {
control . focus ( ) ;
2024-04-09 00:42:33 +02:00
}
}
2024-05-25 00:44:09 +02:00
/ * *
2024-05-30 05:11:23 +02:00
* Completes the popup and sets its result and value
*
* The completion handling will make the popup return the result to the original show promise .
2024-05-25 00:44:09 +02:00
*
2024-05-30 05:11:23 +02:00
* 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 )
2024-05-25 00:44:09 +02:00
* /
2024-05-30 05:11:23 +02:00
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 ;
2024-06-02 00:09:25 +02:00
else if ( result === POPUP _RESULT . NEGATIVE ) value = false ;
else if ( result === POPUP _RESULT . CANCELLED ) value = null ;
2024-05-30 05:11:23 +02:00
else value = false ; // Might a custom negative value?
2024-05-25 00:44:09 +02:00
}
2024-05-30 05:11:23 +02:00
2024-06-25 01:05:35 +02:00
// Cropped image should be returned as a data URL
if ( this . type === POPUP _TYPE . CROP ) {
value = result >= POPUP _RESULT . AFFIRMATIVE
? $ ( this . cropImage ) . data ( 'cropper' ) . getCroppedCanvas ( ) . toDataURL ( 'image/jpeg' )
: null ;
}
2024-05-30 05:11:23 +02:00
this . value = value ;
this . result = result ;
2024-06-22 04:54:13 +02:00
if ( this . onClosing ) {
const shouldClose = this . onClosing ( this ) ;
if ( ! shouldClose ) return ;
}
2024-06-09 22:02:51 +02:00
Popup . util . lastResult = { value , result } ;
2024-05-30 05:11:23 +02:00
this . hide ( ) ;
2024-05-25 00:44:09 +02:00
}
/ * *
* Hides the popup , using the internal resolver to return the value to the original show promise
2024-05-30 05:11:23 +02:00
* @ private
2024-05-25 00:44:09 +02:00
* /
2024-04-09 00:42:33 +02:00
hide ( ) {
2024-05-30 05:11:23 +02:00
// 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 ( ) ;
2024-06-22 04:54:13 +02:00
// Run a possible custom handler right before DOM removal
if ( this . onClose ) {
this . onClose ( this ) ;
}
2024-05-30 05:11:23 +02:00
// Remove it from the dom
this . dlg . remove ( ) ;
// Remove it from the popup references
2024-06-09 22:02:51 +02:00
removeFromArray ( Popup . util . popups , this ) ;
2024-05-30 05:11:23 +02:00
// If there is any popup below this one, see if we can set the focus
2024-06-09 22:02:51 +02:00
if ( Popup . util . popups . length > 0 ) {
2024-05-31 21:59:26 +02:00
const activeDialog = document . activeElement ? . closest ( '.popup' ) ;
2024-05-30 05:11:23 +02:00
const id = activeDialog ? . getAttribute ( 'data-id' ) ;
2024-06-09 22:02:51 +02:00
const popup = Popup . util . popups . find ( x => x . id == id ) ;
2024-05-30 05:11:23 +02:00
if ( popup ) {
if ( popup . lastFocus ) popup . lastFocus . focus ( ) ;
else popup . setAutoFocus ( ) ;
}
}
2024-04-09 00:42:33 +02:00
} ) ;
this . resolver ( this . value ) ;
}
2024-06-09 22:02:51 +02:00
/ * *
* Show a popup with any of the given helper methods . Use ` await ` to make them blocking .
* /
static show = showPopupHelper ;
/ * *
* Utility for popup and popup management .
*
* 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 */
popups : [ ] ,
/** @type {{value: any, result: POPUP_RESULT|number?}?} Last popup result */
lastResult : null ,
2024-06-11 02:22:46 +02:00
2024-06-11 02:25:01 +02:00
/** @returns {boolean} Checks if any modal popup dialog is open */
isPopupOpen ( ) {
2024-06-11 02:22:46 +02:00
return Popup . util . popups . length > 0 ;
} ,
/ * *
2024-06-11 02:25:01 +02:00
* Returns the topmost modal layer in the document . If there is an open dialog popup ,
2024-06-11 02:22:46 +02:00
* it returns the dialog element . Otherwise , it returns the document body .
*
* @ return { HTMLElement } The topmost modal layer element
* /
getTopmostModalLayer ( ) {
return getTopmostModalLayer ( ) ;
} ,
2024-06-23 22:09:22 +02:00
} ;
2024-06-09 22:02:51 +02:00
}
class PopupUtils {
static BuildTextWithHeader ( header , text ) {
return `
< h3 > $ { header } < / h 1 >
$ { text } ` ;
}
2024-04-09 00:42:33 +02:00
}
/ * *
2024-06-09 22:02:51 +02:00
* Displays a blocking popup with a given content and type
2024-06-11 02:22:46 +02:00
*
2024-06-09 22:02:51 +02:00
* @ param { JQuery < HTMLElement > | string | Element } content - Content or text to display in the popup
2024-04-09 00:42:33 +02:00
* @ param { POPUP _TYPE } type
2024-05-25 00:44:09 +02:00
* @ 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
2024-04-09 00:42:33 +02:00
* /
2024-06-09 22:02:51 +02:00
export function callGenericPopup ( content , type , inputValue = '' , popupOptions = { } ) {
2024-04-09 00:42:33 +02:00
const popup = new Popup (
2024-06-09 22:02:51 +02:00
content ,
2024-04-09 00:42:33 +02:00
type ,
inputValue ,
2024-05-25 00:44:09 +02:00
popupOptions ,
2024-04-09 00:42:33 +02:00
) ;
return popup . show ( ) ;
}
2024-05-30 05:11:23 +02:00
2024-06-11 02:22:46 +02:00
/ * *
* Returns the topmost modal layer in the document . If there is an open dialog ,
* it returns the dialog element . Otherwise , it returns the document body .
*
* @ return { HTMLElement } The topmost modal layer element
* /
export function getTopmostModalLayer ( ) {
const dlg = Array . from ( document . querySelectorAll ( 'dialog[open]:not([closing])' ) ) . pop ( ) ;
if ( dlg instanceof HTMLElement ) return dlg ;
return document . body ;
}
2024-06-09 22:02:51 +02:00
/ * *
* Fixes the issue with toastr not displaying on top of the dialog by moving the toastr container inside the dialog or back to the main body
* /
2024-05-30 05:11:23 +02:00
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 ) ;
}
}