ui-lib/lib/ui/popup.js
Tsanie Lily 5baf00de64
add: getText compatibility.
add: `AssetSelector` and `TemplateSelector`.
add: `popup-selector` style class.
add: `ui.resolvePopup` function.
add: `switch` in checkbox.
add: `GridColumn.filterTemplate` supports.
add: add `action` callback in `createIcon`.
change: replace `setTimeout(..., 0)` with `requestAnimationFrame`.
change: Popup result structure adjustment ({ result: any, popup: Popup }).
change: complete add work order flow.
change: reduce Popup title height.
fix: Grid column sort in number.
2024-06-21 17:28:11 +08:00

645 lines
25 KiB
JavaScript

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();
});
});
}