Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/* eslint max-classes-per-file: ["error", 2] */
import {
ModalContainerType, ModalContainer, ModalType, ModalParams, Modal,
} from '@components/modal/modal';
import {isUndefined} from '@components/typeguard';
export interface ConfirmModalContainerType extends ModalContainerType {
message: HTMLElement;
footer: HTMLElement;
closeButton: HTMLElement;
confirmButton: HTMLButtonElement;
}
export interface ConfirmModalType extends ModalType {
modal: ConfirmModalContainerType;
}
export type ConfirmModalParams = ModalParams & {
confirmTitle?: string;
confirmMessage: string;
closeButtonLabel: string;
confirmButtonLabel: string;
confirmButtonClass: string;
confirmCallback: (event: Event) => void,
customButtons: Array<HTMLButtonElement | HTMLAnchorElement>;
}
export type InputConfirmModalParams = Partial<ConfirmModalParams>;
/**
* This class is used to build the modal DOM elements, it is not usable as is because it doesn't even have a show
* method and the elements are created but not added to the DOM. It just creates a basic DOM structure of a
* Bootstrap modal, thus keeping the logic class of the modal separated.
*
* This container is built on the basic ModalContainer and adds some confirm/cancel buttons along with a message
* in the body, it is mostly used as a Rich confirm dialog box.
*/
export class ConfirmModalContainer extends ModalContainer implements ConfirmModalContainerType {
footer!: HTMLElement;
closeButton!: HTMLElement;
confirmButton!: HTMLButtonElement;
/* This constructor is important to force the input type but ESLint is not happy about it*/
/* eslint-disable no-useless-constructor */
constructor(params: ConfirmModalParams) {
super(params);
}
protected buildModalContainer(params: ConfirmModalParams): void {
super.buildModalContainer(params);
// Modal message element
this.message.classList.add('confirm-message');
this.message.innerHTML = params.confirmMessage;
// Modal footer element
this.footer = document.createElement('div');
this.footer.classList.add('modal-footer');
// Modal close button element
this.closeButton = document.createElement('button');
this.closeButton.setAttribute('type', 'button');
this.closeButton.classList.add('btn', 'btn-outline-secondary', 'btn-lg');
this.closeButton.dataset.dismiss = 'modal';
this.closeButton.innerHTML = params.closeButtonLabel;
// Modal confirm button element
this.confirmButton = document.createElement('button');
this.confirmButton.setAttribute('type', 'button');
this.confirmButton.classList.add(
'btn',
params.confirmButtonClass,
'btn-lg',
'btn-confirm-submit',
);
this.confirmButton.dataset.dismiss = 'modal';
this.confirmButton.innerHTML = params.confirmButtonLabel;
// Appending element to the modal
this.footer.append(this.closeButton, ...params.customButtons, this.confirmButton);
this.content.append(this.footer);
}
}
/**
* ConfirmModal component
*
* @param {InputConfirmModalParams} params
* @param {Function} confirmCallback @deprecated You should rely on the confirmCallback param
* @param {Function} cancelCallback @deprecated You should rely on the closeCallback param
*/
export class ConfirmModal extends Modal implements ConfirmModalType {
modal!: ConfirmModalContainerType;
constructor(
inputParams: InputConfirmModalParams,
confirmCallback?: (event: Event) => void,
cancelCallback?: () => void,
) {
let confirmModalCallback: (event: Event) => void;
if (!isUndefined(inputParams.confirmCallback)) {
confirmModalCallback = inputParams.confirmCallback;
} else if (!isUndefined(confirmCallback)) {
confirmModalCallback = confirmCallback;
} else {
// We kept the parameters for backward compatibility, this forces us to keep the param confirmCallback as optional
// but when we remove deprecation it will become mandatory, a confirm callback should always be specified
confirmModalCallback = (): void => {
console.error('No confirm callback provided for ConfirmModal component.');
};
}
const params: ConfirmModalParams = {
id: 'confirm-modal',
confirmMessage: 'Are you sure?',
closeButtonLabel: 'Close',
confirmButtonLabel: 'Accept',
confirmButtonClass: 'btn-primary',
customButtons: [],
closable: false,
modalTitle: inputParams.confirmTitle,
dialogStyle: {},
confirmCallback: confirmModalCallback,
closeCallback: inputParams.closeCallback ?? cancelCallback,
...inputParams,
};
super(params);
}
protected initContainer(params: ConfirmModalParams): void {
this.modal = new ConfirmModalContainer(params);
this.modal.confirmButton.addEventListener('click', params.confirmCallback);
super.initContainer(params);
}
}
export default ConfirmModal;

View File

