|
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; |
|
|
|
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, cropAspect = null, cropImage = null } = {}) { |
|
Popup.util.popups.push(this); |
|
|
|
|
|
this.id = uuidv4(); |
|
this.type = type; |
|
|
|
|
|
this.onClosing = onClosing; |
|
this.onClose = onClose; |
|
|
|
|
|
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'); |
|
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 (!cancelButton) this.cancelButton.style.display = 'none'; |
|
break; |
|
} |
|
case POPUP_TYPE.CONFIRM: { |
|
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) this.okButton.textContent = template.getAttribute('popup-button-save'); |
|
if (cancelButton === false) this.cancelButton.style.display = 'none'; |
|
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; |
|
if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop'); |
|
$(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; |
|
}, |
|
}); |
|
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; |
|
|
|
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'); |
|
}); |
|
|
|
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(); |
|
}, |
|
}; |
|
} |
|
|
|
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); |
|
} |
|
} |
|
} |
|
|