rabbit-simple-ui
Version:
A simple UI component library based on JavaScript
417 lines (347 loc) • 14.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
$el,
bind,
getBooleanTypeAttr,
getNumTypeAttr,
getStrTypeAttr,
removeAttrs,
setCss,
setHtml
} from '../../dom-utils';
import { CssTransition, moreThanOneNode, Scrollable } from '../../mixins';
import { type, validComps } from '../../utils';
import Button from '../button';
import PREFIX from '../prefix';
interface ModalEvents {
onOk?: () => void;
onCancel?: () => void;
}
interface Config {
config(
el: string
): {
title: string;
visible: boolean;
events({ onOk, onCancel }: ModalEvents): void;
};
}
const RABBIT_BTN = new Button();
class Modal implements Config {
readonly VERSION: string;
readonly COMPONENTS: NodeListOf<Element>;
constructor() {
this.VERSION = 'v1.1';
this.COMPONENTS = $el('r-modal', { all: true });
this._create(this.COMPONENTS);
}
public config(
el: string
): {
title: string;
visible: boolean;
events({ onOk, onCancel }: ModalEvents): void;
} {
const target = $el(el);
validComps(target, 'modal');
const { _attrs, _getModalNode, _handleVisable } = Modal.prototype;
const { loading } = _attrs(target);
const M_Child = _getModalNode(target);
return {
get title() {
return setHtml(M_Child.modalTitle);
},
set title(newVal: string) {
if (type.isStr(newVal)) setHtml(M_Child.modalTitle, newVal);
},
get visible() {
return false;
},
set visible(newVal: boolean) {
if (type.isBol(newVal)) {
// 当设置modal为隐藏状态并且确定按钮是加载中的状态则初始化它
if (!newVal) {
if (loading) RABBIT_BTN.config(M_Child.modalOkBtn).loading = newVal;
}
_handleVisable(newVal, target, [
M_Child.modalMask,
M_Child.modalWrap,
M_Child.modal
]);
}
},
events({ onOk, onCancel }: ModalEvents) {
const { closable, maskClosable } = _attrs(target);
const okEv = () => {
// 是否设置按钮为加载中状态
if (loading) RABBIT_BTN.config(M_Child.modalOkBtn).loading = loading;
onOk && type.isFn(onOk);
};
const cancelEv = () => {
// 如果按钮为加载中状态则初始化其状态
if (loading) RABBIT_BTN.config(M_Child.modalOkBtn).loading = !loading;
// 防止关闭modal后按键esc依然可以触发事件
window.onkeydown = (e: any) => (e.key === 'Escape' ? false : '');
onCancel && type.isFn(onCancel);
};
// 由于内部的_handleClose方法使用addEventListener为触发关闭模态框的元素绑定点击事件,
// 从而与这里绑定的事件造成冲突,一个回调事件同时多次触发的问题
// 因此使用on事件绑定,防止触发回调事件的次数随着每次点击而不断的重复叠加
if (maskClosable === 'true') {
// @ts-ignore
M_Child.modalWrap.onclick = () => cancelEv();
// @ts-ignore
M_Child.modal.onclick = (e: any) => e.stopPropagation();
}
if (closable === 'true') {
// @ts-ignore
M_Child.modalClose.onclick = () => cancelEv();
window.onkeydown = (e: any) => (e.key === 'Escape' ? cancelEv() : '');
}
// @ts-ignore
M_Child.modalOkBtn.onclick = () => okEv();
// @ts-ignore
M_Child.modalCancelBtn.onclick = () => cancelEv();
}
};
}
private _create(COMPONENTS: NodeListOf<Element>): void {
COMPONENTS.forEach((node) => {
this._createTemplate(node);
setCss(node, 'display', 'block');
removeAttrs(node, [
'width',
'title',
'ok-text',
'class-name',
'cancel-text',
'mask',
'visible',
'scrollable',
'fullscreen',
'lock-scroll',
'footer-hide'
]);
});
}
private _createTemplate(node: Element): void {
// v1.1 增加占位节点的组成数量判断
if (moreThanOneNode(node)) return;
// 获取最初 modal容器下的占位内容
const placeholderNode = node.firstElementChild;
const { width, title, zIndex, okText, cancelText, className } = this._attrs(node);
const template = `
<div class="${PREFIX.modal}-mask" style="z-index:${zIndex}"></div>
<div class="${PREFIX.modal}-wrap ${className}" style="z-index:${zIndex}">
<div class="${PREFIX.modal}" style="width: ${width}">
<div class="${PREFIX.modal}-content">
<a class="${PREFIX.modal}-close">
<i class="${PREFIX.icon} ${PREFIX.icon}-ios-close"></i>
</a>
<div class="${PREFIX.modal}-header">
<div class="${PREFIX.modal}-header-inner">${title}</div>
</div>
<div class="${PREFIX.modal}-body"></div>
<div class="${PREFIX.modal}-footer">
<button type="button" class="rab-btn rab-btn-text" id="modalBtn1">${cancelText}</button>
<button type="button" class="rab-btn rab-btn-primary" id="modalBtn2">${okText}</button>
</div>
</div>
</div>
</div>
`;
setHtml(node, template);
this._initVisable(node);
this._setHeader(node);
// @ts-ignore
this._setContent(node, placeholderNode);
this._setMask(node);
this._setFullScreen(node);
this._setClosable(node);
this._setFooterHide(node);
this._handleClose(node);
}
private _initVisable(node: Element): void {
const { visible, scrollable } = this._attrs(node);
const { modalMask, modalWrap, modal } = this._getModalNode(node);
let { lockScroll } = this._attrs(node);
!node.getAttribute('lock-scroll') ? (lockScroll = true) : lockScroll;
if (visible) {
setCss(modalMask, 'display', '');
modalWrap.classList.remove(`${PREFIX.modal}-hidden`);
setCss(modal, 'display', '');
Scrollable({ scroll: scrollable, lock: lockScroll });
} else {
setCss(modalMask, 'display', 'none');
modalWrap.classList.add(`${PREFIX.modal}-hidden`);
setCss(modal, 'display', 'none');
}
// @ts-ignore
// 设置初始显示状态
node.dataset.modalVisable = visible;
}
private _setHeader(node: Element): void {
const { title } = this._attrs(node);
if (!title) {
const modalHeader = node.querySelector(`.${PREFIX.modal}-header`);
modalHeader?.remove();
}
}
private _setContent(node: Element, content: Element): void {
const modalBody = node.querySelector(`.${PREFIX.modal}-body`);
if (content) modalBody?.appendChild(content);
}
private _setMask(node: Element): void {
const { _attrs, _getModalNode } = this;
const { mask } = _attrs(node);
if (mask === 'false') {
const { modalMask, modalWrap, modal } = _getModalNode(node);
modalMask.remove();
modalWrap.classList.add(`${PREFIX.modal}-no-mask`);
modal.classList.add(`${PREFIX.modal}-content-no-mask`);
}
}
private _setFullScreen(node: Element): void {
const { fullscreen } = this._attrs(node);
if (fullscreen) {
const { modal } = this._getModalNode(node);
modal.classList.add(`${PREFIX.modal}-fullscreen`);
}
}
private _setClosable(node: Element): void {
const { closable } = this._attrs(node);
if (closable === 'false') {
const { modalClose } = this._getModalNode(node);
modalClose.remove();
}
}
private _setFooterHide(node: Element): void {
const { footerHide } = this._attrs(node);
if (footerHide) {
const modalFooter = node.querySelector(`.${PREFIX.modal}-footer`);
modalFooter?.remove();
}
}
private _handleVisable(visible: boolean, target: Element, children: Element[]): void {
const { _show, _hide } = Modal.prototype;
visible ? _show(target, children) : _hide(target, children);
}
private _handleClose(parent: Element): void {
const { _attrs, _hide, _getModalNode } = this;
const { closable, maskClosable, loading } = _attrs(parent);
const {
modalMask,
modalWrap,
modal,
modalClose,
modalOkBtn,
modalCancelBtn
} = _getModalNode(parent);
const hidden = () => _hide(parent, [modalMask, modalWrap, modal]);
// 右上角关闭按钮
// ESC 键关闭
if (closable === 'true') {
bind(modalClose, 'click', () => hidden());
bind(window, 'keydown', (e: any) => (e.key === 'Escape' ? hidden() : ''));
}
// 遮盖层关闭
if (maskClosable === 'true') {
bind(modalWrap, 'click', () => hidden());
bind(modal, 'click', (e: any) => e.stopPropagation());
}
// 确定和取消按钮关闭
//非加载中状态可以点击关闭模态框
if (!loading) bind(modalOkBtn, 'click', () => hidden());
bind(modalCancelBtn, 'click', () => hidden());
}
private _show(parent: Element, showElm: Element[]): void {
const { _attrs } = Modal.prototype;
const { scrollable } = _attrs(parent);
let { lockScroll } = _attrs(parent);
!parent.getAttribute('lock-scroll') ? (lockScroll = true) : lockScroll;
// @ts-ignore
// 设置当前为显示状态
parent.dataset.modalVisable = 'true';
// showElm[0] 表示遮盖层
// showElm[1] 表示模态框的父容器wrap
// showElm[2] 表示模态框主体
showElm[1].classList.contains(`${PREFIX.modal}-hidden`) &&
showElm[1].classList.remove(`${PREFIX.modal}-hidden`);
CssTransition(showElm[0], {
inOrOut: 'in',
enterCls: 'rab-fade-in',
timeout: 250,
rmCls: true
});
CssTransition(showElm[2], {
inOrOut: 'in',
enterCls: 'zoom-big-enter',
timeout: 250,
rmCls: true
});
Scrollable({ scroll: scrollable, lock: lockScroll });
}
private _hide(parent: Element, hiddenElm: Element[]): void {
// @ts-ignore
// 设置当前为隐藏状态
parent.dataset.modalVisable = 'false';
// hiddenElm[0] 表示遮盖层
// hiddenElm[1] 表示模态框的父容器wrap
// hiddenElm[2] 表示模态框主体
CssTransition(hiddenElm[0], {
inOrOut: 'out',
leaveCls: 'rab-fade-out',
rmCls: true,
timeout: 250
});
CssTransition(hiddenElm[2], {
inOrOut: 'out',
leaveCls: 'zoom-big-leave',
rmCls: true,
timeout: 250
});
setTimeout(() => {
hiddenElm[1].classList.add(`${PREFIX.modal}-hidden`);
setCss(hiddenElm[2], 'display', 'none');
Scrollable({ scroll: true, lock: true, node: parent, tagName: 'modal' });
}, 240);
}
private _getModalNode(node: Element) {
const modalMask = node.querySelector(`.${PREFIX.modal}-mask`)!;
const modalWrap = node.querySelector(`.${PREFIX.modal}-wrap`)!;
const modal = modalWrap.querySelector(`.${PREFIX.modal}`)!;
const modalClose = modalWrap.querySelector(`.${PREFIX.modal}-close`)!;
const modalTitle = modal.querySelector(`.${PREFIX.modal}-header-inner`)!;
const modalOkBtn = modal.querySelector('#modalBtn2')!;
const modalCancelBtn = modal.querySelector('#modalBtn1')!;
return {
modalMask,
modalWrap,
modal,
modalClose,
modalTitle,
modalOkBtn,
modalCancelBtn
};
}
private _attrs(node: Element) {
return {
mask: getStrTypeAttr(node, 'mask', 'true'),
width: getStrTypeAttr(node, 'width', '520px'),
title: getStrTypeAttr(node, 'title', ''),
okText: getStrTypeAttr(node, 'ok-text', '确定'),
closable: getStrTypeAttr(node, 'closable', 'true'),
className: getStrTypeAttr(node, 'class-name', ''),
cancelText: getStrTypeAttr(node, 'cancel-text', '取消'),
maskClosable: getStrTypeAttr(node, 'mask-closable', 'true'),
zIndex: getNumTypeAttr(node, 'z-index', 1000),
visible: getBooleanTypeAttr(node, 'visible'),
loading: getBooleanTypeAttr(node, 'loading'),
scrollable: getBooleanTypeAttr(node, 'scrollable'),
lockScroll: getBooleanTypeAttr(node, 'lock-scroll'),
fullscreen: getBooleanTypeAttr(node, 'fullscreen'),
footerHide: getBooleanTypeAttr(node, 'footer-hide')
};
}
}
export default Modal;