@@ -0,0 +1,119 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import IframeModal, {
IframeModalParams,
IframeModalType, InputIframeModalParams,
} from '@components/modal/iframe-modal';
export type FormIframeModalType = IframeModalType
export type FormIframeCallbackFunction = (
form: HTMLFormElement,
formData: FormData,
dataAttributes: DOMStringMap | null,
event: Event,
) => void;
export type FormIframeConfirmCallback = (
form: HTMLFormElement,
iframe: HTMLIFrameElement,
event: Event
) => void;
export type FormIframeModalParams = Omit<IframeModalParams, 'iframeUrl' | 'onLoaded' | 'confirmCallback'> & {
formUrl: string;
formSelector: string;
cancelButtonSelector: string;
modalTitle?: string;
onFormLoaded?: FormIframeCallbackFunction,
formConfirmCallback?: FormIframeConfirmCallback,
}
export type InputFormIframeModalParams = Partial<FormIframeModalParams> & {
formUrl: string; // formUrl is mandatory in params
};
/**
* This modal opens an url containing a form inside a modal and watches for the submit (via iframe loading)
* On each load it is able to return data from the form via the onFormLoaded callback
*/
export class FormIframeModal extends IframeModal implements FormIframeModalType {
constructor(
params: InputFormIframeModalParams,
) {
const iframeParams: InputIframeModalParams = {
iframeUrl: params.formUrl,
onLoaded: (iframe: HTMLIFrameElement, event: Event) => {
this.onIframeLoaded(
iframe,
event,
params.onFormLoaded,
params.cancelButtonSelector ?? '.cancel-btn',
params.formSelector ?? 'form',
);
},
confirmCallback: (iframe: HTMLIFrameElement, event: Event) => {
this.onConfirmCallback(iframe, event, params.formConfirmCallback, params.formSelector ?? 'form');
},
...params,
};
super(iframeParams);
}
private onIframeLoaded(
iframe: HTMLIFrameElement,
event: Event,
onFormLoaded: FormIframeCallbackFunction | undefined,
cancelButtonSelector: string,
formSelector: string,
): void {
if (!onFormLoaded) {
return;
}
const iframeForm: HTMLFormElement | null = this.getForm(iframe, formSelector);
if (!iframeForm) {
return;
}
// Close modal when cancel button is clicked
const cancelButtons = iframeForm.querySelectorAll(cancelButtonSelector);
cancelButtons.forEach((cancelButton) => {
cancelButton.addEventListener('click', () => {
this.hide();
});
});
onFormLoaded(iframeForm, new FormData(iframeForm), iframeForm.dataset ?? null, event);
}
private onConfirmCallback(
iframe: HTMLIFrameElement,
event: Event,
formConfirmCallback: FormIframeConfirmCallback | undefined,
formSelector: string,
): void {
if (!formConfirmCallback) {
return;
}
const iframeForm: HTMLFormElement | null = this.getForm(iframe, formSelector);
if (!iframeForm) {
return;
}
formConfirmCallback(iframeForm, iframe, event);
}
private getForm(iframe: HTMLIFrameElement, formSelector: string): HTMLFormElement | null {
if (!iframe.contentWindow) {
return null;
}
return iframe.contentWindow.document.querySelector<HTMLFormElement>(formSelector);
}
}

View File

@@ -0,0 +1,30 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import IframeEvent from '@components/modal/iframe-event';
/**
* Client to integrate in a iframe in order to communicate with the parent window via some events.
* The parent window needs to register to the IframeEvent:
*
* window.addEventListener(IframeEvent.parentWindowEvent, (event: IframeEvent) => {
* if (event.name === 'iframeAction') {
* doAction(event.parameters);
* }
* });
*/
export default class IframeClient {
private iframeWindow: Window;
private parentWindow: Window;
constructor() {
this.iframeWindow = window;
this.parentWindow = this.iframeWindow.parent;
}
dispatchEvent(eventName: string, parameters: any = {}): void {
this.parentWindow.dispatchEvent(new IframeEvent(eventName, parameters));
}
}

View File

@@ -0,0 +1,26 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
export default class IframeEvent extends Event {
static readonly parentWindowEvent: string = 'IframeClientEvent';
private readonly eventName: string;
private readonly eventParameters: any;
constructor(eventName: string, parameters: any = {}) {
super(IframeEvent.parentWindowEvent);
this.eventName = eventName;
this.eventParameters = parameters;
}
get name(): string {
return this.eventName;
}
get parameters(): any {
return this.eventParameters;
}
}

View File

