import "./css/popup.scss"; import { r as lang } from "../utility/lgres"; import { nullOrEmpty } from "../utility/strings"; import { global } from "../utility"; import { createElement } from "../functions"; import { createIcon, changeIcon } from "./icon"; import { requestAnimationFrame } from "../ui"; 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' // } function trimPx(px) { if (typeof px !== 'string') { return px; } if (px.endsWith('px')) { const size = Number(px.substring(0, px.length - 2)); return isNaN(size) ? px : size; } return px; } export class Popup { _var = {}; // _var.mask; // _var.option; // _var.bounds; // _var.cursor; constructor(opts = {}) { this._var.option = opts; } get container() { return this._var.mask.querySelector('.ui-popup-container') } get title() { return this._var.option.title } set title(title) { const element = this._var.mask?.querySelector('.ui-popup-container .ui-popup-header .ui-popup-header-title'); if (element != null) { element.innerText = title; } this._var.option.title = title; } get loading() { return this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading')?.style?.visibility === 'visible' } set loading(flag) { let loading = this._var.mask?.querySelector('.ui-popup-body>.ui-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; } } get rect() { const container = this.container; if (container == null) { return null; } const style = global.getComputedStyle(container); const collapsed = container.classList.contains('ui-popup-collapse'); const bounds = this._var.bounds; return { collapsed, left: trimPx(style.left), top: trimPx(style.top), width: collapsed === true && bounds != null ? bounds.width : trimPx(style.width), height: collapsed === true && bounds != null ? bounds.height : trimPx(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`); } const collapse = container.querySelector('.ui-popup-header-icons>.icon-expand'); if (r.collapsed === true) { css.push('width: 160px', 'height: 40px'); this._var.bounds = r; container.classList.add('ui-popup-collapse'); if (collapse != null) { changeIcon(collapse, 'fa-regular', 'expand-alt'); } } else { 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`); } container.classList.remove('ui-popup-collapse'); this._var.bounds = null; if (collapse != null) { changeIcon(collapse, 'fa-regular', 'compress-alt'); } } if (css.length > 0) { container.style.cssText += css.join('; '); } } close(result = null, animation = true) { const option = this._var.option; const mask = this._var.mask; const doClose = () => { if (option.persistent) { mask.style.display = 'none'; } else { mask.remove(); this._var.mask = null; } } if (animation) { mask.classList.add('ui-popup-active'); mask.style.opacity = 0; setTimeout(() => { doClose(); }, 120); } else { doClose(); } if (typeof option.onMasking === 'function') { option.onMasking.call(this, false); } if (typeof option.resolve === 'function') { option.resolve.call(this, { result, popup: this }); } } /** * 创建 Popup 面板 * @returns {HTMLDivElement} 返回遮罩元素(顶层元素) */ create() { const mask = createElement('div', 'ui-popup-mask ui-popup-active'); const option = this._var.option; if (option.mask === false) { mask.classList.add('ui-popup-transparent'); } else if (typeof option.onMasking === 'function') { option.onMasking.call(this, true); } if (!isNaN(option.zIndex)) { mask.style.zIndex = String(option.zIndex); } const container = createElement('div', 'ui-popup-container'); if (option.changeZIndex === true) { container.addEventListener('mousedown', () => { const masks = [...this._var.mask.parentElement.children].filter(e => e.classList.contains('ui-popup-mask')); let max = 200; masks.forEach(m => { let index; if (m.dataset.zindex != null) { index = parseInt(m.dataset.zindex); m.style.zIndex = isNaN(index) ? '' : String(index); delete m.dataset.zindex; } else { index = parseInt(m.style.zIndex); } if (index > max) { max = index; } }); mask.dataset.zindex = mask.style.zIndex; mask.style.zIndex = max + 1; }); } else { } let tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); if (tabIndex < 0) { tabIndex = 0; } container.tabIndex = tabIndex + 1; let content = option.content; if (!(content instanceof HTMLElement)) { content = createElement('div', d => d.innerText = content); } container.append( createElement('div', header => { header.className = 'ui-popup-header'; let title = option.title; if (!(title instanceof HTMLElement)) { title = createElement('div', t => { if (option.movable === false) { t.className = 'ui-popup-header-title no-move'; } else { t.className = 'ui-popup-header-title'; } t.innerText = title; }); } header.appendChild(title); if (option.movable !== false) { const move = header; // title.querySelector('.ui-popup-move') ?? title; move.addEventListener('mousedown', e => { if (['svg', 'use'].includes(e.target?.tagName)) { return; } if (e.buttons !== 1) { return; } const parent = option.mask === false ? mask.parentElement : mask; const x = e.clientX - container.offsetLeft; const y = e.clientY - container.offsetTop; let moved; const move = e => { if (e.buttons === 1) { container.style.left = `${e.clientX - x}px`; container.style.top = `${e.clientY - y}px`; moved = true; } else { parent.dispatchEvent(new MouseEvent('mouseup')); } }; parent.addEventListener('mousemove', move, { passive: false }); const up = () => { parent.removeEventListener('mousemove', move, { passive: false }); parent.removeEventListener('mouseup', up); if (moved === true && typeof option.onMoveEnded === 'function') { option.onMoveEnded.call(this); } moved = false; }; parent.addEventListener('mouseup', up); }); } const icons = createElement('div', icons => { icons.className = 'ui-popup-header-icons'; if (option.collapsable === true) { const collapse = createIcon('fa-regular', 'compress-alt'); collapse.tabIndex = tabIndex + 2; collapse.classList.add('icon-expand'); collapse.addEventListener('keypress', e => { if (e.key === ' ' || e.key === 'Enter') { collapse.dispatchEvent(new MouseEvent('click')); } }); collapse.addEventListener('click', () => { if (container.classList.contains('ui-popup-collapse')) { const bounds = this._var.bounds; if (bounds != null) { container.style.cssText += `width: ${bounds.width}px; height: ${bounds.height}px`; this._var.bounds = null; } container.classList.remove('ui-popup-collapse'); changeIcon(collapse, 'fa-regular', 'compress-alt'); } else { const rect = this.rect; this._var.bounds = rect; container.style.cssText += `width: 160px; height: 40px`; container.classList.add('ui-popup-collapse'); changeIcon(collapse, 'fa-regular', 'expand-alt'); } if (typeof option.onResizeEnded === 'function') { option.onResizeEnded.call(this); } }); icons.appendChild(collapse); } if (option.closable !== false) { const cancel = createIcon('fa-regular', 'times'); cancel.tabIndex = tabIndex + 3; cancel.addEventListener('keypress', e => { if (e.key === ' ' || e.key === 'Enter') { this.close(); } }); cancel.addEventListener('click', () => this.close()); icons.appendChild(cancel); } }); header.appendChild(icons); }), createElement('div', 'ui-popup-body', content, createElement('div', 'ui-popup-loading', createElement('div', null, createIcon('fa-regular', 'spinner-third')) )) ); if (Array.isArray(option.buttons) && option.buttons.length > 0) { tabIndex = Math.max.apply(null, [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); container.appendChild( createElement('div', 'ui-popup-footer', ...option.buttons.map((b, i) => { const button = createElement('button', 'ui-popup-button'); if (b.className != null) { button.classList.add(b.className); } if (b.tabIndex > 0) { button.tabIndex = b.tabIndex; } else { button.tabIndex = tabIndex + i + 1; } 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) { this.close(r); } }).catch(reason => console.warn(reason)); } else if (result !== false) { this.close(result); } } else { this.close(b.key ?? i); } }); return button; })) ); const tabs = [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0); const tabMin = Math.min.apply(null, tabs); const tabMax = Math.max.apply(null, tabs); const last = container.querySelector(`[tabindex="${tabMax}"]`); if (last != null) { last.addEventListener('keydown', e => { if (e.key === 'Tab') { const first = container.querySelector(`[tabindex="${tabMin}"]`); first?.focus(); e.preventDefault(); } }); } } else { container.querySelector('.ui-popup-body>.ui-popup-loading').classList.add('ui-popup-loading-content'); } // resizable if (option.resizable === true) { container.append( createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-right'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.right, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-bottom'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottom, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-left'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.left, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-top'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.top, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-bottom-right'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottomRight, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-bottom-left'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottomLeft, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-top-left'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.topLeft, e)); }), createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-top-right'; layer.addEventListener('mousedown', e => this._resize(ResizeMods.topRight, e)); }) ) } mask.appendChild(container); this._var.mask = mask; return mask; } show(parent = document.body, hidden = false) { if (parent == null) { return; } let mask = this._var.mask; if (mask == null) { mask = this._var.mask = this.create(); } if (mask.parentElement == null) { // const exists = [...parent.children].filter(e => e.classList.contains('ui-popup-mask')); const exists = parent.querySelectorAll('.ui-popup-mask'); let zindex = 0; for (let ex of exists) { let z = parseInt(global.getComputedStyle(ex).zIndex); if (!isNaN(z) && z > zindex) { zindex = z; } } if (zindex > 0) { mask.style.zIndex = String(zindex + 1); } parent.appendChild(mask); if (hidden === true) { mask.style.display = 'none'; return Promise.resolve(mask); } } if (this._var.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 => { mask.style.display = ''; requestAnimationFrame(() => { mask.classList.remove('ui-popup-active'); mask.style.opacity = 1; this.container.focus(); setTimeout(() => resolve(mask), 120); }); }); } _resize(mod, e) { if (e.buttons !== 1) { return; } const container = this.container; const option = this._var.option; if (typeof option.onResizeStarted === 'function') { option.onResizeStarted.call(this); } const mask = this._var.mask; // this._var.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; let resized; const parent = option.mask === false ? mask.parentElement : mask; const move = e => { if (e.buttons !== 1) { parent.dispatchEvent(new MouseEvent('mouseup')); return; } 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`; } resized = true; } parent.addEventListener('mousemove', move, { passive: false }); const up = () => { parent.removeEventListener('mousemove', move, { passive: false }); parent.removeEventListener('mouseup', up); // mask.style.cursor = this._var.cursor; if (resized === true && typeof option.onResizeEnded === 'function') { option.onResizeEnded.call(this); } resized = false; }; parent.addEventListener('mouseup', up); } } export function createPopup(title, content, ...buttons) { const popup = new Popup({ title, content, buttons }); return popup; } /** * 解析对话框元素 * @param {HTMLElement | string} wrapper - 解析该 `.dialog` 元素 * @param {Function} [callback] - 关闭对话框时的回调 * @param {boolean} [removable] - 是否可移除 * @param {number} [zIndex] - 对话框默认 `z-index` * @returns {Popup} 返回弹出框字典 */ export function resolvePopup(wrapper, callback, removable, zIndex) { if (typeof wrapper === 'string') { wrapper = document.querySelector(wrapper); } if (wrapper == null) { return null; } if (!wrapper.classList.contains('dialog')) { return null; } const title = wrapper.querySelector('.dialog-title>.title')?.innerText; const content = wrapper.querySelector('.dialog-title+div'); const buttons = [...wrapper.querySelectorAll('.dialog-func>input[type="button"]')].reverse().map(b => ({ tabIndex: b.tabIndex, text: b.value, trigger: b.onclick == null ? null : (popup => (b.onclick.call(popup), false)) })); const popup = new Popup({ title, content, persistent: !removable, resolve: typeof callback === 'function' ? (result => callback(result)) : null, zIndex: wrapper.zIndex ?? zIndex, buttons }); popup.show(document.body, true); 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) { const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang; 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) ), resolve, buttons: [ { text: r('ok', 'OK') } ] }); popup.show(parent).then(mask => { const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button:last-child'); button?.focus(); }); }); } export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) { const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang; return new Promise(resolve => { 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, resolve, buttons: buttons?.map((b, i) => { return { text: b.text, trigger: p => { let result; if (typeof b.trigger === 'function') { result = b.trigger(p, b); } else { result = b.key ?? i; } return result; } }; }) ?? [ { key: 'yes', text: r('yes', 'Yes') }, { key: 'no', text: r('no', 'No') } ] }); popup.show(parent).then(mask => { const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button:last-child'); button?.focus(); }); }); }