import "./css/popup.scss"; import { r } from "../utility/lgres"; import { nullOrEmpty } from "../utility/strings"; import { global } from "../utility"; import { createElement } from "../functions"; import { createIcon, changeIcon } from "./icon"; const ResizeMods = { right: 1, bottom: 2, left: 4, top: 8, bottomRight: 2 | 1, bottomLeft: 2 | 4, topRight: 8 | 1, topLeft: 8 | 4 } // const Cursors = { // [ResizeMods.right]: 'ew-resize', // [ResizeMods.bottom]: 'ns-resize', // [ResizeMods.bottomRight]: 'nwse-resize', // [ResizeMods.left]: 'ew-resize', // [ResizeMods.bottomLeft]: 'nesw-resize', // [ResizeMods.top]: 'ns-resize', // [ResizeMods.topRight]: 'nesw-resize', // [ResizeMods.topLeft]: 'nwse-resize' // } class Popup { #mask; #option; #bounds; // #cursor; constructor(opts = {}) { this.#option = opts; } get container() { return this.#mask.querySelector('.popup-container') } get rect() { const container = this.container; if (container == null) { return null; } const style = global.getComputedStyle(container); return { left: style.left, top: style.top, width: style.width, height: style.height }; } set rect(r) { const container = this.container; if (container == null) { return; } const css = []; if (!isNaN(r.left)) { css.push(`left: ${r.left}px`); } if (!isNaN(r.top)) { css.push(`top: ${r.top}px`); } if (!isNaN(r.width) && r.width > 0) { css.push(`width: ${r.width}px`); } if (!isNaN(r.height) && r.height > 0) { css.push(`height: ${r.height}px`); } if (css.length > 0) { container.style.cssText += css.join('; '); } } create() { const mask = createElement('div', 'popup-mask'); if (this.#option.mask === false) { mask.classList.add('popup-transparent'); } const container = createElement('div', 'popup-container'); const close = () => { mask.classList.add('popup-active'); mask.style.opacity = 0; setTimeout(() => mask.remove(), 120); }; let content = this.#option.content; if (!(content instanceof HTMLElement)) { content = createElement('div', d => d.innerText = content); } container.append( createElement('div', header => { header.className = 'popup-header'; let title = this.#option.title; if (!(title instanceof HTMLElement)) { title = createElement('div', t => { t.className = 'popup-header-title'; t.innerText = title; }); } header.appendChild(title); if (this.#option.movable !== false) { const move = title.querySelector('.popup-move') ?? title; move.addEventListener('mousedown', e => { const x = e.clientX - container.offsetLeft; const y = e.clientY - container.offsetTop; const move = e => { container.style.left = `${e.clientX - x}px`; container.style.top = `${e.clientY - y}px`; }; mask.addEventListener('mousemove', move, { passive: false }); const up = () => { mask.removeEventListener('mousemove', move, { passive: false }); mask.removeEventListener('mouseup', up); if (typeof this.#option.onMoveEnded === 'function') { this.#option.onMoveEnded.call(this); } }; mask.addEventListener('mouseup', up); }); } if (this.#option.collapsable === true) { const collapse = createIcon('fa-regular', 'compress-alt'); collapse.classList.add('icon-expand'); collapse.addEventListener('click', () => { if (container.classList.contains('popup-collapse')) { const bounds = this.#bounds; if (bounds != null) { container.style.cssText += `width: ${bounds.width}; height: ${bounds.height}`; } container.classList.remove('popup-collapse'); changeIcon(collapse, 'fa-regular', 'compress-alt'); } else { const rect = this.rect; this.#bounds = rect; container.style.cssText += `left: ${rect.left}; top: ${rect.top}; width: 160px; height: 40px`; container.classList.add('popup-collapse'); changeIcon(collapse, 'fa-regular', 'expand-alt'); } }); header.appendChild(collapse); } const cancel = createIcon('fa-regular', 'times'); cancel.addEventListener('click', () => close()); header.appendChild(cancel); }), createElement('div', 'popup-body', content, createElement('div', 'popup-loading', createElement('div', null, createIcon('fa-regular', 'spinner-third')) )) ); if (Array.isArray(this.#option.buttons)) { container.appendChild( createElement('div', 'popup-footer', ...this.#option.buttons.map(b => { const button = createElement('div', 'popup-button'); button.innerText = b.text; button.addEventListener('click', () => { if (typeof b.trigger === 'function') { const result = b.trigger(this); if (typeof result?.then === 'function') { result.then(r => { if (r !== false) { close(); } }).catch(() => { }); } else if (result !== false) { close(); } } else { close(); } }); return button; })) ); } // resizable if (this.#option.resizable === true) { container.append( createElement('layer', layer => { layer.className = 'popup-border popup-border-right'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.right, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-bottom'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottom, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-left'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.left, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-top'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.top, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-bottom-right'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottomRight, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-bottom-left'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottomLeft, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-top-left'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.topLeft, e)); }), createElement('layer', layer => { layer.className = 'popup-border popup-border-top-right'; layer.addEventListener('mousedown', e => this.#resize(ResizeMods.topRight, e)); }) ) } mask.appendChild(container); this.#mask = mask; return mask; } show(parent = document.body) { if (parent == null) { return; } let mask = this.#mask ?? this.create(); parent.appendChild(mask); if (this.#option.mask === false) { // calculator position const container = this.container; container.style.left = String((parent.offsetWidth - container.offsetWidth) / 2) + 'px'; container.style.top = String((parent.offsetHeight - container.offsetHeight) / 2) + 'px'; } return new Promise(resolve => { setTimeout(() => { mask.style.opacity = 1 resolve(mask); }, 0); }); } get loading() { return this.#mask?.querySelector('.popup-body>.popup-loading')?.style?.visibility === 'visible' } set loading(flag) { let loading = this.#mask?.querySelector('.popup-body>.popup-loading'); if (loading == null) { return; } if (flag === false) { loading.style.visibility = 'hidden'; loading.style.opacity = 0; } else { loading.style.visibility = 'visible'; loading.style.opacity = 1; } } #resize(mod, e) { const container = this.container; const option = this.#option; if (typeof option.onResizeStarted === 'function') { option.onResizeStarted.call(this); } const mask = this.#mask; // this.#cursor = mask.style.cursor; // mask.style.cursor = Cursors[mod]; const originalX = e.clientX; const originalY = e.clientY; const original = { width: container.offsetWidth, height: container.offsetHeight, left: container.offsetLeft, top: container.offsetTop }; const minWidth = option.minWidth ?? 200; const minHeight = option.minHeight ?? 200; const move = e => { const offsetX = e.clientX - originalX; const offsetY = e.clientY - originalY; let width = original.width; let height = original.height; let x = original.left; let y = original.top; if ((mod & ResizeMods.right) === ResizeMods.right) { width += offsetX; if (width < minWidth) { width = minWidth; } } if ((mod & ResizeMods.bottom) === ResizeMods.bottom) { height += offsetY; if (height < minHeight) { height = minHeight; } } if ((mod & ResizeMods.left) === ResizeMods.left) { width -= offsetX; if (width < minWidth) { width = minWidth; x = originalX + original.width - minWidth; } else { x += offsetX; } } if ((mod & ResizeMods.top) === ResizeMods.top) { height -= offsetY; if (height < minHeight) { height = minHeight; y = originalY + original.height - minHeight; } else { y += offsetY; } } if (typeof option.onResizing === 'function') { option.onResizing.call(this, x, y, width, height); } else { container.style.cssText += `left: ${x}px; top: ${y}px; width: ${width}px; height: ${height}px`; } } const parent = option.mask === false ? mask.parentElement : mask; parent.addEventListener('mousemove', move, { passive: false }); const up = () => { parent.removeEventListener('mousemove', move, { passive: false }); parent.removeEventListener('mouseup', up); // mask.style.cursor = this.#cursor; if (typeof option.onResizeEnded === 'function') { option.onResizeEnded.call(this); } }; parent.addEventListener('mouseup', up); } } export default Popup; export function createPopup(title, content, ...buttons) { const popup = new Popup({ title, content, buttons }); return popup; } const iconTypes = { 'info': 'info-circle', 'information': 'info-circle', 'warn': 'exclamation-triangle', 'warning': 'exclamation-triangle', 'question': 'question-circle', 'error': 'times-circle' } export function showAlert(title, message, iconType = 'info', parent = document.body) { return new Promise(resolve => { const popup = new Popup({ title, content: createElement('div', 'message-wrapper', createIcon('fa-solid', iconTypes[iconType] ?? 'info-circle'), createElement('span', span => span.innerText = message) ), buttons: [ { text: r('ok', 'OK'), trigger: resolve } ] }); popup.show(parent); }); } export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) { return new Promise((resolve, reject) => { const wrapper = createElement('div', 'message-wrapper'); if (!nullOrEmpty(iconType)) { wrapper.appendChild(createIcon('fa-solid', iconTypes[iconType] ?? 'question-circle')); } wrapper.appendChild(content instanceof HTMLElement ? content : createElement('span', span => span.innerText = content)); const popup = new Popup({ title, content: wrapper, buttons: buttons?.map(b => { return { text: b.text, trigger: p => { let result; if (typeof b.trigger === 'function') { result = b.trigger(p, b); if (typeof result?.then === 'function') { return result.then(r => { r !== false && resolve(r); return r; }); } result !== false && resolve(result); } else { result = { key: b.key, popup: p }; resolve(result); } return result; } }; }) ?? [ { text: r('yes', 'Yes'), trigger: p => resolve({ key: 'yes', popup: p }) }, { text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) } ] }); popup.show(parent); }); }