UNPKG

lu2

Version:

Simple and flexible UI component library based on native HTML and JavaScript

924 lines (799 loc) 36.6 kB
/** * @Dialog.js * @author zhangxinxu * @version * @created 15-06-18 * @edited 19-11-01 * @edited 20-06-26 @ziven27 * @edited 20-12-01 by zhangxinxu method extends from <dialog> * @edited 24-04-15 by zhangxinxu support modal dialog */ const Dialog = (() => { // 类名前缀 const DIALOG = 'dialog'; // 处理类名 const CL = { add: (...arg) => `ui-${DIALOG}-${arg.join('-')}`, toString: (value) => `ui-${value || DIALOG}` }; /** * 弹框实例方法 * @param {Object} options 纯对象,可选参数 */ class Component { constructor (options = {}) { // 最终参数 const objParams = { title: '', // 不同类别的内容类型 content: '', // 弹框的宽度 width: 'auto', // 弹框高度 height: 'auto', // 不同类别的默认按钮 buttons: [], // 关闭按钮 closable: true, // 弹框显示、隐藏、移除的回调 onShow: function () { }, onHide: function () { }, onRemove: function () { }, ...options }; // 各个元素创建 // 容器-含半透明遮罩背景 const dialog = document.createElement(DIALOG); dialog.setAttribute('is', 'ui-dialog'); // MutationObserver是一个异步的过程,因此 // 元素样式的设置在'DOMContentLoaded'事件之后完成 dialog.addEventListener('DOMContentLoaded', () => { // 务必有传参 // 否则不会对初始元素进行处理 if (JSON.stringify(options) != '"{}"') { // 改变参数,会自动触发DOM元素内容的变化 dialog.setParams({ ...objParams }); } // 显示 if (dialog.hasAttribute('modal')) { dialog.showModal(); } else { dialog.show(); } }); // 插入的细节 // 1. 插在所有dialog的前面 // 2. 如果没有,则放在页面后面 const eleExistDialog = document.querySelector('body > ' + DIALOG); if (eleExistDialog) { eleExistDialog.insertAdjacentElement('beforebegin', dialog); } else { document.body.appendChild(dialog); } // 注册当前<dialog>元素 // 为了可以立即使用alert()、confirm()方法, // 在这里提前注册了 funDialogRegist(dialog); return dialog; } } // 对不支持<dialog>元素的浏览器进行polyfill // 仅polyfill部分主要功能 const DialogPolyfill = function (dialog) { this.element = { dialog: dialog }; // aria支持 if (!dialog.hasAttribute('role')) { dialog.setAttribute('role', 'dialog'); } // 内置方法 dialog.show = this.show.bind(this); dialog.showModal = this.showModal.bind(this); dialog.close = this.close.bind(this); // 自定义方法 dialog.zIndex = this.zIndex.bind(this); Object.defineProperty(dialog, 'open', { set: this.setOpen.bind(this), get: dialog.hasAttribute.bind(dialog, 'open') }); }; DialogPolyfill.prototype = { get dialog () { return this.element.dialog; }, show () { this.setOpen(true); // 层级最高 this.zIndex(); }, showModal () { this.setOpen(true); }, close () { this.setOpen(false); // 原生<dialog>就有close事件 this.dialog.dispatchEvent(new CustomEvent('close', { bubbles: false, cancelable: false })); }, setOpen (value) { if (value) { this.dialog.setAttribute('open', ''); } else { this.dialog.removeAttribute('open'); } }, /** * 弹框元素zIndex实时最大化 * 原生dialog无需此能力(更正,浏览器变化,现在也需要了) * @return {[type]} [description] */ zIndex () { var dialog = this.dialog; // 原生元素需要 if (this.matches && this.matches('dialog')) { dialog = this; } // 返回eleTarget才是的样式计算对象 const objStyleTarget = window.getComputedStyle(dialog); // 此时元素的层级 const numZIndexTarget = objStyleTarget.zIndex; // 用来对比的层级,也是最小层级 let numZIndexNew = 19; // 只对<body>子元素进行层级最大化计算处理 [...document.body.children].forEach(function (eleChild) { const objStyleChild = window.getComputedStyle(eleChild); const numZIndexChild = objStyleChild.zIndex * 1; if (numZIndexChild && (dialog !== eleChild && objStyleChild.display !== 'none')) { numZIndexNew = Math.max(numZIndexChild + 1, numZIndexNew); } }); if (numZIndexNew !== numZIndexTarget) { dialog.style.zIndex = numZIndexNew; } } }; // 对弹框元素进行方法注册 const funDialogRegist = function (dialog) { if (dialog.hide && dialog.button) { // 已经注册过 return; } if ('open' in document.createElement('dialog') == false) { new DialogPolyfill(dialog); } // 自定义的方法支持 // 全新的remove和show方法 // 新增hide, alert, confirm等方法 if (dialog.getAttribute('is') == 'ui-dialog') { Object.defineProperties(dialog, { // 劫持原生的open属性 open: { get () { return this.hasAttribute('open'); }, set (value) { this.toggleAttribute('open', value); } }, setParams: { value: function (options) { Object.assign(this.params, options || {}); // 回调处理 if (typeof options.onShow == 'function') { this.addEventListener('show', function (event) { options.onShow.call(this, event); }); } if (typeof options.onHide == 'function') { this.addEventListener('hide', function (event) { options.onHide.call(this, event); }); } if (typeof options.onRemove == 'function') { this.addEventListener('remove', function (event) { options.onRemove.call(this, event); }); } return this.params; } }, /** * 弹框按钮的处理 * @returns {Object} 返回当前<dialog>元素 */ button: { value: function () { const objParams = this.params; const objElement = this.element; // 清除之前的按钮内容和数据 objElement.footer.innerHTML = ''; // 元素数据清除 for (const keyElement in objElement) { if (/^button/.test(keyElement)) { delete objElement[keyElement]; } } // 按钮元素创建 objParams.buttons.forEach(function (objButton, numIndex) { // objButton可能是null等 objButton = objButton || { type: 'normal' }; // 按钮类型和值的处理 let strType = objButton.type; let strValue = objButton.value; if (strType === 'remind' || (!strType && numIndex === 0)) { strType = 'primary'; } else if (!strType && numIndex === 1) { strType = 'normal'; } if (!strValue) { strValue = ['确定', '取消'][numIndex]; } let eleButton = document.createElement('button'); if (objButton['for']) { eleButton = document.createElement('label'); eleButton.setAttribute('for', objButton['for']); } else if (objButton.form) { eleButton.setAttribute('form', objButton.form); eleButton.type = 'submit'; } // 自定义的类名 if (objButton.className) { eleButton.className = objButton.className; } // 按钮样式 eleButton.classList.add(CL.toString('button')); if (strType) { eleButton.setAttribute('data-type', strType); } // 按钮是否禁用 eleButton.disabled = Boolean(objButton.disabled); // 按钮内容 eleButton.innerHTML = strValue; // 放在底部元素中 objElement.footer.appendChild(eleButton); // 对外暴露 objElement['button' + numIndex] = eleButton; }); // 按钮事件 // 底部确定取消按钮 objParams.buttons.forEach((objButton, numIndex) => { // objButton可能是null等 objButton = objButton || {}; const eleButton = objElement['button' + numIndex]; if (!eleButton || objButton['for'] || objButton.form) { return; } let objEvents = objButton.events || { click: () => { this[this.closeMode](); } }; if (typeof objEvents === 'function') { objEvents = { click: objEvents }; } for (const strEventType in objEvents) { eleButton.addEventListener(strEventType, (event) => { // 把实例对象传入 event.dialog = this; // 事件执行 objEvents[strEventType](event); }); } // 额外的focus事件支持 eleButton.addEventListener('focus', function () { if (window.isKeyEvent) { this.style.outline = ''; } else { this.style.outline = 'none'; } }); }); return this; } }, /** * 固定结构元素的事件绑定 * @returns {Object} 返回当前<dialog>元素对象 */ events: { value: function () { const objElement = this.element; this.addEventListener('animationend', function (event) { if (event.target.tagName.toLowerCase() === DIALOG) { this.classList.remove(CL.add('animation')); } }); // 关闭弹框按钮 const eleClose = objElement.close; if (eleClose) { eleClose.addEventListener('click', () => { // 有其他可ESC元素存在时候,弹框不关闭 const eleActiveElement = document.activeElement; const attrActiveElement = eleActiveElement.getAttribute('data-target'); let eleTargetElement = null; if (attrActiveElement) { eleTargetElement = document.getElementById(attrActiveElement); } // 如果是其他元素的键盘访问 if (window.isKeyEvent && eleTargetElement && eleActiveElement !== eleClose && document.querySelector('a[data-target="' + attrActiveElement + '"],input[data-target="' + attrActiveElement + '"],button[data-target="' + attrActiveElement + '"]') && eleTargetElement.clientWidth > 0) { return; } // 关闭弹框 this[this.closeMode](); }); } this.dispatchEvent(new CustomEvent('DOMContentLoaded')); return this; } }, /** * alert类型的弹框,默认仅一个“确定”按钮 * @param {String} content 提示文字或者提示HTML片段 * @param {Object} options 提示可选参数 * @return {Object} 返回当前<dialog>元素对象 */ alert: { value: function (content, options) { if (!content) { return; } let strContent = content; // alert框的默认参数 const defaults = { title: '', // 类型, 'remind', 'success', 'warning', danger', 或者任意 'custom' type: 'remind', buttons: [{}] }; // 最终参数 const objParams = { ...defaults, ...options }; if (objParams.type === 'error' || objParams.type === 'fail') { objParams.type = 'danger'; } else if (objParams.type === 'primary') { objParams.type = 'remind'; } if (objParams.buttons.length && !objParams.buttons[0].type) { objParams.buttons[0].type = objParams.type; // 如果是自定义类型,则使用'primary'作为按钮类型 if (/^remind|success|warning|danger$/.test(objParams.type) === false) { objParams.buttons[0].type = defaults.type; } } let nodes = new DOMParser().parseFromString(strContent, 'text/html').body.childNodes; if (nodes.length == 1) { // 如果是纯文本 if (nodes[0].nodeType === 3) { strContent = '<p class="' + CL.add('wrap') + '">' + strContent + '</p>'; } } else { strContent = '<div class="' + CL.add('wrap') + '">' + strContent + '</div>'; } // 主体内容更新 strContent = '<div class="' + CL.add(objParams.type) + ' ' + CL.add('alert') + '">' + strContent + '</div>'; this.setParams({ width: 'auto', title: objParams.title, buttons: objParams.buttons, content: strContent }); this.type = 'alert'; this.open = false; this.showModal(); return this; } }, /** * confirm类型的弹框,默认有一个“确定”和一个“取消”按钮 * @param {String} content 提示文字或者提示HTML片段 * @param {Object} options 提示可选参数 * @return {Object} 返回当前<dialog>元素对象 */ confirm: { value: function (content, options) { if (!content) { return; } let strContent = content; // confirm框的默认参数 const defaults = { title: '', type: 'danger', buttons: [{}, {}] }; // 最终参数 const objParams = { ...defaults, ...options }; if (objParams.type === 'error' || objParams.type === 'fail') { objParams.type = 'danger'; } if (objParams.type === 'primary') { objParams.type = 'remind'; } // danger类型的按钮可缺省 if (objParams.buttons.length && !objParams.buttons[0].type) { objParams.buttons[0].type = objParams.type; // 如果是自定义类型,则使用'primary'作为按钮类型 if (/^remind|success|warning|danger$/.test(objParams.type) === false) { objParams.buttons[0].type = defaults.type; } } let nodes = new DOMParser().parseFromString(strContent, 'text/html').body.childNodes; if (nodes.length == 1) { // 如果是纯文本 if (nodes[0].nodeType === 3) { strContent = '<p class="' + CL.add('wrap') + '">' + strContent + '</p>'; } } else { strContent = '<div class="' + CL.add('wrap') + '">' + strContent + '</div>'; } // 主体内容设置 strContent = '<div class="' + CL.add(objParams.type) + ' ' + CL.add('confirm') + '">' + strContent + '</div>'; // 参数对外 this.setParams({ width: 'auto', title: objParams.title, buttons: objParams.buttons, content: strContent }); this.type = 'confirm'; this.open = false; this.showModal(); return this; } }, /** * loading弹框,通常用在ajax请求之前使用 * loading结束后可以直接调用弹框实例的open()方法显示 * @return {Object} 返回当前实例对象 */ loading: { value: function () { const objElement = this.element; this.params.content = '<ui-loading rows="10" size="3"></ui-loading>'; // 显示loading样式 objElement.dialog.classList.add(CL.add('loading')); this.open = false; this.showModal(); return this; } }, /** * 内容赋值 */ content: { get () { return this.params.content; }, set (content) { // 让直接设置content时候可以和params.content数据保持一致 if (content != this.params.content) { this.params.content = content; return; } let eleBody = this.element.body; let eleDialog = this.element.dialog; // 去除可能的loading类名 eleDialog.classList.remove(CL.add('loading')); // content可以是函数 if (typeof content == 'function') { content = content(); } else if (typeof content == 'string' && /^#?\w+(?:[-_]\w+)*$/i.test(content)) { // 如果是字符串 // 如果是选择器,仅支持ID选择器 let eleMatch = document.querySelector(content); if (eleMatch) { if (eleMatch.matches('textarea')) { content = eleMatch.value; } else if (eleMatch.matches('script')) { content = eleMatch.innerHTML; } else { content = eleMatch; } } } // 基于内容的数据类型,使用不同的默认的弹框关闭方式 this.closeMode = typeof content == 'string' ? 'remove' : 'hide'; // 是隐藏模式,则eleBody里面的内容保护出来 // 主要是使用content语法替换内容时候用到,这段代码一般不会执行到 if (this.closeMode == 'hide' && eleBody.innerHTML) { let eleProtect = document.createElement('div'); eleProtect.setAttribute('hidden', ''); // 遍历并转移 eleBody.childNodes.forEach(node => { eleProtect.appendChild(node); }); // 保护到页面中 document.body.appendChild(eleProtect); } // 清空主内容区域的内容 eleBody.innerHTML = ''; if (this.closeMode == 'remove') { eleBody.innerHTML = content; } else { let eleContentParent = content.parentElement; let isParentHidden = eleContentParent && eleContentParent.matches('div[hidden]'); // 弹框中显示 eleBody.appendChild(content); // 如果原父级是隐藏div,该div删除 if (isParentHidden && eleContentParent.innerHTML.trim() === '') { eleContentParent.remove(); } // 如果content是隐藏的则显示 if (content.nodeType === 1 && getComputedStyle(content).display == 'none') { content.removeAttribute('hidden'); content.style.display = ''; // 如果此时元素的display状态还是none,则设置为浏览器初始display值 if (getComputedStyle(content).display == 'none') { content.style.display = 'revert'; } } } } }, /** * 弹框显示 * @returns 当前<dialog>元素 */ show: { value: function () { if (!this.open) { this.classList.add(CL.add('animation')); if (typeof HTMLDialogElement == 'function') { HTMLDialogElement.prototype.show.call(this); } } if (!this.zIndex) { this.zIndex = DialogPolyfill.prototype.zIndex.bind(this); } // 面板显示 if (this.zIndex) { this.zIndex(); } this.dispatchEvent(new CustomEvent('show', { detail: { type: 'ui-dialog' } })); return this; } }, /** * 弹框隐藏 * @returns 当前<dialog>元素 */ hide: { value: function () { this.close(); this.dispatchEvent(new CustomEvent('hide', { detail: { type: 'ui-dialog' } })); return this; } }, /** * 弹框移除 * @returns 当前<dialog>元素 */ remove: { value: function () { this.open = false; this.parentElement.removeChild(this); this.dispatchEvent(new CustomEvent('remove', { detail: { type: 'ui-dialog' } })); return this; } } }); // 重置 removeAttribute、setAttribute 和 toggleAttribute 方法 // 如果是 open 属性,不允许直接设置 dialog.removeAttribute = function (name) { if (name == 'open') { dialog.hide(); return; } HTMLElement.prototype.removeAttribute.call(this, name); }; dialog.setAttribute = function (name, value) { if (name == 'open') { dialog.toggleAttribute('open', true); return; } HTMLElement.prototype.setAttribute.call(this, name, value); } dialog.toggleAttribute = function (name, force) { if (name == 'open') { const value = force !== undefined ? force : !dialog.open; // 有open属性,显示 if (value) { if (this.hasAttribute('modal')) { this.showModal(); } else { this.show(); } } else { this.hide(); } return; } HTMLElement.prototype.toggleAttribute.call(this, name, force); } // 暴露的参数 // 并观察参数变化 dialog.params = new Proxy(dialog.params || {}, { get (target, prop) { return target[prop]; }, set (target, prop, value) { if (!dialog.element) { return false; } // 赋值 target[prop] = value; // 拦截 if (prop == 'title' && dialog.element.title) { dialog.element.title.innerHTML = value; } else if (prop == 'content') { dialog.content = value; } else if (prop == 'buttons') { dialog.button(); } else if (prop == 'closable' && dialog.element.close) { dialog.element.close.style.display = value ? '' : 'none'; } else if (dialog.element.dialog && (prop == 'width' || prop == 'height')) { let eleDialog = dialog.element.dialog; eleDialog.classList.remove(CL.add('stretch')); // 纯数值认为是px长度 if (value !== '' && Number(value) == value) { eleDialog.style[prop] = value + 'px'; } else if (prop == 'height' && value == 'stretch') { eleDialog.classList.add(CL.add(value)); } else { if (value == 'auto') { value = ''; } eleDialog.style[prop] = value; } } return true; } }); // 弹框主要元素的创建 // 1. 主体 const eleDialog = dialog; eleDialog.classList.add(CL); // 2. 标题 const eleTitle = document.createElement('h4'); eleTitle.classList.add(CL.add('title')); eleTitle.innerHTML = dialog.title; dialog.removeAttribute('title'); // 3. 关闭按钮 // 随机id,ESC快捷键关闭弹框用到 const strIdClose = ('lulu_' + Math.random()).replace('0.', ''); // 关闭按钮元素创建 const eleClose = document.createElement('button'); eleClose.textContent = '关闭'; eleClose.classList.add(CL.add('close'), 'ESC'); eleClose.id = strIdClose; // 无障碍支持 eleClose.setAttribute('data-target', strIdClose); // 4. 主体内容元素 const eleBody = document.createElement('div'); eleBody.classList.add(CL.add('body')); // 5. 底部元素 const eleFooter = document.createElement('div'); eleFooter.classList.add(CL.add('footer')); // 暴露元素 dialog.element = Object.assign(dialog.element || {}, { dialog: eleDialog, close: eleClose, title: eleTitle, body: eleBody, footer: eleFooter }); // 下面是主体元素的创建 // 如果默认弹框里面就有文本节点或元素节点 let nodesOriginDialog = [...dialog.childNodes].filter(node => node.nodeType == 1 || node.nodeType == 3); // 则不参与任何组装 if (!nodesOriginDialog.length) { // 元素插入 // 组装 eleDialog.append(eleClose, eleTitle, eleBody, eleFooter); } else { eleDialog.prepend(eleClose); if (eleTitle.innerHTML) { eleDialog.prepend(eleTitle); } } // 参数处理,如果有 const strParams = dialog.dataset.params || dialog.getAttribute('params'); if (strParams && /{/.test(strParams)) { try { const objParams = (new Function('return ' + strParams))(); if (nodesOriginDialog.length) { if (objParams.buttons) { eleDialog.append(eleFooter); } if (objParams.title && !eleDialog.contains(eleTitle)) { eleDialog.prepend(eleTitle); } } // 参数设置 dialog.setParams(objParams); } catch (e) { console.error(e); } } // 默认模式是关闭 dialog.closeMode = 'hide'; // 事件 dialog.events(); } // 回调 dialog.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-dialog' } })); // 设置定义完毕标志量 dialog.setAttribute('defined', ''); dialog.isConnectedCallback = true; }; // 弹框观察并注册 const funDialogInitAndWatching = function () { const elesDialog = document.querySelectorAll('dialog'); elesDialog.forEach(item => { funDialogRegist(item); }); // 观察Dialog元素载入页面 var observerTips = new MutationObserver(function (mutationsList) { // 此时不检测DOM变化 mutationsList.forEach(function (mutation) { mutation.addedNodes.forEach(function (eleAdd) { if (eleAdd.matches && eleAdd.matches('dialog')) { funDialogRegist(eleAdd); } else if (eleAdd.querySelector) { eleAdd.querySelectorAll('dialog').forEach(item => { funDialogRegist(item); }); } }); }); }); observerTips.observe(document.body, { childList: true, subtree: true }); }; // 如果浏览器不支持<dialog>,则对出现在页面上的<dialog>元素进行注册 if (document.readyState != 'loading') { funDialogInitAndWatching(); } else { window.addEventListener('DOMContentLoaded', funDialogInitAndWatching); } return Component; })(); // 为了直接使用 window.Dialog = Dialog; // 可以 import export default Dialog;