@@ -0,0 +1,342 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/* eslint max-classes-per-file: ["error", 2] */
import ResizeObserver from 'resize-observer-polyfill';
import {
ModalContainerType, ModalContainer, ModalType, ModalParams, Modal,
} from '@components/modal/modal';
import IframeEvent from '@components/modal/iframe-event';
import {isUndefined} from '@components/typeguard';
export interface IframeModalContainerType extends ModalContainerType {
iframe: HTMLIFrameElement;
loader: HTMLElement;
spinner: HTMLElement;
closeButton?: HTMLElement;
confirmButton?: HTMLButtonElement;
}
export interface IframeModalType extends ModalType {
modal: IframeModalContainerType;
render: (content: string, hideIframe?: boolean) => void;
}
export type IframeCallbackFunction = (iframe:HTMLIFrameElement, event: Event) => void;
export type IframeEventCallbackFunction = (event: IframeEvent) => void;
export type IframeModalParams = ModalParams & {
// Callback method executed each time the iframe loads an url
onLoaded?: IframeCallbackFunction,
// Callback method executed each time the iframe is about to unload its content
onUnload?: IframeCallbackFunction,
// The iframe can launch IframeEvent to communicate with its parent via this callback
onIframeEvent?: IframeEventCallbackFunction,
// Initial url of the iframe
iframeUrl: string;
// When true the iframe height is computed based on its content
autoSize: boolean;
// By default the body of the iframe is used as a reference of its content's size but this option can customize it
autoSizeContainer: string;
// Optional, when set a close button is added in the modal's footer
closeButtonLabel?: string;
// Optional, when set a confirm button is added in the modal's footer
confirmButtonLabel?: string;
// Callback when the confirm button is clicked
confirmCallback?: (iframe: HTMLIFrameElement, event: Event) => void;
// By default the iframe closes when confirm button is clicked, this options overrides this behaviour
closeOnConfirm: boolean;
// When the iframe is refreshed auto scroll up the body container (true by default)
autoScrollUp: boolean;
}
export type InputIframeModalParams = Partial<IframeModalParams> & {
iframeUrl: string; // iframeUrl is mandatory in input
};
/**
* This class is used to build the modal DOM elements, it is not usable as is because it doesn't even have a show
* method and the elements are created but not added to the DOM. It just creates a basic DOM structure of a
* Bootstrap modal, thus keeping the logic class of the modal separated.
*
* This container is built on the basic ModalContainer and adds an iframe to load external content along with a
* loader div on top of it.
*
* @param {InputIframeModalParams} inputParams
*/
export class IframeModalContainer extends ModalContainer implements IframeModalContainerType {
iframe!: HTMLIFrameElement;
loader!: HTMLElement;
spinner!: HTMLElement;
footer?: HTMLElement;
closeButton?: HTMLElement;
confirmButton?: HTMLButtonElement;
/* This constructor is important to force the input type but ESLint is not happy about it*/
/* eslint-disable no-useless-constructor */
constructor(params: IframeModalParams) {
super(params);
}
protected buildModalContainer(params: IframeModalParams): void {
super.buildModalContainer(params);
this.container.classList.add('modal-iframe');
// Message is hidden by default
this.message.classList.add('d-none');
this.iframe = document.createElement('iframe');
this.iframe.frameBorder = '0';
this.iframe.scrolling = 'no';
this.iframe.width = '100%';
this.iframe.setAttribute('name', `${params.id}-iframe`);
if (!params.autoSize) {
this.iframe.height = '100%';
}
this.loader = document.createElement('div');
this.loader.classList.add('modal-iframe-loader');
this.spinner = document.createElement('div');
this.spinner.classList.add('spinner');
this.loader.appendChild(this.spinner);
this.body.append(this.loader, this.iframe);
// Modal footer element
if (!isUndefined(params.closeButtonLabel) || !isUndefined(params.confirmButtonLabel)) {
this.footer = document.createElement('div');
this.footer.classList.add('modal-footer');
// Modal close button element
if (!isUndefined(params.closeButtonLabel)) {
this.closeButton = document.createElement('button');
this.closeButton.setAttribute('type', 'button');
this.closeButton.classList.add('btn', 'btn-outline-secondary', 'btn-lg');
this.closeButton.dataset.dismiss = 'modal';
this.closeButton.innerText = params.closeButtonLabel;
this.footer.append(this.closeButton);
}
// Modal confirm button element
if (!isUndefined(params.confirmButtonLabel)) {
this.confirmButton = document.createElement('button');
this.confirmButton.setAttribute('type', 'button');
this.confirmButton.classList.add('btn', 'btn-primary', 'btn-lg', 'btn-confirm-submit');
if (params.closeOnConfirm) {
this.confirmButton.dataset.dismiss = 'modal';
}
this.confirmButton.innerHTML = params.confirmButtonLabel;
this.footer.append(this.confirmButton);
}
// Appending element to the modal
this.content.append(this.footer);
}
}
}
/**
* This modal opens an url inside a modal, it then can handle two specific callbacks
* - onLoaded: called when the iframe has juste been refreshed
* - onUnload: called when the iframe is about to refresh (so it is unloaded)
*/
export class IframeModal extends Modal implements IframeModalType {
modal!: IframeModalContainerType;
protected autoSize!: boolean;
protected autoSizeContainer!: string;
protected resizeObserver?: ResizeObserver | null;
constructor(
inputParams: InputIframeModalParams,
) {
const params: IframeModalParams = {
id: 'iframe-modal',
closable: false,
autoSize: true,
autoSizeContainer: 'body',
closeOnConfirm: true,
autoScrollUp: true,
...inputParams,
};
super(params);
}
protected initContainer(params: IframeModalParams): void {
// Construct the container
this.modal = new IframeModalContainer(params);
super.initContainer(params);
this.autoSize = params.autoSize;
this.autoSizeContainer = params.autoSizeContainer;
this.modal.iframe.addEventListener('load', (loadedEvent: Event) => {
// Scroll the body container back to the top after iframe loaded
this.modal.body.scroll(0, 0);
this.hideLoading();
if (params.onLoaded) {
params.onLoaded(this.modal.iframe, loadedEvent);
}
if (this.modal.iframe.contentWindow) {
this.modal.iframe.contentWindow.addEventListener('beforeunload', (unloadEvent: BeforeUnloadEvent) => {
if (params.onUnload) {
params.onUnload(this.modal.iframe, unloadEvent);
}
this.showLoading();
});
// Auto resize the iframe container
this.initAutoResize();
}
});
this.$modal.on('shown.bs.modal', () => {
this.modal.iframe.src = params.iframeUrl;
});
window.addEventListener(IframeEvent.parentWindowEvent, ((event: IframeEvent) => {
if (params.onIframeEvent) {
params.onIframeEvent(event);
}
}) as EventListener);
if (this.modal.confirmButton && params.confirmCallback) {
this.modal.confirmButton.addEventListener('click', (event) => {
if (params.confirmCallback) {
params.confirmCallback(this.modal.iframe, event);
}
});
}
}
render(content: string, hideIframe: boolean = true, useInnerText: boolean = false): this {
if (useInnerText) {
this.modal.message.innerText = content;
} else {
this.modal.message.innerHTML = content;
}
this.modal.message.classList.remove('d-none');
if (hideIframe) {
this.hideIframe();
}
this.autoResize();
this.hideLoading();
return this;
}
showLoading(): this {
const bodyHeight = this.getOuterHeight(this.modal.body);
const bodyWidth = this.getOuterWidth(this.modal.body);
this.modal.loader.style.height = `${bodyHeight}px`;
this.modal.loader.style.width = `${bodyWidth}px`;
this.modal.loader.classList.remove('d-none');
this.modal.iframe.classList.remove('invisible');
this.modal.iframe.classList.add('invisible');
return this;
}
hideLoading(): this {
this.modal.iframe.classList.remove('invisible');
this.modal.iframe.classList.add('visible');
this.modal.loader.classList.add('d-none');
return this;
}
hide(): this {
super.hide();
this.cleanResizeObserver();
return this;
}
hideIframe(): void {
this.modal.iframe.classList.add('d-none');
}
private getResizableContainer(): HTMLElement | null {
if (this.autoSize && this.modal.iframe.contentWindow) {
return this.modal.iframe.contentWindow.document.querySelector(this.autoSizeContainer);
}
return null;
}
private initAutoResize(): void {
const iframeContainer: HTMLElement | null = this.getResizableContainer();
if (iframeContainer) {
this.cleanResizeObserver();
this.resizeObserver = new ResizeObserver(() => {
this.autoResize();
});
this.resizeObserver.observe(iframeContainer);
}
this.autoResize();
}
private cleanResizeObserver(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
private autoResize(): void {
const iframeContainer: HTMLElement | null = this.getResizableContainer();
if (iframeContainer) {
const iframeScrollHeight = iframeContainer.scrollHeight;
const contentHeight = this.getOuterHeight(this.modal.message)
+ iframeScrollHeight;
// Avoid applying height of 0 (on first load for example)
if (contentHeight) {
// We force the iframe to its real height and it's the container that handles the overflow with scrollbars
this.modal.iframe.style.height = `${contentHeight}px`;
}
}
}
private getOuterHeight(element: HTMLElement): number {
// If the element height is 0 it is likely empty or hidden, then no need to compute the margin
if (!element.offsetHeight) {
return 0;
}
let height = element.offsetHeight;
const style: CSSStyleDeclaration = getComputedStyle(element);
height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);
return height;
}
private getOuterWidth(element: HTMLElement): number {
// If the element height is 0 it is likely empty or hidden, then no need to compute the margin
if (!element.offsetWidth) {
return 0;
}
let width = element.offsetWidth;
const style: CSSStyleDeclaration = getComputedStyle(element);
width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
return width;
}
}
export default IframeModal;

