615 lines
18 KiB
JavaScript
615 lines
18 KiB
JavaScript
/**
|
|
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
|
|
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
|
*/
|
|
|
|
// Default template for the popup
|
|
const popupTemplate = `<div class="joomla-dialog-container">
|
|
<header class="joomla-dialog-header"></header>
|
|
<section class="joomla-dialog-body"></section>
|
|
<footer class="joomla-dialog-footer"></footer>
|
|
</div>`;
|
|
|
|
/**
|
|
* JoomlaDialog class for Joomla Dialog implementation.
|
|
* With use of <joomla-dialog> custom element as dialog holder.
|
|
*/
|
|
class JoomlaDialog extends HTMLElement {
|
|
/**
|
|
* The popup type, supported: inline, iframe, image, ajax.
|
|
* @type {string}
|
|
*/
|
|
// popupType = 'inline';
|
|
|
|
/**
|
|
* An optional text for header.
|
|
* @type {string}
|
|
*/
|
|
// textHeader = '';
|
|
|
|
/**
|
|
* An optional text for close button. Applied when no Buttons provided.
|
|
* @type {string}
|
|
*/
|
|
// textClose = 'Close';
|
|
|
|
/**
|
|
* Content string (html) for inline type popup.
|
|
* @type {string}
|
|
*/
|
|
// popupContent = '';
|
|
|
|
/**
|
|
* Source path for iframe, image, ajax.
|
|
* @type {string}
|
|
*/
|
|
// src = '';
|
|
|
|
/**
|
|
* An optional list of buttons, to be rendered in footer or header, or bottom or top of the popup body.
|
|
* Example:
|
|
* [{label: 'Yes', onClick: () => popup.close()},
|
|
* {label: 'No', onClick: () => popup.close(), className: 'btn btn-danger'},
|
|
* {label: 'Click me', onClick: () => popup.close(), location: 'header'}]
|
|
* @type {[]}
|
|
*/
|
|
// popupButtons = [];
|
|
|
|
/**
|
|
* Whether popup can be closed by Esc button.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
// cancelable = true;
|
|
|
|
/**
|
|
* An optional limit for the popup width, any valid CSS value.
|
|
* @type {string}
|
|
*/
|
|
// width = '';
|
|
|
|
/**
|
|
* An optional height for the popup, any valid CSS value.
|
|
* @type {string}
|
|
*/
|
|
// height = '';
|
|
|
|
/**
|
|
* An optional Class names for header icon.
|
|
*
|
|
* @type {string}
|
|
*/
|
|
// iconHeader = '';
|
|
|
|
/**
|
|
* A template for the popup.
|
|
* @type {string|HTMLTemplateElement}
|
|
*/
|
|
// popupTemplate = popupTemplate;
|
|
|
|
/**
|
|
* The element where to attach the dialog, for cases when no parentElement exist, see show().
|
|
* This allows to keep the dialog in the same branch of DOM as the popupContent.
|
|
* @type {string|HTMLElement}
|
|
*/
|
|
// preferredParent = null;
|
|
|
|
/**
|
|
* Class constructor
|
|
* @param {Object} config
|
|
*/
|
|
constructor(config) {
|
|
super();
|
|
|
|
// Define default params (doing it here because browser support of public props)
|
|
this.popupType = this.getAttribute('type') || 'inline';
|
|
this.textHeader = this.getAttribute('text-header') || '';
|
|
this.iconHeader = '';
|
|
this.textClose = 'Close';
|
|
this.popupContent = '';
|
|
this.src = this.getAttribute('src') || '';
|
|
this.popupButtons = [];
|
|
this.cancelable = !this.hasAttribute('not-cancelable');
|
|
this.width = this.getAttribute('width') || '';
|
|
this.height = this.getAttribute('height') || '';
|
|
this.popupTemplate = popupTemplate;
|
|
this.preferredParent = null;
|
|
// @internal. Parent of the popupContent for cases when it is HTMLElement. Need for recovery on destroy().
|
|
this.popupContentSrcLocation = null;
|
|
if (!config) return;
|
|
|
|
// Check configurable properties
|
|
['popupType', 'textHeader', 'textClose', 'popupContent', 'src', 'popupButtons', 'cancelable', 'width', 'height', 'popupTemplate', 'iconHeader', 'id', 'preferredParent'].forEach(key => {
|
|
if (config[key] !== undefined) {
|
|
this[key] = config[key];
|
|
}
|
|
});
|
|
if (config.className) {
|
|
this.classList.add(...config.className.split(' '));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal. Connected Callback.
|
|
*/
|
|
connectedCallback() {
|
|
this.renderLayout();
|
|
}
|
|
|
|
/**
|
|
* Internal. Render a main layout, based on given template.
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
renderLayout() {
|
|
if (this.dialog) return this;
|
|
|
|
// On close callback
|
|
const onClose = () => {
|
|
this.dispatchEvent(new CustomEvent('joomla-dialog:close', {
|
|
bubbles: true
|
|
}));
|
|
};
|
|
const onCancel = event => {
|
|
if (!this.cancelable) {
|
|
// Prevent closing by Esc
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
// Check for existing layout
|
|
if (this.firstElementChild && this.firstElementChild.nodeName === 'DIALOG') {
|
|
this.dialog = this.firstElementChild;
|
|
this.dialog.addEventListener('cancel', onCancel);
|
|
this.dialog.addEventListener('close', onClose);
|
|
this.popupTmplB = this.querySelector('.joomla-dialog-body') || this.dialog;
|
|
this.popupContentElement = this.popupTmplB;
|
|
return this;
|
|
}
|
|
|
|
// Render a template
|
|
let templateContent;
|
|
if (this.popupTemplate.tagName && this.popupTemplate.tagName === 'TEMPLATE') {
|
|
templateContent = this.popupTemplate.content.cloneNode(true);
|
|
} else {
|
|
const template = document.createElement('template');
|
|
template.innerHTML = this.popupTemplate;
|
|
templateContent = template.content;
|
|
}
|
|
this.dialog = document.createElement('dialog');
|
|
this.dialog.appendChild(templateContent);
|
|
this.dialog.addEventListener('cancel', onCancel);
|
|
this.dialog.addEventListener('close', onClose);
|
|
this.appendChild(this.dialog);
|
|
|
|
// Get template parts
|
|
this.popupTmplH = this.dialog.querySelector('.joomla-dialog-header');
|
|
this.popupTmplB = this.dialog.querySelector('.joomla-dialog-body');
|
|
this.popupTmplF = this.dialog.querySelector('.joomla-dialog-footer');
|
|
this.popupContentElement = null;
|
|
if (!this.popupTmplB) {
|
|
throw new Error('The popup body not found in the template.');
|
|
}
|
|
|
|
// Set the header
|
|
if (this.popupTmplH && this.textHeader) {
|
|
const h = document.createElement('h3');
|
|
h.insertAdjacentHTML('afterbegin', this.textHeader);
|
|
this.popupTmplH.insertAdjacentElement('afterbegin', h);
|
|
if (this.iconHeader) {
|
|
const i = document.createElement('span');
|
|
i.ariaHidden = true;
|
|
i.classList.add('header-icon');
|
|
i.classList.add(...this.iconHeader.split(' '));
|
|
this.popupTmplH.insertAdjacentElement('afterbegin', i);
|
|
}
|
|
}
|
|
|
|
// Set the body
|
|
this.renderBodyContent();
|
|
this.setAttribute('type', this.popupType);
|
|
|
|
// Create buttons if any
|
|
const buttons = this.popupButtons || [];
|
|
|
|
// Add at least one button to close the popup
|
|
if (!buttons.length) {
|
|
buttons.push({
|
|
label: '',
|
|
ariaLabel: this.textClose,
|
|
className: 'button-close btn-close',
|
|
data: {
|
|
buttonClose: ''
|
|
},
|
|
onClick: () => this.close(),
|
|
location: 'header'
|
|
});
|
|
}
|
|
|
|
// Buttons holders
|
|
const btnHHolder = document.createElement('div');
|
|
const btnFHolder = document.createElement('div');
|
|
btnHHolder.classList.add('buttons-holder');
|
|
btnFHolder.classList.add('buttons-holder');
|
|
this.popupButtons.forEach(btnData => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = btnData.label || '';
|
|
btn.ariaLabel = btnData.ariaLabel || null;
|
|
if (btnData.className) {
|
|
btn.classList.add(...btnData.className.split(' '));
|
|
} else {
|
|
btn.classList.add('button', 'button-primary', 'btn', 'btn-primary');
|
|
}
|
|
if (btnData.data) {
|
|
Object.entries(btnData.data).forEach(([k, v]) => {
|
|
btn.dataset[k] = v;
|
|
});
|
|
if (btnData.data.dialogClose !== undefined) {
|
|
btnData.onClick = () => this.close();
|
|
}
|
|
if (btnData.data.dialogDestroy !== undefined) {
|
|
btnData.onClick = () => this.destroy();
|
|
}
|
|
}
|
|
if (btnData.onClick) {
|
|
btn.addEventListener('click', btnData.onClick);
|
|
}
|
|
if (btnData.location === 'header') {
|
|
btnHHolder.appendChild(btn);
|
|
} else {
|
|
btnFHolder.appendChild(btn);
|
|
}
|
|
});
|
|
if (btnHHolder.children.length) {
|
|
if (this.popupType === 'image' && !this.textHeader) {
|
|
this.popupTmplB.insertAdjacentElement('afterbegin', btnHHolder);
|
|
} else if (this.popupTmplH) {
|
|
this.popupTmplH.insertAdjacentElement('beforeend', btnHHolder);
|
|
} else {
|
|
this.popupTmplB.insertAdjacentElement('afterbegin', btnHHolder);
|
|
}
|
|
}
|
|
if (btnFHolder.children.length) {
|
|
(this.popupTmplF || this.popupTmplB).insertAdjacentElement('beforeend', btnFHolder);
|
|
}
|
|
|
|
// Adjust the sizes if requested
|
|
if (this.width) {
|
|
this.dialog.style.width = '100%';
|
|
this.dialog.style.maxWidth = this.width;
|
|
}
|
|
if (this.height) {
|
|
this.dialog.style.height = this.height;
|
|
}
|
|
|
|
// Mark an empty template elements
|
|
if (this.popupTmplH && !this.popupTmplH.children.length) {
|
|
this.popupTmplH.classList.add('empty');
|
|
}
|
|
if (this.popupTmplF && !this.popupTmplF.children.length) {
|
|
this.popupTmplF.classList.add('empty');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Internal. Render the body content, based on popupType.
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
renderBodyContent() {
|
|
if (!this.popupTmplB || this.popupContentElement) return this;
|
|
|
|
// Callback for loaded content event listener
|
|
const onLoad = () => {
|
|
this.classList.add('loaded');
|
|
this.classList.remove('loading');
|
|
this.popupContentElement.removeEventListener('load', onLoad);
|
|
this.dispatchEvent(new CustomEvent('joomla-dialog:load'));
|
|
if (this.popupType === 'inline' || this.popupType === 'ajax') {
|
|
// Dispatch joomla:updated for inline content
|
|
this.popupContentElement.dispatchEvent(new CustomEvent('joomla:updated', {
|
|
bubbles: true,
|
|
cancelable: true
|
|
}));
|
|
}
|
|
};
|
|
this.classList.add('loading');
|
|
switch (this.popupType) {
|
|
// Create an Inline content
|
|
case 'inline':
|
|
{
|
|
let inlineContent = this.popupContent;
|
|
|
|
// Check for content selector: src: '#content-selector' or src: '.content-selector'
|
|
if (!inlineContent && this.src && (this.src[0] === '.' || this.src[0] === '#')) {
|
|
inlineContent = document.querySelector(this.src);
|
|
this.popupContent = inlineContent;
|
|
}
|
|
if (inlineContent instanceof HTMLElement) {
|
|
// Render content provided as HTMLElement
|
|
if (inlineContent.nodeName === 'TEMPLATE') {
|
|
this.popupTmplB.appendChild(inlineContent.content.cloneNode(true));
|
|
} else {
|
|
// Store parent reference to be able to recover after the popup is destroyed
|
|
this.popupContentSrcLocation = {
|
|
parent: inlineContent.parentElement,
|
|
nextSibling: inlineContent.nextSibling
|
|
};
|
|
this.popupTmplB.appendChild(inlineContent);
|
|
}
|
|
} else {
|
|
// Render content string
|
|
this.popupTmplB.insertAdjacentHTML('afterbegin', Joomla.sanitizeHtml(inlineContent));
|
|
}
|
|
this.popupContentElement = this.popupTmplB;
|
|
onLoad();
|
|
break;
|
|
}
|
|
|
|
// Create an IFrame content
|
|
case 'iframe':
|
|
{
|
|
const frame = document.createElement('iframe');
|
|
frame.addEventListener('load', onLoad);
|
|
frame.src = this.src;
|
|
// Enlarge default size of iframe, make sure it is usable without extra styling
|
|
frame.width = '100%';
|
|
frame.height = '720';
|
|
if (!this.width) {
|
|
frame.style.maxWidth = '100%';
|
|
frame.width = '1024';
|
|
}
|
|
frame.classList.add('iframe-content');
|
|
this.popupContentElement = frame;
|
|
this.popupTmplB.appendChild(frame);
|
|
break;
|
|
}
|
|
|
|
// Create an Image content
|
|
case 'image':
|
|
{
|
|
const img = document.createElement('img');
|
|
img.addEventListener('load', onLoad);
|
|
img.src = this.src;
|
|
this.popupContentElement = img;
|
|
this.popupTmplB.appendChild(img);
|
|
break;
|
|
}
|
|
|
|
// Create an AJAX content
|
|
case 'ajax':
|
|
{
|
|
fetch(this.src).then(response => {
|
|
if (response.status !== 200) {
|
|
throw new Error(response.statusText);
|
|
}
|
|
return response.text();
|
|
}).then(text => {
|
|
this.popupTmplB.insertAdjacentHTML('afterbegin', Joomla.sanitizeHtml(text));
|
|
this.popupContentElement = this.popupTmplB;
|
|
onLoad();
|
|
}).catch(error => {
|
|
throw error;
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
throw new Error('Unknown popup type requested');
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Internal. Find an Element to be used as parent element,
|
|
* for cases when Dialog does not have one already. See show().
|
|
*
|
|
* @returns {HTMLElement|boolean}
|
|
*/
|
|
findPreferredParent() {
|
|
let parent;
|
|
if (this.preferredParent instanceof HTMLElement) {
|
|
// We have configured one already
|
|
parent = this.preferredParent;
|
|
} else if (this.preferredParent) {
|
|
// Query Document
|
|
parent = document.querySelector(this.preferredParent);
|
|
} else if (this.popupType === 'inline') {
|
|
// Pick the parent element of the Content
|
|
let inlineContent = this.popupContent;
|
|
// Check for content selector: src: '#content-selector' or src: '.content-selector'
|
|
if (!inlineContent && this.src && (this.src[0] === '.' || this.src[0] === '#')) {
|
|
inlineContent = document.querySelector(this.src);
|
|
parent = inlineContent ? inlineContent.parentElement : false;
|
|
}
|
|
}
|
|
return parent || false;
|
|
}
|
|
|
|
/**
|
|
* Return the popup header element.
|
|
* @returns {HTMLElement|boolean}
|
|
*/
|
|
getHeader() {
|
|
this.renderLayout();
|
|
return this.popupTmplH || false;
|
|
}
|
|
|
|
/**
|
|
* Return the popup body element.
|
|
* @returns {HTMLElement}
|
|
*/
|
|
getBody() {
|
|
this.renderLayout();
|
|
return this.popupTmplB;
|
|
}
|
|
|
|
/**
|
|
* Return the popup content element, or body for inline and ajax types.
|
|
* @returns {HTMLElement}
|
|
*/
|
|
getBodyContent() {
|
|
this.renderLayout();
|
|
return this.popupContentElement || this.popupTmplB;
|
|
}
|
|
|
|
/**
|
|
* Return the popup footer element.
|
|
* @returns {HTMLElement|boolean}
|
|
*/
|
|
getFooter() {
|
|
this.renderLayout();
|
|
return this.popupTmplF || false;
|
|
}
|
|
|
|
/**
|
|
* Open the popup as modal window.
|
|
* Will append the element to Document body if not appended before.
|
|
*
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
show() {
|
|
// Check whether the element already attached to DOM
|
|
if (!this.parentElement) {
|
|
// Check for preferred parent to attach to DOM
|
|
const parent = this.findPreferredParent();
|
|
(parent || document.body).appendChild(this);
|
|
}
|
|
this.dialog.showModal();
|
|
this.dispatchEvent(new CustomEvent('joomla-dialog:open', {
|
|
bubbles: true
|
|
}));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Alias for show() method.
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
open() {
|
|
return this.show();
|
|
}
|
|
|
|
/**
|
|
* Closes the popup
|
|
*
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
close() {
|
|
if (!this.dialog) {
|
|
throw new Error('Calling close for non opened dialog is discouraged.');
|
|
}
|
|
this.dialog.close();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Alias for close() method.
|
|
* @returns {JoomlaDialog}
|
|
*/
|
|
hide() {
|
|
return this.close();
|
|
}
|
|
|
|
/**
|
|
* Destroys the popup.
|
|
*/
|
|
destroy() {
|
|
if (!this.dialog) {
|
|
return;
|
|
}
|
|
this.dialog.close();
|
|
this.removeChild(this.dialog);
|
|
this.parentElement.removeChild(this);
|
|
|
|
// Restore original location of the popup content element
|
|
if (this.popupContentSrcLocation && this.popupContent) {
|
|
const {
|
|
parent,
|
|
nextSibling
|
|
} = this.popupContentSrcLocation;
|
|
parent.insertBefore(this.popupContent, nextSibling);
|
|
}
|
|
this.dialog = null;
|
|
this.popupTmplH = null;
|
|
this.popupTmplB = null;
|
|
this.popupTmplF = null;
|
|
this.popupContentElement = null;
|
|
this.popupContentSrcLocation = null;
|
|
}
|
|
|
|
/**
|
|
* Helper method to show an Alert.
|
|
*
|
|
* @param {String} message
|
|
* @param {String} title
|
|
* @returns {Promise}
|
|
*/
|
|
static alert(message, title) {
|
|
return new Promise(resolve => {
|
|
const popup = new this();
|
|
popup.popupType = 'inline';
|
|
popup.popupContent = message;
|
|
popup.textHeader = title || Joomla.Text._('INFO', 'Info');
|
|
popup.popupButtons = [{
|
|
label: Joomla.Text._('JOK', 'Okay'),
|
|
data: {
|
|
buttonOk: ''
|
|
},
|
|
onClick: () => popup.close()
|
|
}];
|
|
popup.classList.add('joomla-dialog-alert');
|
|
popup.addEventListener('joomla-dialog:close', () => {
|
|
popup.destroy();
|
|
resolve();
|
|
});
|
|
popup.show();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper method to show a Confirmation popup.
|
|
*
|
|
* @param {String} message
|
|
* @param {String} title
|
|
* @returns {Promise}
|
|
*/
|
|
static confirm(message, title) {
|
|
return new Promise(resolve => {
|
|
let result = false;
|
|
const popup = new this();
|
|
popup.popupType = 'inline';
|
|
popup.popupContent = message;
|
|
popup.textHeader = title || Joomla.Text._('INFO', 'Info');
|
|
popup.popupButtons = [{
|
|
label: Joomla.Text._('JYES', 'Yes'),
|
|
data: {
|
|
buttonOk: ''
|
|
},
|
|
onClick: () => {
|
|
result = true;
|
|
popup.destroy();
|
|
}
|
|
}, {
|
|
label: Joomla.Text._('JNO', 'No'),
|
|
data: {
|
|
buttonCancel: ''
|
|
},
|
|
onClick: () => {
|
|
result = false;
|
|
popup.destroy();
|
|
},
|
|
className: 'button button-secondary btn btn-outline-secondary'
|
|
}];
|
|
popup.cancelable = false;
|
|
popup.classList.add('joomla-dialog-confirm');
|
|
popup.addEventListener('joomla-dialog:close', () => resolve(result));
|
|
popup.show();
|
|
});
|
|
}
|
|
}
|
|
customElements.define('joomla-dialog', JoomlaDialog);
|
|
|
|
export { JoomlaDialog as default };
|