Merge branch 'staging' into parser-followup-2

This commit is contained in:
LenAnderson
2024-06-27 11:49:46 -04:00
43 changed files with 731 additions and 367 deletions

View File

@ -1,3 +1,4 @@
import { power_user } from './power-user.js';
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
/** @readonly */
@ -11,6 +12,8 @@ export const POPUP_TYPE = {
INPUT: 3,
/** Popup without any button controls. Used to simply display content, with a small X in the corner. */
DISPLAY: 4,
/** Popup that displays an image to crop. Returns a cropped image in result. */
CROP: 5,
};
/** @readonly */
@ -36,12 +39,14 @@ export const POPUP_RESULT = {
* @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
* @property {number?} [cropAspect=null] - Aspect ratio for the crop popup
* @property {string?} [cropImage=null] - Image URL to display in the crop popup
*/
/**
* @typedef {object} CustomPopupButton
* @property {string} text - The text of the button
* @property {POPUP_RESULT|number?} result - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
* @property {POPUP_RESULT|number?} [result] - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
* @property {string[]|string?} [classes] - Optional custom CSS classes applied to the button
* @property {()=>void?} [action] - Optional action to perform when the button is clicked
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
@ -69,6 +74,22 @@ const showPopupHelper = {
const value = await popup.show();
return value ? String(value) : null;
},
/**
* 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;
}
};
export class Popup {
@ -84,6 +105,8 @@ export class Popup {
/** @type {HTMLElement} */ okButton;
/** @type {HTMLElement} */ cancelButton;
/** @type {HTMLElement} */ closeButton;
/** @type {HTMLElement} */ cropWrap;
/** @type {HTMLImageElement} */ cropImage;
/** @type {POPUP_RESULT|number?} */ defaultResult;
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
@ -92,6 +115,7 @@ export class Popup {
/** @type {POPUP_RESULT|number} */ result;
/** @type {any} */ value;
/** @type {any} */ cropData;
/** @type {HTMLElement} */ lastFocus;
@ -106,7 +130,7 @@ export class Popup {
* @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/
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 } = {}) {
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 } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
@ -128,6 +152,8 @@ export class Popup {
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');
@ -159,6 +185,10 @@ export class Popup {
} else {
this.controls.insertBefore(buttonElement, this.okButton);
}
if (typeof button.action === 'function') {
buttonElement.addEventListener('click', button.action);
}
});
// Set the default button class
@ -169,6 +199,7 @@ export class Popup {
// General styling for all types first, that might be overriden for specific types below
this.input.style.display = 'none';
this.closeButton.style.display = 'none';
this.cropWrap.style.display = 'none';
switch (type) {
case POPUP_TYPE.TEXT: {
@ -190,6 +221,22 @@ export class Popup {
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;
@ -220,6 +267,7 @@ export class Popup {
this.dlg.querySelectorAll('[data-result]').forEach(resultControl => {
if (!(resultControl instanceof HTMLElement)) return;
const result = Number(resultControl.dataset.result);
if (String(undefined) === String(resultControl.dataset.result)) return;
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));
@ -347,6 +395,13 @@ export class Popup {
else value = false; // Might a custom negative value?
}
// 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;
}
this.value = value;
this.result = result;