View File

@@ -0,0 +1,227 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/* eslint max-classes-per-file: ["error", 2] */
export interface ModalContainerType {
container: HTMLElement;
dialog: HTMLElement;
content: HTMLElement;
body: HTMLElement;
message: HTMLElement;
header: HTMLElement;
title?: HTMLElement;
closeIcon?: HTMLButtonElement;
}
export interface ModalCoreType {
show: () => void;
hide: () => void;
}
export interface ModalType extends ModalCoreType {
modal: ModalContainerType;
render: (content: string) => void;
}
export type CssProps = Record<string, string>;
export type ModalParams = {
id: string;
closable?: boolean;
modalTitle?: string
dialogStyle?: CssProps;
closeCallback?: () => void;
}
export type InputModalParams = Partial<ModalParams>;
/**
* This class is used to build the modal DOM elements, it is not usable as is because it doesn't even have a show
* method and the elements are created but not added to the DOM. It just creates a basic DOM structure of a
* Bootstrap modal, thus keeping the logic class of the modal separated.
*
* This is the most basic modal container (only the modal and dialog box, with a close icon
* and an optional title). No footer and no content is handled.
*
* @param {ModalParams} params
*/
export class ModalContainer implements ModalContainerType {
container!: HTMLElement;
dialog!: HTMLElement;
content!: HTMLElement;
message!: HTMLElement;
header!: HTMLElement;
title?: HTMLElement;
closeIcon?: HTMLButtonElement;
body!: HTMLElement;
constructor(inputParams: InputModalParams) {
const params: ModalParams = {
id: 'confirm-modal',
closable: false,
...inputParams,
};
this.buildModalContainer(params);
}
protected buildModalContainer(params: ModalParams): void {
// Main modal element
this.container = document.createElement('div');
this.container.classList.add('modal', 'fade');
this.container.id = params.id;
// Modal dialog element
this.dialog = document.createElement('div');
this.dialog.classList.add('modal-dialog');
if (params.dialogStyle) {
Object.keys(params.dialogStyle).forEach((key: string) => {
// @ts-ignore
this.dialog.style[key] = params.dialogStyle[key];
});
}
// Modal content element
this.content = document.createElement('div');
this.content.classList.add('modal-content');
// Modal message element
this.message = document.createElement('p');
this.message.classList.add('modal-message');
// Modal header element
this.header = document.createElement('div');
this.header.classList.add('modal-header');
// Modal title element
if (params.modalTitle) {
this.title = document.createElement('h4');
this.title.classList.add('modal-title');
this.title.innerHTML = params.modalTitle;
}
// Modal close button icon
this.closeIcon = document.createElement('button');
this.closeIcon.classList.add('close');
this.closeIcon.setAttribute('type', 'button');
this.closeIcon.dataset.dismiss = 'modal';
this.closeIcon.innerHTML = '×';
// Modal body element
this.body = document.createElement('div');
this.body.classList.add('modal-body', 'text-left', 'font-weight-normal');
// Constructing the modal
if (this.title) {
this.header.appendChild(this.title);
}
this.header.appendChild(this.closeIcon);
this.content.append(this.header, this.body);
this.body.appendChild(this.message);
this.dialog.appendChild(this.content);
this.container.appendChild(this.dialog);
}
}
/**
* Modal component
*
* @param {InputModalParams} params
* @param {Function} closeCallback
*/
export class Modal implements ModalType {
modal!: ModalContainerType;
protected $modal!: JQuery;
constructor(
inputParams: InputModalParams,
) {
const params: ModalParams = {
id: 'confirm-modal',
closable: false,
dialogStyle: {},
...inputParams,
};
this.initContainer(params);
}
protected initContainer(params: ModalParams): void {
// Construct the modal, check if it already exists This allows child classes to use their custom container
if (!this.modal) {
this.modal = new ModalContainer(params);
}
// jQuery modal object
this.$modal = $(this.modal.container);
const {id, closable} = params;
this.$modal.modal({
backdrop: closable ? true : 'static',
keyboard: closable !== undefined ? closable : true,
});
this.$modal.modal('hide');
this.$modal.on('hidden.bs.modal', () => {
const modal = document.querySelector(`#${id}`);
if (modal) {
modal.remove();
}
if (params.closeCallback) {
params.closeCallback();
}
});
document.body.appendChild(this.modal.container);
}
setTitle(modalTitle: string): this {
if (!this.modal.title) {
this.modal.title = document.createElement('h4');
this.modal.title.classList.add('modal-title');
if (this.modal.closeIcon) {
this.modal.header.insertBefore(this.modal.title, this.modal.closeIcon);
} else {
this.modal.header.appendChild(this.modal.title);
}
}
this.modal.title.innerHTML = modalTitle;
return this;
}
render(content: string): this {
this.modal.message.innerHTML = content;
return this;
}
show(): this {
this.$modal.modal('show');
return this;
}
hide(): this {
this.$modal.modal('hide');
// Sometimes modal animation is still in progress and hiding fails, so we attach event listener for that case.
this.$modal.on('shown.bs.modal', () => {
this.$modal.modal('hide');
this.$modal.off('shown.bs.modal');
});
return this;
}
}
export default Modal;

