| | import dialogPolyfill from '../lib/dialog-polyfill.esm.js'; |
| | import { shouldSendOnEnter } from './RossAscends-mods.js'; |
| | import { power_user, toastPositionClasses } from './power-user.js'; |
| | import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js'; |
| |
|
| | |
| | |
| | export const POPUP_TYPE = { |
| | |
| | TEXT: 1, |
| | |
| | CONFIRM: 2, |
| | |
| | INPUT: 3, |
| | |
| | DISPLAY: 4, |
| | |
| | CROP: 5, |
| | }; |
| |
|
| | |
| | |
| | export const POPUP_RESULT = { |
| | AFFIRMATIVE: 1, |
| | NEGATIVE: 0, |
| | CANCELLED: null, |
| | CUSTOM1: 1001, |
| | CUSTOM2: 1002, |
| | CUSTOM3: 1003, |
| | CUSTOM4: 1004, |
| | CUSTOM5: 1005, |
| | CUSTOM6: 1006, |
| | CUSTOM7: 1007, |
| | CUSTOM8: 1008, |
| | CUSTOM9: 1009, |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const showPopupHelper = { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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(); |
| | |
| | |
| | if (value === '') return ''; |
| | return value ? String(value) : null; |
| | }, |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | }, |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | text: async (header, text, popupOptions = {}) => { |
| | const content = PopupUtils.BuildTextWithHeader(header, text); |
| | const popup = new Popup(content, POPUP_TYPE.TEXT, null, popupOptions); |
| | const result = await popup.show(); |
| | if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. TEXT popups only support numbers, or null. Result: ${result}`); |
| | return result; |
| | }, |
| | }; |
| |
|
| | export class Popup { |
| | type; |
| |
|
| | id; |
| |
|
| | dlg; |
| | body; |
| | content; |
| | mainInput; |
| | inputControls; |
| | buttonControls; |
| | okButton; |
| | cancelButton; |
| | closeButton; |
| | cropWrap; |
| | cropImage; |
| | defaultResult; |
| | customButtons; |
| | customInputs; |
| |
|
| | onClosing; |
| | onClose; |
| | onOpen; |
| |
|
| | result; |
| | value; |
| | inputResults; |
| | cropData; |
| |
|
| | lastFocus; |
| |
|
| | #promise; |
| | #resolver; |
| | #isClosingPrevented; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, onOpen = null, cropAspect = null, cropImage = null } = {}) { |
| | Popup.util.popups.push(this); |
| |
|
| | |
| | this.id = uuidv4(); |
| | this.type = type; |
| |
|
| | |
| | this.onClosing = onClosing; |
| | this.onClose = onClose; |
| | this.onOpen = onOpen; |
| |
|
| | |
| | const template = document.querySelector('#popup_template'); |
| | |
| | this.dlg = template.content.cloneNode(true).querySelector('.popup'); |
| | if (!this.dlg.showModal) { |
| | this.dlg.classList.add('poly_dialog'); |
| | dialogPolyfill.registerDialog(this.dlg); |
| | |
| | |
| | const resizeObserver = new ResizeObserver((entries) => { |
| | for (const entry of entries) { |
| | dialogPolyfill.reposition(entry.target); |
| | } |
| | }); |
| | resizeObserver.observe(this.dlg); |
| | } |
| | this.body = this.dlg.querySelector('.popup-body'); |
| | this.content = this.dlg.querySelector('.popup-content'); |
| | this.mainInput = this.dlg.querySelector('.popup-input'); |
| | this.inputControls = this.dlg.querySelector('.popup-inputs'); |
| | this.buttonControls = this.dlg.querySelector('.popup-controls'); |
| | this.okButton = this.dlg.querySelector('.popup-button-ok'); |
| | this.cancelButton = this.dlg.querySelector('.popup-button-cancel'); |
| | this.closeButton = this.dlg.querySelector('.popup-button-close'); |
| | this.cropWrap = this.dlg.querySelector('.popup-crop-wrap'); |
| | this.cropImage = this.dlg.querySelector('.popup-crop-image'); |
| |
|
| | 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 (transparent) this.dlg.classList.add('transparent_dialogue_popup'); |
| | if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup'); |
| | if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup'); |
| | if (leftAlign) this.dlg.classList.add('left_aligned_dialogue_popup'); |
| | if (animation) this.dlg.classList.add('popup--animation-' + animation); |
| |
|
| | |
| | this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK'; |
| | this.okButton.dataset.i18n = this.okButton.textContent; |
| | this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel'); |
| | this.cancelButton.dataset.i18n = this.cancelButton.textContent; |
| |
|
| | this.defaultResult = defaultResult; |
| | this.customButtons = customButtons; |
| | this.customButtons?.forEach((x, index) => { |
| | |
| | const button = typeof x === 'string' ? { text: x, result: index + 2 } : x; |
| |
|
| | const buttonElement = document.createElement('div'); |
| | buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control'); |
| | buttonElement.classList.add(...(button.classes ?? [])); |
| | buttonElement.dataset.result = String(button.result); |
| | buttonElement.textContent = button.text; |
| | buttonElement.dataset.i18n = buttonElement.textContent; |
| | buttonElement.tabIndex = 0; |
| |
|
| | if (button.appendAtEnd) { |
| | this.buttonControls.appendChild(buttonElement); |
| | } else { |
| | this.buttonControls.insertBefore(buttonElement, this.okButton); |
| | } |
| |
|
| | if (typeof button.action === 'function') { |
| | buttonElement.addEventListener('click', button.action); |
| | } |
| | }); |
| |
|
| | this.customInputs = customInputs; |
| | this.customInputs?.forEach(input => { |
| | if (!input.id || !(typeof input.id === 'string')) { |
| | console.warn('Given custom input does not have a valid id set'); |
| | return; |
| | } |
| |
|
| | if (!input.type || input.type === 'checkbox') { |
| | const label = document.createElement('label'); |
| | label.classList.add('checkbox_label', 'justifyCenter'); |
| | label.setAttribute('for', input.id); |
| | const inputElement = document.createElement('input'); |
| | inputElement.type = 'checkbox'; |
| | inputElement.id = input.id; |
| | inputElement.checked = Boolean(input.defaultState ?? false); |
| | label.appendChild(inputElement); |
| | const labelText = document.createElement('span'); |
| | labelText.innerText = input.label; |
| | labelText.dataset.i18n = input.label; |
| | label.appendChild(labelText); |
| |
|
| | if (input.tooltip) { |
| | const tooltip = document.createElement('div'); |
| | tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p'); |
| | tooltip.title = input.tooltip; |
| | tooltip.dataset.i18n = '[title]' + input.tooltip; |
| | label.appendChild(tooltip); |
| | } |
| |
|
| | this.inputControls.appendChild(label); |
| | } else if (input.type === 'text') { |
| | const label = document.createElement('label'); |
| | label.classList.add('text_label', 'justifyCenter'); |
| | label.setAttribute('for', input.id); |
| |
|
| | const inputElement = document.createElement('input'); |
| | inputElement.classList.add('text_pole', 'result-control'); |
| | inputElement.type = 'text'; |
| | inputElement.id = input.id; |
| | inputElement.value = String(input.defaultState ?? ''); |
| | inputElement.placeholder = input.tooltip ?? ''; |
| |
|
| | const labelText = document.createElement('span'); |
| | labelText.innerText = input.label; |
| | labelText.dataset.i18n = input.label; |
| |
|
| | label.appendChild(labelText); |
| | label.appendChild(inputElement); |
| |
|
| | this.inputControls.appendChild(label); |
| | } else { |
| | console.warn('Unknown custom input type. Only checkbox and text are supported.', input); |
| | return; |
| | } |
| | }); |
| |
|
| | |
| | const defaultButton = this.buttonControls.querySelector(`[data-result="${this.defaultResult}"]`); |
| | if (defaultButton) defaultButton.classList.add('menu_button_default'); |
| |
|
| | |
| | |
| | this.mainInput.style.display = 'none'; |
| | this.inputControls.style.display = customInputs ? 'block' : 'none'; |
| | this.closeButton.style.display = 'none'; |
| | this.cropWrap.style.display = 'none'; |
| |
|
| | switch (type) { |
| | case POPUP_TYPE.TEXT: { |
| | |
| | if (okButton === false) this.okButton.style.display = 'none'; |
| | if (!cancelButton) this.cancelButton.style.display = 'none'; |
| | break; |
| | } |
| | case POPUP_TYPE.CONFIRM: { |
| | |
| | if (okButton === false) this.okButton.style.display = 'none'; |
| | if (cancelButton === false) this.cancelButton.style.display = 'none'; |
| | |
| | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes'); |
| | if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no'); |
| | break; |
| | } |
| | case POPUP_TYPE.INPUT: { |
| | this.mainInput.style.display = 'block'; |
| | |
| | if (okButton === false) this.okButton.style.display = 'none'; |
| | if (cancelButton === false) this.cancelButton.style.display = 'none'; |
| | |
| | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save'); |
| | break; |
| | } |
| | case POPUP_TYPE.DISPLAY: { |
| | |
| | this.buttonControls.style.display = 'none'; |
| | this.closeButton.style.display = 'block'; |
| | break; |
| | } |
| | case POPUP_TYPE.CROP: { |
| | this.cropWrap.style.display = 'block'; |
| | this.cropImage.src = cropImage; |
| | $(this.cropImage).cropper({ |
| | aspectRatio: cropAspect ?? 2 / 3, |
| | autoCropArea: 1, |
| | viewMode: 2, |
| | rotatable: false, |
| | crop: (event) => { |
| | this.cropData = event.detail; |
| | this.cropData.want_resize = !power_user.never_resize_avatars; |
| | }, |
| | }); |
| | |
| | if (okButton === false) this.okButton.style.display = 'none'; |
| | if (cancelButton === false) this.cancelButton.style.display = 'none'; |
| | |
| | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop'); |
| | break; |
| | } |
| | default: { |
| | console.warn('Unknown popup type.', type); |
| | break; |
| | } |
| | } |
| |
|
| | this.mainInput.value = inputValue; |
| | this.mainInput.rows = rows ?? 1; |
| |
|
| | 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; |
| | } else { |
| | console.warn('Unknown popup text type. Should be jQuery, HTMLElement or string.', content); |
| | } |
| |
|
| | |
| | this.setAutoFocus({ applyAutoFocus: true }); |
| |
|
| | |
| | this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; }); |
| |
|
| | |
| | this.dlg.querySelectorAll('[data-result]').forEach(resultControl => { |
| | if (!(resultControl instanceof HTMLElement)) return; |
| | |
| | if (String(resultControl.dataset.result) === String(undefined)) return; |
| |
|
| | |
| | const result = String(resultControl.dataset.result) === String(null) ? null |
| | : Number(resultControl.dataset.result); |
| |
|
| | if (result !== null && 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, async () => await this.complete(result)); |
| | }); |
| |
|
| | |
| | const cancelListener = async (evt) => { |
| | evt.preventDefault(); |
| | evt.stopPropagation(); |
| | await this.complete(POPUP_RESULT.CANCELLED); |
| | }; |
| | this.dlg.addEventListener('cancel', cancelListener.bind(this)); |
| |
|
| | |
| | |
| | |
| | |
| | const closeListener = async (evt) => { |
| | if (this.#isClosingPrevented) { |
| | evt.preventDefault(); |
| | evt.stopPropagation(); |
| | this.dlg.showModal(); |
| | } |
| | }; |
| | this.dlg.addEventListener('close', closeListener.bind(this)); |
| |
|
| | const keyListener = async (evt) => { |
| | switch (evt.key) { |
| | case 'Enter': { |
| | |
| | if (evt.altKey || evt.shiftKey) |
| | return; |
| |
|
| | |
| | if (this.dlg != document.activeElement?.closest('.popup')) |
| | return; |
| |
|
| | |
| | const resultControl = document.activeElement?.closest('.result-control'); |
| | if (!resultControl) |
| | return; |
| |
|
| | |
| | const textarea = document.activeElement?.closest('textarea'); |
| | if (textarea instanceof HTMLTextAreaElement && !shouldSendOnEnter()) |
| | return; |
| | const input = document.activeElement?.closest('input[type="text"]'); |
| | if (input instanceof HTMLInputElement && !shouldSendOnEnter()) |
| | return; |
| |
|
| | |
| | |
| | if ((textarea instanceof HTMLTextAreaElement || input instanceof HTMLInputElement) |
| | && !evt.ctrlKey && this.mainInput.rows > 1) { |
| | return; |
| | } |
| |
|
| | evt.preventDefault(); |
| | evt.stopPropagation(); |
| | const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult); |
| |
|
| | |
| | await this.complete(result); |
| |
|
| | break; |
| | } |
| | } |
| |
|
| | }; |
| | this.dlg.addEventListener('keydown', keyListener.bind(this)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async show() { |
| | document.body.append(this.dlg); |
| |
|
| | |
| | this.dlg.setAttribute('opening', ''); |
| |
|
| | this.dlg.showModal(); |
| |
|
| | |
| | fixToastrForDialogs(); |
| |
|
| | runAfterAnimation(this.dlg, () => { |
| | this.dlg.removeAttribute('opening'); |
| |
|
| | |
| | if (this.onOpen) { |
| | this.onOpen(this); |
| | } |
| | }); |
| |
|
| | this.#promise = new Promise((resolve) => { |
| | this.#resolver = resolve; |
| | }); |
| | return this.#promise; |
| | } |
| |
|
| | setAutoFocus({ applyAutoFocus = false } = {}) { |
| | |
| | let control; |
| |
|
| | |
| | control = this.dlg.querySelector('[autofocus]'); |
| |
|
| | |
| | if (!control) { |
| | switch (this.type) { |
| | case POPUP_TYPE.INPUT: { |
| | control = this.mainInput; |
| | break; |
| | } |
| | default: |
| | |
| | control = this.buttonControls.querySelector(`[data-result="${this.defaultResult}"]`); |
| | break; |
| | } |
| | } |
| |
|
| | if (applyAutoFocus) { |
| | control.setAttribute('autofocus', ''); |
| | |
| | |
| | control.tabIndex = 0; |
| | } else { |
| | control.focus(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async complete(result) { |
| | |
| | |
| | let value = result; |
| | |
| | if (this.type === POPUP_TYPE.INPUT) { |
| | if (result >= POPUP_RESULT.AFFIRMATIVE) value = this.mainInput.value; |
| | else if (result === POPUP_RESULT.NEGATIVE) value = false; |
| | else if (result === POPUP_RESULT.CANCELLED) value = null; |
| | else value = false; |
| | } |
| |
|
| | |
| | if (this.type === POPUP_TYPE.CROP) { |
| | value = result >= POPUP_RESULT.AFFIRMATIVE |
| | ? $(this.cropImage).data('cropper').getCroppedCanvas().toDataURL('image/jpeg') |
| | : null; |
| | } |
| |
|
| | if (this.customInputs?.length) { |
| | this.inputResults = new Map(this.customInputs.map(input => { |
| | |
| | const inputControl = this.dlg.querySelector(`#${input.id}`); |
| | const value = input.type === 'text' ? inputControl.value : inputControl.checked; |
| | return [inputControl.id, value]; |
| | })); |
| | } |
| |
|
| | this.value = value; |
| | this.result = result; |
| |
|
| | if (this.onClosing) { |
| | const shouldClose = await this.onClosing(this); |
| | if (!shouldClose) { |
| | this.#isClosingPrevented = true; |
| | |
| | this.value = undefined; |
| | this.result = undefined; |
| | this.inputResults = undefined; |
| | return undefined; |
| | } |
| | } |
| | this.#isClosingPrevented = false; |
| |
|
| | Popup.util.lastResult = { value, result, inputResults: this.inputResults }; |
| | this.#hide(); |
| |
|
| | return this.#promise; |
| | } |
| | async completeAffirmative() { |
| | return await this.complete(POPUP_RESULT.AFFIRMATIVE); |
| | } |
| | async completeNegative() { |
| | return await this.complete(POPUP_RESULT.NEGATIVE); |
| | } |
| | async completeCancelled() { |
| | return await this.complete(POPUP_RESULT.CANCELLED); |
| | } |
| |
|
| | |
| | |
| | |
| | #hide() { |
| | |
| | this.dlg.setAttribute('closing', ''); |
| |
|
| | |
| | fixToastrForDialogs(); |
| |
|
| | |
| | runAfterAnimation(this.dlg, async () => { |
| | |
| | this.dlg.close(); |
| |
|
| | |
| | if (this.onClose) { |
| | await this.onClose(this); |
| | } |
| |
|
| | |
| | this.dlg.remove(); |
| |
|
| | |
| | removeFromArray(Popup.util.popups, this); |
| |
|
| | |
| | if (Popup.util.popups.length > 0) { |
| | const activeDialog = document.activeElement?.closest('.popup'); |
| | const id = activeDialog?.getAttribute('data-id'); |
| | const popup = Popup.util.popups.find(x => x.id == id); |
| | if (popup) { |
| | if (popup.lastFocus) popup.lastFocus.focus(); |
| | else popup.setAutoFocus(); |
| | } |
| | } |
| |
|
| | this.#resolver(this.value); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | static show = showPopupHelper; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | static util = { |
| | |
| | popups: [], |
| |
|
| | |
| | lastResult: null, |
| |
|
| | |
| | isPopupOpen() { |
| | return Popup.util.popups.filter(x => x.dlg.hasAttribute('open')).length > 0; |
| | }, |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | getTopmostModalLayer() { |
| | return getTopmostModalLayer(); |
| | }, |
| | }; |
| | } |
| |
|
| | export class PopupUtils { |
| | |
| | |
| | |
| | |
| | |
| | |
| | static BuildTextWithHeader(header, text) { |
| | if (!header) { |
| | return text; |
| | } |
| | return `<h3>${header}</h3> |
| | ${text ?? ''}`; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function callGenericPopup(content, type, inputValue = '', popupOptions = {}) { |
| | const popup = new Popup( |
| | content, |
| | type, |
| | inputValue, |
| | popupOptions, |
| | ); |
| | return popup.show(); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function getTopmostModalLayer() { |
| | const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop(); |
| | if (dlg instanceof HTMLElement) return dlg; |
| | return document.body; |
| | } |
| |
|
| | |
| | |
| | |
| | export function fixToastrForDialogs() { |
| | |
| | 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); |
| | } |
| |
|
| | |
| | |
| | if (dlg && !dlg.contains(toastContainer)) { |
| | dlg?.appendChild(toastContainer); |
| | return; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (!dlg && isAlreadyPresent) { |
| | if (!toastContainer.childNodes.length) { |
| | toastContainer.remove(); |
| | } else { |
| | document.body.appendChild(toastContainer); |
| | toastContainer.classList.remove(...toastPositionClasses); |
| | toastContainer.classList.add(toastr.options.positionClass); |
| | } |
| | } |
| | } |
| |
|