UNPKG

lu2

Version:

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

967 lines (832 loc) 38.8 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> */ 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: [], // 弹框显示、隐藏、移除的回调 onShow: function () { }, onHide: function () { }, onRemove: function () { }, ...options }; if (objParams.buttons.length && typeof objParams.close == 'undefined') { objParams.close = false; } // 各个元素创建 // 容器-含半透明遮罩背景 const dialog = document.createElement(DIALOG); dialog.setAttribute('is', 'ui-dialog'); // MutationObserver是一个异步的过程,因此 // 元素样式的设置在'DOMContentLoaded'事件之后完成 dialog.addEventListener('DOMContentLoaded', () => { // 务必有传参 // 否则不会对初始元素进行处理 if (JSON.stringify(options) != '"{}"') { // 改变参数,会自动触发DOM元素内容的变化 dialog.setParams({ title: objParams.title, width: objParams.width, height: objParams.height, content: objParams.content, buttons: objParams.buttons, close: objParams.close }); // 回调处理 if (typeof options.onShow == 'function') { dialog.addEventListener('show', function (event) { options.onShow.call(dialog, event); }); } if (typeof options.onHide == 'function') { dialog.addEventListener('hide', function (event) { options.onHide.call(dialog, event); }); } if (typeof options.onRemove == 'function') { dialog.addEventListener('remove', function (event) { options.onRemove.call(dialog, event); }); } } // 显示 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部分主要功能 let 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; // 返回eleTarget才是的样式计算对象 const objStyleTarget = window.getComputedStyle(dialog); // 此时元素的层级 const numZIndexTarget = objStyleTarget.zIndex; // 用来对比的层级,也是最小层级 let numZIndexNew = 19; // 只对<body>子元素进行层级最大化计算处理 document.body.childNodes.forEach(function (eleChild) { if (eleChild.nodeType !== 1) { return; } 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; } } }; // 对弹框元素进行方法注册 let 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, { setParams: { value: function (options) { Object.assign(this.params, options || {}); 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]; } } // 是否单按钮 const isOnlyButton = (objParams.buttons.length === 1); // 按钮默认的文字填充 let arrDefultFill = ['取消', '确定']; let indexMainButton = 1; // 根据 events 参数,确定哪个是确定 if (objParams.buttons.length === 2 && objParams.buttons[0].events && !objParams.buttons[1].events) { arrDefultFill = arrDefultFill.reverse(); indexMainButton = 0; } // 按钮元素创建 objParams.buttons.forEach(function (objButton, numIndex) { // objButton可能是null等 objButton = objButton || {}; // 按钮类型和值的处理 let strType = objButton.type; let strValue = objButton.value; if (!strType && (isOnlyButton || numIndex === indexMainButton)) { strType = 'primary'; } if (!strValue) { if (isOnlyButton) { strValue = '确定'; } else { strValue = arrDefultFill[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); if (strType == 'reset') { eleButton.type = strType; } else { eleButton.type = 'submit'; } } // 自定义的类名 if (objButton.className) { eleButton.className = objButton.className; } // 按钮样式 eleButton.setAttribute('is', 'ui-button'); // 无底色无线框按钮 eleButton.setAttribute('blank', '') if (strType) { let strAttrType = eleButton.hasAttribute('form') ? 'data-type' : 'type'; eleButton.setAttribute(strAttrType, strType); } // 按钮是否禁用 eleButton.disabled = Boolean(objButton.disabled); // 按钮内容 eleButton.innerHTML = strValue; // 插入分隔线 if (numIndex) { let eleHr = document.createElement('hr'); eleHr.setAttribute('v', ''); objElement.footer.append(eleHr); } // 放在底部元素中 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); }); } }); // 和关闭按钮的关系 if (objElement.close && typeof objParams.close == 'undefined') { objElement.close.style.display = objParams.buttons.length ? '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', () => { // 关闭弹框 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: [{}], close: false }; // 最终参数 const objParams = { ...defaults, ...options }; if (objParams.type === 'fail') { objParams.type = 'danger'; } if (objParams.buttons.length) { if (!objParams.buttons[0].type) { objParams.buttons[0].type = objParams.type; // 如果是自定义类型,则使用'primary'作为按钮类型 if (/^(?:remind|primary|success|safe|warn|warning|danger|error)$/.test(objParams.type) === false) { objParams.buttons[0].type = defaults.type; } } if (!objParams.buttons[0].value) { objParams.buttons[0].value = '确定'; } } 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.show(); 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: [{}, {}], close: false }; // 最终参数 const objParams = { ...defaults, ...options }; if (objParams.type === 'error' || objParams.type === 'fail') { objParams.type = 'danger'; } if (objParams.type === 'primary') { objParams.type = 'remind'; } // danger 类型的按钮可缺省 // events 所在的按钮作为确认按钮 if (objParams.buttons.length === 2) { if (objParams.buttons[0].events && !objParams.buttons[1].events) { // 参数两个对象调换顺序 objParams.buttons = objParams.buttons.reverse(); } if (!objParams.buttons[1].type) { objParams.buttons[1].type = objParams.type; // 如果是自定义类型,则使用'danger'作为按钮类型 if (/^(?:remind|primary|success|safe|warn|warning|danger|error)$/.test(objParams.type) === false) { objParams.buttons[1].type = defaults.type; } } } let nodes = new DOMParser().parseFromString(strContent, 'text/html').body.childNodes; if (nodes.length == 1) { // 如果是纯文本 if (nodes[0].nodeType === Node.TEXT_NODE) { 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.show(); return this; } }, /** * loading弹框,通常用在ajax请求之前使用 * loading结束后可以直接调用弹框实例的open()方法显示 * @return {Object} 返回当前实例对象 */ loading: { value: function () { const objElement = this.element; const eleDialog = objElement.dialog; this.params.content = '<ui-loading rows="10" size="3"></ui-loading>'; // 显示loading样式 eleDialog.classList.add(CL.add('loading')); // 点击隐藏,防止假死 // 10s 过期时间 setTimeout(() => { if (/loading/.test(eleDialog.className)) { eleDialog.addEventListener('click', () => { if (/loading/.test(eleDialog.className)) { this.remove(); } }); } }, 10000); this.show(); 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>元素 */ scrollbar: { value: function () { const eleAllDialog = document.querySelectorAll('dialog[is="ui-dialog"]'); // 是否有显示的弹框 const isDisplayed = [].slice.call(eleAllDialog).some(function (eleDialog) { return window.getComputedStyle(eleDialog).display !== 'none'; }); document.documentElement.style.overflow = ''; document.body.style.borderRight = ''; let widthScrollbar = window.innerWidth - document.documentElement.clientWidth; // 因为去掉了滚动条,所以宽度需要偏移,保证页面内容没有晃动 if (isDisplayed) { // 所有PC浏览器都滚动锁定 document.documentElement.style.overflow = 'hidden'; document.body.style.borderRight = widthScrollbar + 'px solid transparent'; } return this; } }, /** * 键盘访问与聚焦的细节设置 * @returns 当前<dialog>元素 */ tabindex: { value: function () { var eleDialog = this.element.dialog; var eleLastActiveElement = this.lastActiveElement; if (this.open == true) { var eleActiveElement = document.activeElement; if (this.type == 'alert' || this.type == 'confirm') { if (this.element.button0 != eleActiveElement) { this.lastActiveElement = eleActiveElement; } this.element.button0.focus(); } else if (eleDialog) { if (eleDialog != eleActiveElement) { this.lastActiveElement = eleActiveElement; } // 键盘索引起始位置变为在弹框元素上 eleDialog.focus(); } } else if (eleLastActiveElement && eleLastActiveElement.tagName.toLowerCase() != 'body') { // 键盘焦点元素还原 eleLastActiveElement.focus(); eleLastActiveElement.blur(); this.lastActiveElement = null; } return this; } }, /** * 弹框显示 * @returns 当前<dialog>元素 */ show: { value: function () { if (this.open !== true) { this.classList.add(CL.add('animation')); } // 弹框显示 this.open = true; // 面板显示 if (this.zIndex) { this.zIndex(); } if (this.element.close && typeof this.params.close != 'undefined') { this.element.close.style.display = this.params.close ? '' : 'none'; } 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; } } }); // 暴露的参数 // 并观察参数变化 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 (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 = document.createElement('div'); eleDialog.classList.add(CL); // 使该元素也可以被focus eleDialog.setAttribute('tabindex', '-1'); // 2. 标题 const eleTitle = document.createElement('h4'); eleTitle.classList.add(CL.add('title')); eleTitle.innerHTML = dialog.title; dialog.removeAttribute('title'); // 3. 关闭按钮 // 关闭按钮元素创建 const eleClose = document.createElement('button'); eleClose.textContent = '关闭'; eleClose.classList.add(CL.add('close')); // 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]; // 原始节点放在eleBody主体内容元素中 if (nodesOriginDialog.length) { nodesOriginDialog.forEach(node => { eleBody.append(node); }); } // 元素插入 // 组装 eleDialog.append(eleClose, eleTitle, eleBody, eleFooter); dialog.append(eleDialog); // 观察open属性变化 var moDialogOpen = new MutationObserver(function (mutationsList) { mutationsList.forEach(mutation => { let eleDialog = mutation.target; if (mutation.type == 'attributes') { // 滚动条状态变化 eleDialog.scrollbar(); // 焦点变化 eleDialog.tabindex(); } }); }); moDialogOpen.observe(dialog, { attributes: true, attributeFilter: ['open'] }); // 默认模式是关闭 dialog.closeMode = 'hide'; // 事件 dialog.events(); } // 回调 dialog.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-dialog' } })); // 设置定义完毕标志量 dialog.setAttribute('defined', ''); dialog.isConnectedCallback = true; }; // 弹框观察并注册 let 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;