View File

@@ -0,0 +1,573 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/* eslint max-classes-per-file: ["error", 4] */
import ComponentsMap from '@components/components-map';
import {
ModalParams, Modal, ModalCoreType, ModalContainerType, ModalContainer,
} from '@components/modal/modal';
export interface ProgressModalContainerType extends ModalContainerType {
progressView: ProgressView;
errorView: ErrorView;
switchView(view: string): void;
}
export interface ProgressModalType extends ModalCoreType {
modal: ProgressModalContainerType;
}
export type ProgressModalParams = Omit<ModalParams, 'modalTitle'> & {
/**
* Progression view title (optional) ex: 'Completing %total% actions'
*/
progressionTitle?: string;
/**
* Progression message in progress view Default: 'Processing %done% / %total% elements.'
*/
progressionMessage: string;
/**
* Close button label Default: 'Close'
*/
closeLabel: string;
/**
* Abort process button label Default: 'Stop processing'
*/
abortProcessingLabel: string;
/**
* Errors message in error view, Default: '%error_count% errors occurred. You can download the logs for future reference.'
*/
errorsMessage: string;
/**
* Back to processing button label Default: 'Back to processing'
*/
backToProcessingLabel: string;
/**
* Download error log label Default: 'Download error log'
*/
downloadErrorLogLabel: string;
/**
* View error log label Default: 'View %error_count% error logs'
*/
viewErrorLogLabel: string;
/**
* Error view title Default: 'Error log'
*/
viewErrorTitle: string;
/**
* Total number of elements to process (mandatory)
*/
total: number;
/**
* Additional buttons to display in the progress view footer (optional)
*/
customButtons: Array<HTMLButtonElement | HTMLAnchorElement>;
/**
* Abort callback triggered when the user stops/abort the bulk progress (optional)
*/
abortCallback?: () => void;
}
export type InputProgressModalParams = Partial<ProgressModalParams> & {
/**
* Total must be provided in input params, the other field are either optional or have default values.
*/
total: number;
};
/**
* Common interface for all views
*/
export type ViewContainerType = {
content: HTMLElement;
body: HTMLElement;
message: HTMLElement;
header: HTMLElement;
title?: HTMLElement;
};
const ProgressModalMap = ComponentsMap.progressModal;
export class ProgressModalContainer extends ModalContainer implements ProgressModalContainerType {
static readonly PROGRESS_VIEW: string = 'progress_view';
static readonly ERROR_VIEW: string = 'error_view';
progressView!: ProgressView;
errorView!: ErrorView;
protected currentView!: string;
/* This constructor is important to force the input type but ESLint is not happy about it*/
/* eslint-disable no-useless-constructor */
constructor(params: ProgressModalParams) {
super(params);
}
/**
* This container is a bit different it
*/
protected buildModalContainer(params: ProgressModalParams): void {
this.container = document.createElement('div');
this.container.classList.add('modal', 'fade', ProgressModalMap.classes.modal);
this.container.id = params.id;
// Modal dialog element
this.dialog = document.createElement('div');
this.dialog.classList.add('modal-dialog');
if (params.dialogStyle) {
Object.keys(params.dialogStyle).forEach((key: string) => {
// @ts-ignore
this.dialog.style[key] = params.dialogStyle[key];
});
}
this.progressView = new ProgressView(params);
this.errorView = new ErrorView(params);
this.container.appendChild(this.dialog);
this.toggleView(this.progressView);
this.currentView = ProgressModalContainer.PROGRESS_VIEW;
}
switchView(view: string): void {
if (this.currentView === view) {
return;
}
if (view === ProgressModalContainer.PROGRESS_VIEW) {
this.toggleView(this.progressView);
} else if (view === ProgressModalContainer.ERROR_VIEW) {
this.toggleView(this.errorView);
} else {
console.error(`Unknown view ${view}`);
return;
}
this.currentView = view;
}
protected toggleView(viewContainer: ViewContainerType): void {
if (this.dialog.contains(this.progressView.content)) {
this.dialog.removeChild(this.progressView.content);
}
if (this.dialog.contains(this.errorView.content)) {
this.dialog.removeChild(this.errorView.content);
}
// Update references to modal usual elements
this.content = viewContainer.content;
this.message = viewContainer.message;
this.header = viewContainer.header;
this.title = viewContainer.title;
this.body = viewContainer.body;
this.dialog.appendChild(viewContainer.content);
}
}
export class ProgressView implements ViewContainerType {
footer: HTMLElement;
content: HTMLElement;
message: HTMLElement;
header: HTMLElement;
title?: HTMLElement;
body: HTMLElement;
abortProcessingButton: HTMLElement;
closeModalButton: HTMLElement;
switchToErrorButton: HTMLElement;
progressDone!: HTMLElement;
lastError: HTMLElement;
progressMessage: HTMLElement;
progressPercent: HTMLElement;
progressIcon: HTMLElement;
constructor(params: ProgressModalParams) {
// Modal content element
this.content = document.createElement('div');
this.content.classList.add('modal-content');
// Modal message element
this.message = document.createElement('p');
this.message.classList.add('modal-message');
// Modal header element
this.header = document.createElement('div');
this.header.classList.add('modal-header');
if (params.progressionTitle) {
this.title = document.createElement('h4');
this.title.classList.add('modal-title');
this.title.innerHTML = params.progressionTitle.replace('%total%', String(params.total));
this.header.append(this.title);
}
this.switchToErrorButton = document.createElement('div');
this.switchToErrorButton.classList.add(
ProgressModalMap.classes.switchToErrorButton,
'alert',
'alert-warning',
'd-none',
);
this.switchToErrorButton.innerHTML = params.viewErrorLogLabel.replace('%error_count%', '0');
this.header.append(this.switchToErrorButton);
// Modal body element
this.body = document.createElement('div');
this.body.classList.add('modal-body', 'text-left', 'font-weight-normal');
// Progress headline with icon and progression message embedded in a parent div
const progressHeadline = document.createElement('div');
progressHeadline.classList.add(ProgressModalMap.classes.progressHeadline);
this.progressMessage = document.createElement('div');
this.progressMessage.classList.add(ProgressModalMap.classes.progressMessage);
this.progressMessage.innerHTML = params.progressionMessage
.replace('%done%', '0')
.replace('%total%', String(params.total));
this.progressIcon = document.createElement('span');
this.progressIcon.classList.add(ProgressModalMap.classes.progressIcon);
const spinner = document.createElement('div');
spinner.classList.add('spinner');
this.progressIcon.appendChild(spinner);
this.progressPercent = document.createElement('span');
this.progressPercent.classList.add(ProgressModalMap.classes.progressPercent);
this.progressPercent.innerHTML = '0%';
progressHeadline.append(this.progressIcon);
progressHeadline.append(this.progressMessage);
progressHeadline.append(this.progressPercent);
this.body.append(progressHeadline);
// Then add progress bar
this.body.append(this.buildProgressBar());
this.lastError = document.createElement('div');
this.lastError.classList.add('alert', 'alert-warning', 'd-print-none', 'd-none');
this.body.append(this.lastError);
// Modal footer element
this.footer = document.createElement('div');
this.footer.classList.add('modal-footer');
this.abortProcessingButton = document.createElement('button');
this.abortProcessingButton.setAttribute('type', 'button');
this.abortProcessingButton.classList.add('btn', 'btn-secondary', 'btn-lg', ProgressModalMap.classes.stopProcessing);
this.abortProcessingButton.innerHTML = params.abortProcessingLabel;
this.closeModalButton = document.createElement('button');
this.closeModalButton.setAttribute('type', 'button');
this.closeModalButton.classList.add('btn', 'btn-primary', 'btn-lg', ProgressModalMap.classes.closeModalButton, 'd-none');
this.closeModalButton.innerHTML = params.closeLabel;
this.closeModalButton.dataset.dismiss = 'modal';
// Appending element to the modal
this.footer.append(this.abortProcessingButton, this.closeModalButton, ...params.customButtons);
this.content.append(this.header, this.body, this.footer);
}
protected buildProgressBar(): HTMLElement {
const progressBar = document.createElement('div');
progressBar.setAttribute('style', 'display: block; width: 100%');
progressBar.classList.add(
'progress',
'active',
);
this.progressDone = document.createElement('div');
this.progressDone.classList.add(
'progress-bar',
'progress-bar-success',
);
this.progressDone.setAttribute('style', 'width: 0%');
this.progressDone.setAttribute('role', 'progressbar');
this.progressDone.setAttribute('aria-valuemax', '100');
this.progressDone.id = ProgressModalMap.classes.progressBarDone;
progressBar.append(this.progressDone);
return progressBar;
}
}
export class ErrorView implements ViewContainerType {
footer: HTMLElement;
content: HTMLElement;
message: HTMLElement;
header: HTMLElement;
title?: HTMLElement;
body: HTMLElement;
errorMessage: HTMLElement;
switchToProgressButton: HTMLElement;
downloadErrorsButton: HTMLElement;
errorContainer: HTMLElement;
constructor(params: ProgressModalParams) {
this.content = document.createElement('div');
this.content.classList.add('modal-content');
// Modal message element
this.message = document.createElement('p');
this.message.classList.add('modal-message');
// Modal header element
this.header = document.createElement('div');
this.header.classList.add('modal-header');
// Modal title element
this.title = document.createElement('h4');
this.title.classList.add('modal-title');
this.title.innerHTML = params.viewErrorTitle;
this.header.appendChild(this.title);
this.body = document.createElement('div');
this.body.classList.add('modal-body', 'text-left', 'font-weight-normal');
this.errorMessage = document.createElement('div');
this.errorMessage.classList.add(ProgressModalMap.classes.errorMessage);
this.errorMessage.innerHTML = params.errorsMessage.replace('%error_count%', '0');
this.body.append(this.errorMessage);
this.errorContainer = document.createElement('div');
this.errorContainer.classList.add(
ProgressModalMap.classes.errorContainer,
'd-print-none',
);
this.body.append(this.errorContainer);
this.footer = document.createElement('div');
this.footer.classList.add('modal-footer');
this.switchToProgressButton = document.createElement('div');
this.switchToProgressButton.classList.add(
ProgressModalMap.classes.switchToProgressButton,
'btn',
'btn-secondary',
);
this.switchToProgressButton.innerHTML = params.backToProcessingLabel;
this.downloadErrorsButton = document.createElement('div');
this.downloadErrorsButton.classList.add(
ProgressModalMap.classes.downloadErrorLogButton,
'btn',
'btn-secondary',
);
const downloadIcon = ProgressModal.getProgressIcon('download');
this.downloadErrorsButton.innerHTML = `${downloadIcon.outerHTML} ${params.downloadErrorLogLabel}`;
this.footer.append(this.switchToProgressButton);
this.footer.append(this.downloadErrorsButton);
this.content.append(this.header, this.body, this.footer);
}
}
/**
* ConfirmModal component
*
* @param {InputConfirmModalParams} params
*/
export class ProgressModal extends Modal implements ProgressModalType {
modal!: ProgressModalContainerType;
protected doneCount: number;
protected total: number;
protected errors: Array<string>;
protected progressStopped: boolean;
protected params: ProgressModalParams;
constructor(inputParams: InputProgressModalParams) {
const params: ProgressModalParams = {
id: 'progress-modal',
customButtons: [],
closable: false,
dialogStyle: {},
progressionMessage: 'Processing %done% / %total% elements.',
closeLabel: 'Close',
abortProcessingLabel: 'Stop processing',
errorsMessage: '%error_count% errors occurred. You can download the logs for future reference.',
backToProcessingLabel: 'Back to processing',
downloadErrorLogLabel: 'Download error log',
viewErrorLogLabel: 'View %error_count% error logs',
viewErrorTitle: 'Error log',
...inputParams,
};
super(params);
this.doneCount = 0;
this.total = params.total;
this.errors = [];
this.progressStopped = false;
this.params = params;
}
protected initContainer(params: ProgressModalParams): void {
this.modal = new ProgressModalContainer(params);
super.initContainer(params);
this.initListeners(params);
}
public updateProgress(doneCount: number): void {
this.doneCount = doneCount;
const percentDone = (this.doneCount * 100) / this.total;
this.modal.progressView.progressDone.style.width = `${String(percentDone)}%`;
// This attribute is used in CSS rules for low values
this.modal.progressView.progressDone.setAttribute('aria-valuenow', percentDone.toFixed());
this.modal.progressView.progressMessage.innerHTML = this.params.progressionMessage
.replace('%done%', String(this.doneCount))
.replace('%total%', String(this.params.total));
this.modal.progressView.progressPercent.innerHTML = `${String(percentDone.toFixed())}%`;
}
public addError(error: string): void {
this.errors.push(error);
const errorContent = document.createElement('p');
errorContent.classList.add(ProgressModalMap.classes.progressModalError);
errorContent.append(this.getWarningIcon());
errorContent.append(error);
this.modal.errorView.errorContainer.append(errorContent);
this.modal.progressView.switchToErrorButton.innerHTML = this.params.viewErrorLogLabel.replace(
'%error_count%',
this.errors.length.toString(),
);
this.modal.errorView.errorMessage.innerHTML = this.params.errorsMessage.replace(
'%error_count%',
this.errors.length.toFixed(),
);
this.modal.progressView.lastError.classList.remove('d-none');
this.modal.progressView.lastError.innerHTML = error;
this.modal.progressView.switchToErrorButton.classList.remove('d-none');
}
public completeProgress(): void {
this.stopProgress(this.errors.length > 0 ? this.getWarningIcon() : this.getCompleteIcon());
}
public interruptProgress(): void {
this.stopProgress(this.getStopIcon());
}
protected stopProgress(progressIcon: HTMLElement): void {
if (this.progressStopped) {
return;
}
this.replaceStopProcessButton();
this.modal.progressView.progressIcon.innerHTML = progressIcon.outerHTML;
this.progressStopped = true;
}
protected initListeners(params: ProgressModalParams): void {
this.modal.errorView.downloadErrorsButton.addEventListener('click', () => {
let csvContent = 'data:text/csv;charset=utf-8,';
this.errors.forEach((error) => {
csvContent += `${error}\r\n`;
});
const link = document.createElement('a');
link.href = encodeURI(csvContent);
link.download = 'errors.csv';
link.click();
});
this.modal.errorView.switchToProgressButton.addEventListener('click', () => {
this.modal.switchView(ProgressModalContainer.PROGRESS_VIEW);
});
this.modal.progressView.switchToErrorButton.addEventListener('click', () => {
this.modal.switchView(ProgressModalContainer.ERROR_VIEW);
});
this.modal.progressView.abortProcessingButton.addEventListener('click', () => {
this.interruptProgress();
if (params.abortCallback) {
params.abortCallback();
}
});
this.modal.progressView.closeModalButton.addEventListener('click', () => {
if (params.closeCallback) {
params.closeCallback();
}
});
}
protected replaceStopProcessButton(): void {
this.modal.progressView.abortProcessingButton.classList.add('d-none');
this.modal.progressView.closeModalButton.classList.remove('d-none');
}
protected getWarningIcon(): HTMLElement {
return ProgressModal.getProgressIcon('warning');
}
protected getCompleteIcon(): HTMLElement {
return ProgressModal.getProgressIcon('complete');
}
protected getStopIcon(): HTMLElement {
return ProgressModal.getProgressIcon('stop');
}
public static getProgressIcon(progressStatus: string): HTMLElement {
let iconContent: string;
switch (progressStatus) {
case 'complete':
iconContent = 'check';
break;
case 'stop':
iconContent = 'close';
break;
case 'download':
iconContent = 'file_download';
break;
default:
iconContent = progressStatus;
break;
}
const progressIcon = document.createElement('span');
progressIcon.classList.add(
'material-icons',
ProgressModalMap.classes.progressStatusIcon(progressStatus),
);
progressIcon.innerHTML = iconContent;
return progressIcon;
}
}
export default ProgressModal;