UNPKG

lu2

Version:

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

802 lines (677 loc) 28 kB
/** * @Table.js * @author zhangxinxu * @version * @created 15-06-28 * @edited 17-07-18 非模块化使用支持 * 19-12-03 ES5原生语法支持 * 21-01-08 Web Components * 25-06-23 ajax header支持 **/ import '../ui/Drop.js'; import '../ui/Pagination.js'; import Validate from '../ui/Validate.js'; const Table = (function () { /** * 项目表格组件 */ let CHECKED = 'checked'; let SELECTED = 'selected'; // 一些元素类名 let CL = { // 容器 container: 'table-x', // 为空 empty: 'table-null-x', // 错误 error: 'table-error-x', // 分页容器类名 page: 'table-page', // 复选框选择器 checkbox: '[type="checkbox"]' }; class Table extends HTMLTableElement { static get defaultKeyMap () { return { key: '', total: 'total', per: 'per', current: 'current' }; } // 滚动到顶部缓动实现 // rate表示缓动速率,默认是2 static scrollTopTo (top, callback) { let scrollTop = document.scrollingElement.scrollTop; let rate = 2; let funTop = function () { scrollTop = scrollTop + (top - scrollTop) / rate; // 临界判断,终止动画 if (Math.abs(scrollTop - top) <= 1) { document.scrollingElement.scrollTop = top; callback && callback(); return; } document.scrollingElement.scrollTop = scrollTop; // 动画gogogo! requestAnimationFrame(funTop); }; funTop(); } constructor () { super(); this.params = this.params || {}; this.element = this.element || {}; // 内置的固定参数 this.params.parse = (data) => { // 如果有模板,按照模板和内置的ES6模板字符串语法进行渲染 if (this.params.template) { return this.params.template.interpolate(data); } return ''; }; // 其他参数的设置 this.params.ajax = {}; this.params.form = {}; this.params.list = [15, 30, 50]; this.params.page = { // 总数据量 total: 0, // 每页显示数目 per: 15, // 当前的页数 current: 1, // 与后台交互时候UI分页需要的参数和后台分页参数对应关系 // 下面注释的是起点个人中心的接口对应关系 // 其中key表示后台分页数据在那个接口名称下,例如,下面注释内容表示的JSON数据结构是: // { // "code": 0, // "data": { // "pageInfo": { // "pageIndex": 1, // "pageSize": 20, // "pageMax": 6, // "totalCount": 113 // } // } // } // keyMap: { // key: 'pageInfo', // total: 'totalCount', // per: 'pageSize', // current: 'pageIndex' // } // 下面这里未注释的是默认设定 keyMap: Table.defaultKeyMap }; } setParams (options) { options = options || {}; // 分页数据中的keyMap也是一个可选参数对象 // 为了支持嵌套合并,这里提前处理下 let objPageOption = options.page; if (objPageOption) { objPageOption.keyMap = Object.assign({}, this.params.page.keyMap || Table.defaultKeyMap, objPageOption.keyMap || {}); } this.params = Object.assign(this.params, options); return this; } /** * 列表解决方案中的事件处理 * @return {[type]} [description] */ events () { // 实例暴露的参数 let objParams = this.params; let objElement = this.element; // 一些事件 // 单复选框选中效果 this.addEventListener('click', function (event) { let eleCheckbox = event.target; if (!eleCheckbox || eleCheckbox.type != 'checkbox') { return; } let eleTr = eleCheckbox.closest('tr'); // 如果不是第一td中的checkbox,忽略 if (eleTr.querySelector(':first-child ' + CL.checkbox) !== eleCheckbox) { return; } // 获取所有的td标签中的复选框 // 需要tr单元行显示,需要第一个子元素,需要可用 let eleAllTdCheckbox = []; this.querySelectorAll('tr').forEach(function (tr) { if (tr.clientWidth == 0) { return; } // 所有非禁用复选框 let eleTdCheckbox = tr.querySelector('td:first-child ' + CL.checkbox + ':enabled'); if (eleTdCheckbox) { eleAllTdCheckbox.push(eleTdCheckbox); } }); // 全选和非全选标志量 let isAllChecked = false; let isAllUnchecked = false; // 全选 let eleTh = eleCheckbox.closest('th'); // 点击的是全选复选框 if (eleTh) { isAllChecked = eleCheckbox[CHECKED]; isAllUnchecked = !isAllChecked; // 下面所有的td复选框同步 eleAllTdCheckbox.forEach(function (eleTdCheckbox) { eleTdCheckbox[CHECKED] = isAllChecked; }); } else { let numLengthChecked = [].slice.call(eleAllTdCheckbox).filter(function (eleTdCheckbox) { return eleTdCheckbox[CHECKED]; }).length; // 是否取消全选 isAllChecked = (eleAllTdCheckbox.length == numLengthChecked); // 是否全部非选 isAllUnchecked = (numLengthChecked == 0); } // 改变全选复选框的状态 let eleThCheckbox = this.querySelector('th:first-child ' + CL.checkbox); if (eleThCheckbox) { eleThCheckbox[CHECKED] = isAllChecked; } // 根据复选框状态决定表格行样式 eleAllTdCheckbox.forEach(function (eleTdCheckbox) { eleTdCheckbox.closest('tr').classList[eleTdCheckbox[CHECKED] ? 'add' : 'remove'](SELECTED); }); // 回调 this.dispatchEvent(new CustomEvent('check', { detail: { isAllChecked: isAllChecked, isAllUnchecked: isAllUnchecked, target: eleCheckbox, allEnabledCheckbox: eleAllTdCheckbox } })); }); // 点击列表,只有不是a元素或者是复选框本身,选中当前复选框 this.addEventListener('click', function (event) { let eleTarget = event.target; let eleCheckbox = null; if (eleTarget && /^(?:a|input|textarea|tbody|i|select|label|th)$/i.test(eleTarget.tagName) == false) { eleCheckbox = eleTarget.closest('tr') && eleTarget.closest('tr').querySelector('td:first-child ' + CL.checkbox + ':enabled'); if (eleCheckbox) { eleCheckbox.click(); } } }); // 分页选择的事件处理 let elePagination = this.element.pagination; if (elePagination) { elePagination.addEventListener('change', event => { let numCurrent = event.detail.current; // 更新分页 objParams.page.current = numCurrent; // 显示小loading elePagination.loading = true; // ajax再次 this.ajax(); }); } // 切换每页数目的dropList // 求得当前选中的分页数 // 优先本地存储 let strStoreId = this.id; let numCurrentPer = objParams.page.per; let elePer = objElement.drop; // 触发分页数量的下拉元素 if (elePer && elePer.list) { if (strStoreId && localStorage[strStoreId]) { numCurrentPer = localStorage[strStoreId]; objParams.page.per = Number(numCurrentPer); } // 赋值 elePer.textContent = numCurrentPer; // 下拉 elePer.list(() => { return objParams.list.map(function (number) { return { value: number }; }); }, { width: 60, onSelect: (data) => { let numPerNew = data.value; // 记住当前选择的分页数 if (strStoreId) { localStorage[strStoreId] = numPerNew; } // 如果分页有变 if (objParams.page.per != numPerNew) { // 改变分页数目 objParams.page.per = numPerNew; // 当前页重置为1 objParams.page.current = 1; // 重新刷新 this.ajax(); } } }); } // 关联表单的行为处理 const eleForm = this.form; if (eleForm) { new Validate(eleForm, () => { objParams.page.current = 1; this.ajax({ data: this.params.form.data || {} }); }, { validate: () => { return this.params.form.validate || []; } }); } } /** * 列表数据请求 * @param {[type]} options [description] * @return {[type]} [description] */ ajax (options) { options = options || {}; // 暴露的参数和元素 let objParams = this.params; let objElement = this.element; // 避免重复请求 if (this.getAttribute('aria-busy') == 'true') { return this; } // 列表容器元素 let eleContainer = this.parentElement; let eleTbody = this.querySelector('tbody'); let eleForm = this.form; // ajax请求参数 // 可以从4处获取 // 1. options直接传参 // 2. this.setParams()传参,此参数可以多次使用,只要不被重置 // 3. 关联form元素中的参数 // 4. <table>元素上data-*获取,但是只能获取url参数 // 以上优先级越往后越低 // eleForm元素与参数获取 let objAjaxForm = { data: {} }; if (eleForm) { let strAttrAction = eleForm.getAttribute('action'); if (strAttrAction) { objAjaxForm.url = strAttrAction; } eleForm.querySelectorAll('input[name]:enabled, select[name]:enabled, textarea[name]:enabled').forEach(ele => { if (/^(?:submit|image|file)$/.test(ele.type)) { return; } objAjaxForm.data[ele.name] = ele.value; }); } let objAjax = Object.assign({}, this.dataset, objAjaxForm, objParams.ajax, options); // ajax地址是必需项 if (!objAjax.url) { if (this.element.pagination) { this.element.pagination.loading = false; } return this; } // 补充协议头 if (/^\/\//.test(objAjax.url)) { objAjax.url = location.protocol + objAjax.url; } // 1. ajax发送的data数据走合并策略 let data = options.data || {}; let dataForm = objAjaxForm.data; let dataOptions = objParams.ajax.data || {}; if (typeof dataOptions == 'function') { dataOptions = dataOptions() || {}; } if (typeof dataForm == 'function') { dataForm = dataForm() || {}; } if (typeof data == 'function') { data = data() || {}; } // 发送给后台的分页数据的键值对 let dataPage = {}; let objKeyMap = objParams.page.keyMap; if (objKeyMap) { dataPage[objKeyMap['current']] = objParams.page.current; dataPage[objKeyMap['per']] = objParams.page.per; } // current和per数据的更新 let objDataSend = Object.assign({}, dataPage, dataForm, dataOptions, data); if (objKeyMap) { objParams.page.current = objDataSend[objKeyMap['current']]; objParams.page.per = objDataSend[objKeyMap['per']]; } // 2. url get请求地址合并 let objAjaxParams = new URLSearchParams(objDataSend); // URL处理 let strUrlAjax = objAjax.url; // 查询字符串的处理 let strUrlSearch = '?'; if (strUrlAjax.split('?').length > 1) { strUrlSearch = strUrlSearch + strUrlAjax.split('?')[1] + '&'; } // URL拼接 strUrlAjax = strUrlAjax.split('?')[0] + strUrlSearch + objAjaxParams.toString(); // 3. 回调方法的处理 // ajax发生错误的处理 let funError = (content) => { let eleError = objElement.error || eleContainer.querySelector('.' + CL.error); if (!eleError) { eleError = document.createElement('div'); eleError.className = CL.error; // 创建的错误元素插到列表的后面 this.insertAdjacentElement('afterend', eleError); objElement.error = eleError; } eleError.style.display = 'flex'; eleError.innerHTML = content || '数据没有获取成功'; if (typeof objAjax.error == 'function') { objAjax.error(); } }; // 请求完成的事件处理 const funComplete = () => { // 请求结束标志量 this.removeAttribute('aria-busy'); // 去除中间的大loading if (objElement.loading) { objElement.loading.style.display = 'none'; } // 去掉分页的小loading if (objElement.pagination) { objElement.pagination.loading = false; } if (typeof objAjax.complete == 'function') { objAjax.complete(); } // 列表内容呈现 this.show(); }; // 执行Ajax请求的方法 const funAjax = () => { const xhr = new XMLHttpRequest(); // 建立连接 xhr.open('GET', strUrlAjax); // 发送header信息 if (objAjax.headers) { for (let key in objAjax.headers) { xhr.setRequestHeader(key, objAjax.headers[key]); } } xhr.onload = () => { let json = {}; try { json = JSON.parse(xhr.responseText) || {}; } catch (event) { funError('解析异常,请稍后重试'); return; } // 出错处理 // 0认为是成功 // 关键字支持code或者error // { code: 0 } 或 { error: 0 } 都认为成功 if (json.code !== 0 && json.error !== 0) { funError(json.msg || '返回数据格式不符合要求'); return; } let strHtml = objParams.parse(json); // 如果解析后无数据,显示空提示信息 eleTbody.innerHTML = strHtml || ''; let eleEmpty = objElement.empty; if (!strHtml || !strHtml.trim()) { if (!eleEmpty) { eleEmpty = document.querySelector('.' + CL.empty) || document.createElement('div'); eleEmpty.className = CL.empty; // 插入到列表元素后面 this.insertAdjacentElement('afterend', eleEmpty); objElement.empty = eleEmpty; } eleEmpty.style.display = 'flex'; } // 获得后端返回的分页总数 let jsonKey = objKeyMap.key; // 总数目 let numTotal; if (jsonKey) { numTotal = json.data[jsonKey][objKeyMap['total']]; } else { numTotal = json.data[objKeyMap['total']]; } // 修改总数值并显示 if (numTotal || numTotal == 0) { eleContainer.querySelectorAll('output[data-type="total"]').forEach(function (eleTotal) { eleTotal.innerHTML = numTotal; }); objParams.page.total = numTotal; } this.page(); if (typeof objAjax.success == 'function') { objAjax.success(json); } }; xhr.onerror = () => { funError('网络异常,数据没有获取成功,您可以稍后重试!'); }; xhr.onloadend = () => { funComplete(); }; xhr.send(); // 标记输入框忙碌状态 this.setAttribute('aria-busy', 'true'); }; // 滚动到表格上边缘 let numScrollTop = window.pageYOffset; // loading元素 let eleLoading = objElement.loading; if (!eleLoading) { eleLoading = document.createElement('ui-loading'); eleLoading.setAttribute('rows', 15); // 插入到列表元素后面 this.insertAdjacentElement('afterend', eleLoading); objElement.loading = eleLoading; } // 显示loading eleLoading.style.paddingBottom = ''; if (window.getComputedStyle(eleLoading).display == 'none') { let eleThead = this.querySelector('thead'); eleLoading.style.height = (this.clientHeight - (eleThead ? eleThead.clientHeight : 0)) + 'px'; if (eleTbody.innerHTML.trim() == '') { eleLoading.style.height = ''; } } // 微调圈圈的位置 let numDistance = parseFloat(eleLoading.style.height) - window.innerHeight; // loading尺寸比浏览器窗口还大 // 微调未知,使在窗体居中位置 if (numDistance > 0) { eleLoading.style.paddingBottom = numDistance + 'px'; } // loading显示 eleLoading.style.display = 'block'; // 其他元素隐藏 if (objElement.empty) { objElement.empty.style.display = 'none'; } if (objElement.error) { objElement.error.style.display = 'none'; } eleTbody.innerHTML = ''; // 请求走起 // 判断是否需要先滚动 let objBound = this.getBoundingClientRect(); // 第一次不定位 if (!this.isFirstAjax && objBound.top < 0) { numScrollTop = objBound.top + numScrollTop; Table.scrollTopTo(numScrollTop, funAjax); } else if (!this.isFirstAjax && objBound.top > window.innerHeight) { numScrollTop = numScrollTop - objBound.top; Table.scrollTopTo(numScrollTop, funAjax); } else { funAjax(); this.isFirstAjax = false; } return this; } /** * 分页的处理 * @param {[type]} total [description] * @return {[type]} [description] */ page (total) { const objPage = this.params.page; // 分页元素 const elePagination = this.element.pagination; // 显示分页 if (!elePagination) { return; } // 分页数据的设置 const objParamPage = { total: total || objPage.total, current: objPage.current, per: objPage.per, mode: objPage.mode || 'long' }; // 分页数据赋值 for (let key in objParamPage) { elePagination[key] = objParamPage[key]; } } /** * 列表请求完毕显示的方法 * @return {[type]} [description] */ show () { // 显示loading if (this.element.loading) { this.element.loading.style.paddingBottom = ''; } //没有全选 const eleThCheckbox = this.querySelector('th:first-child ' + CL.checkbox); if (eleThCheckbox) { eleThCheckbox[CHECKED] = false; } // 显示的回调 this.dispatchEvent(new CustomEvent('show', { detail: { type: 'ui-table' } })); return this; } // 元素进入页面时候的生命周期函数执行 connectedCallback () { // 获得数据 let eleTbody = this.querySelector('tbody'); if (!eleTbody) { eleTbody = document.createElement('tbody'); this.append(eleTbody); } // 模板元素 let eleTemplate = this.querySelector('template'); if (eleTemplate) { this.params.template = eleTemplate.innerHTML; } // 容器 let eleContainer = this.closest('.' + CL.container); // 分页元素 let elePagination = null; if (eleContainer) { elePagination = eleContainer.querySelector('ui-pagination'); if (elePagination) { this.element.pagination = elePagination; // 设置分页的参数 this.setParams({ page: { current: elePagination.current, total: elePagination.total, per: elePagination.per } }); } else { // 创建一个分页元素 elePagination = document.createElement('ui-pagination'); this.element.pagination = elePagination; // 分页内容显示 let elePage = eleContainer.querySelector('.' + CL.page); if (elePage) { elePage.appendChild(elePagination); } } // loading元素 let eleLoading = eleContainer.querySelector('ui-loading'); if (eleLoading) { this.element.loading = eleLoading; } // 下拉列表,切换分页元素 let eleDrop = eleContainer.querySelector('ui-drop[data-type="per"]'); if (eleDrop) { this.element.drop = eleDrop; } } // 为了动态呈现的列表可以先设置参数,后执行,这里延后 setTimeout(() => { // 事件绑定和处理 this.events(); // 基于tbody内容决定首次交互的行为 if (eleTbody.textContent.trim() == '') { this.isFirstAjax = true; this.ajax(); } else { // 认为是列表第一页直出, // 这样的交互可以有更好体验 this.page(); } }, 1); // 全局事件 this.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-table' } })); this.isConnectedCallback = true; this.dispatchEvent(new CustomEvent('DOMContentLoaded')); } } if (!('').interpolate) { // 字符串转模板字符串 https://www.zhangxinxu.com/wordpress/2020/10/es6-html-template-literal/ String.prototype.interpolate = function (params) { const names = Object.keys(params); const vals = Object.values(params); return new Function(...names, `return \`${(function (str) { return str.replace(/&(lt|gt|amp|quot);/ig, function (all, t) { return ({ 'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"' })[t]; }); })(this)}\`;`)(...vals); }; } return Table; })(); // 给<table>元素扩展form属性 Object.defineProperty(HTMLTableElement.prototype, 'form', { get () { let attrForm = this.getAttribute('form'); if (!attrForm) { return null; } return document.getElementById(attrForm); } }); if (!customElements.get('ui-table')) { customElements.define('ui-table', Table, { extends: 'table' }); }