UNPKG

lu2

Version:

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

657 lines (591 loc) 21.7 kB
/** * @Pagination.js * @author XboxYan(yanwenbin) * @version * @Created: 20-04-22 * @edit: 20-04-22 */ class Pagination extends HTMLElement { static get observedAttributes () { return ['per', 'total', 'current', 'loading']; } constructor ({per, total, current, loading, href, container = null, onChange = () => {}} = {}) { super(); const shadowRoot = this.attachShadow({ mode: 'open' }); const isLink = href || this.href; const el = isLink ? 'a' : 'button'; if (per) { this.per = per; } if (total) { this.total = total; } if (current) { this.setAttribute('current', current); } this.loading = loading; this.onchange = onChange; shadowRoot.innerHTML = ` <style> :host { display: flex; font-size: 14px; height: 30px; align-items: center; } .ui-page { display: inline-flex; min-width: 18px; padding: 0 var(--ui-page-padding, 2px); margin: 0 5px; height: var(--ui-page-height, 28px); border: 1px solid transparent; border-radius: var(--ui-page-radius, 0); color: var(--ui-gray, #a2a9b6); font-size: var(--ui-font, 14px); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; transition: border-color var(--ui-animate-time, .2s), background-color var(--ui-animate-time, .2s); text-decoration: none; user-select: none; position: relative; justify-content: center; align-items: center; background: none; box-sizing: content-box; } .ui-page:not(:focus-visible){ outline: 0; } .ui-page[current] { cursor: default; } .ui-page:not([current]):not([disabled]):not(:disabled):hover { border-color: #b6bbc6; color: var(--ui-gray, #a2a9b6); cursor: pointer; } .ui-page:disabled { color: #ccd0d7; cursor: default; } .ui-page > svg { width: 20px; height: 20px; } .ui-page-prev, .ui-page-next { text-align: center; fill: currentColor; overflow: hidden; } /* 当前不可点的按钮颜色 */ span.ui-page-prev, span.ui-page-next { color: var(--ui-diabled, #ccd0d7); } .ui-page-next svg { transform: scaleX(-1); } .ui-page-prev { margin-left: 0; } .ui-page-next { margin-right: 0; } .ui-page-ellipsis { display: inline-block; } :host(:not([loading]):not([data-loading=true])) .ui-page[current] { color: var(--ui-white, #ffffff); background-color: var(--ui-blue, #2a80eb); } .ui-page-text { color: var(--ui-dark, #4c5161); } .ui-page.loading > svg { visibility: hidden; } :host([loading]) .ui-page[current]::before, :host([data-loading=true]) .ui-page[current]::before { content: ""; position: absolute; left: 0; top: 0; right: 0; bottom: 0; background-repeat: no-repeat; width: 20px; height: 20px; background: url("data:image/svg+xml,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M512 1024q-104 0-199-40-92-39-163-110T40 711Q0 616 0 512q0-15 10.5-25.5T36 476t25.5 10.5T72 512q0 90 35 171 33 79 94 140t140 95q81 34 171 34t171-35q79-33 140-94t95-140q34-81 34-171t-35-171q-33-79-94-140t-140-95q-81-34-171-34-15 0-25.5-10.5T476 36t10.5-25.5T512 0q104 0 199 40 92 39 163 110t110 163q40 95 40 199t-40 199q-39 92-110 163T711 984q-95 40-199 40z' fill='%232a80eb'/%3E%3C/svg%3E") no-repeat center; background-size: 20px 20px; margin: auto; animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } } .simple-page { width: auto; padding: 0 .625em; pointer-events: none; color: #4c5161; } .page { display: inline-flex; height: 100%; align-items: center; } .pagination-wrap { display: contents; visibility: var(--ui-visibility, initial); } @media (prefers-reduced-motion: reduce) { .ui-page { transition: none; } } </style> <fieldset id="wrap" class="pagination-wrap"> <${el} class="ui-page ui-page-prev" id="left"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><path d="M85.876,100.5l49.537-50.526c4.089-4.215,4.089-11.049,0-15.262 c-4.089-4.218-10.719-4.218-14.808,0L63.586,92.868c-4.089,4.215-4.089,11.049,0,15.264l57.018,58.156 c4.089,4.215,10.719,4.215,14.808,0c4.089-4.215,4.089-11.049,0-15.262L85.876,100.5z"></path></svg> </${el}> <div class="page" id="page"></div> <${el} class="ui-page ui-page-next" id="right"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><path d="M85.876,100.5l49.537-50.526c4.089-4.215,4.089-11.049,0-15.262 c-4.089-4.218-10.719-4.218-14.808,0L63.586,92.868c-4.089,4.215-4.089,11.049,0,15.264l57.018,58.156 c4.089,4.215,10.719,4.215,14.808,0c4.089-4.215,4.089-11.049,0-15.262L85.876,100.5z"></path></svg> </${el}> </fieldset> `; // trigger元素的获取 this.element = new Proxy({}, { get: (target, prop) => { if (prop == 'trigger') { return this.htmlFor && document.getElementById(this.htmlFor); } } }); // 如果存在容器,就append进去 if (typeof container == 'string') { container = document.querySelector(container); } if (container) { container.append(this); } } get per () { return Number(this.getAttribute('per')) || 15; } get simple () { return this.getAttribute('mode') === 'short'; } get total () { return Number(this.getAttribute('total')) || 0; } get current () { return Number(this.getAttribute('current')) || 1; } get loading () { return this.getAttribute('loading') !== null; } get href () { //?page=1 return this.getAttribute('href'); } set current (value) { this.setAttribute('current', Math.min(Math.max(1, value), this.count)); } set per (value) { this.setAttribute('per', value); } set total (value) { this.setAttribute('total', value); } set loading (value) { if (!value) { this.removeAttribute('loading'); } else { this.setAttribute('loading', ''); } } get htmlFor () { return this.getAttribute('for'); } set htmlFor (value) { this.setAttribute('for', value); } render (per, total) { const item = this.href ? 'a' : 'button'; this.count = Math.ceil(total / per) || 1; const current = Math.min(Math.max(1, this.current), this.count); if (this.simple) { const html = `<div class="simple-page ui-page" >${current} / ${this.count}</div>`; this.page.innerHTML = html; } else { // 生成一个长度为count,且最大长度为10的数组 [undefined,undefined,undefined,undefined,undefined,...] const arr = Array.from({length: this.count}).splice(0, 9); // 将数组映射成每一页的节点 const html = arr.map((el, index) => { // <button class="ui-page" data-current="2" aria-label="第2页,共20页">2</button> return `<${item} class="ui-page" data-current="${index + 1}" aria-label="第${index + 1}页,共${this.count}页">${index + 1}</${item}>`; }).join(''); this.page.innerHTML = html; } // 存在同时改变多个自定义元素属性的情况,此时应该执行一次 clearTimeout(this.timerRender); this.timerRender = setTimeout(() => { this.updatePage(current); }); } updatePage (current = this.current) { if (current == 1) { this.left.setAttribute('disabled', true); this.left.setAttribute('aria-label', '已经是第一页了'); this.left.removeAttribute('href'); } else { this.left.removeAttribute('disabled'); this.left.setAttribute('aria-label', `上一页,当前第${current}页`); this.left.href = this.href ? this.href.replace(/\${current}/g, current - 1) : 'javascript:;'; } if (current == this.count) { this.right.setAttribute('disabled', true); this.right.setAttribute('aria-label', '已经是最后一页了'); this.right.removeAttribute('href'); } else { this.right.removeAttribute('disabled'); this.right.setAttribute('aria-label', `下一页,当前第${current}页`); this.right.href = this.href ? this.href.replace(/\${current}/g, current + 1) : 'javascript:;'; } if (this.simple) { this.page.querySelector('.simple-page').textContent = current + ' / ' + this.count; } else if (this.count > 9) { let place = []; switch (current) { case 1: case 2: case 3: case 4: case 5: place = [1, 2, 3, 4, 5, 6, 7, 'next', this.count]; break; case this.count: case this.count - 1: case this.count - 2: case this.count - 3: case this.count - 4: place = [1, 'pre', this.count - 6, this.count - 5, this.count - 4, this.count - 3, this.count - 2, this.count - 1, this.count]; break; default: place = [1, 'pre', current - 2, current - 1, current, current + 1, current + 2, 'next', this.count]; break; } this.page.querySelectorAll('.ui-page').forEach((el, i) => { if (typeof place[i] === 'number') { el.dataset.current = place[i]; el.textContent = place[i]; el.disabled = false; el.href = 'javascript:;'; if (place[i] == current) { el.setAttribute('current', ''); if (this.isKeepFocusIn) { el.focus({ preventScroll: true }); } } else { el.removeAttribute('current'); } el.removeAttribute('disabled'); el.setAttribute('aria-label', `第${place[i]}页,共${this.count}页`); if (this.href) { el.href = this.href.replace(/\${current}/g, el.dataset.current); } } else { el.textContent = '...'; el.removeAttribute('current'); el.removeAttribute('data-current'); el.removeAttribute('aria-label'); el.setAttribute('disabled', true); el.removeAttribute('href'); } }); } else { this.page.querySelectorAll('.ui-page').forEach((el) => { if (el.dataset.current == current) { el.setAttribute('current', ''); if (this.isKeepFocusIn) { el.focus({ preventScroll: true }); } } else { el.removeAttribute('current'); } if (this.href) { el.href = this.href.replace(/\${current}/g, el.dataset.current); } }); } } // 上一个聚焦元素 focusPrev () { const current = this.shadowRoot.activeElement; if (current === this.right) { if (this.simple) { this.left.focus(); } else { this.page.lastElementChild.focus(); } } else { const prev = current.previousElementSibling; if (prev) { if (!prev.disabled) { prev.focus(); } else { prev.previousElementSibling.focus(); } } else { this.left.focus(); } } } // 下一个聚焦元素 focusNext () { const current = this.shadowRoot.activeElement; if (current === this.left) { if (this.simple) { this.right.focus(); } else { this.page.firstElementChild.focus(); } } else { const next = current.nextElementSibling; if (next) { if (!next.disabled) { next.focus(); } else { next.nextElementSibling.focus(); } } else { this.right.focus(); } } } connectedCallback () { if (this.isConnectedCallback) { return this; } this.page = this.shadowRoot.getElementById('page'); this.left = this.shadowRoot.getElementById('left'); this.right = this.shadowRoot.getElementById('right'); this.wrap = this.shadowRoot.getElementById('wrap'); this.render(this.per, this.total); this.page.addEventListener('click', (ev) => { const item = ev.target.closest('.ui-page'); if (item) { this.nativeClick = true; this.current = Number(item.dataset.current); } }); this.page.addEventListener('focusin', () => { this.isKeepFocusIn = true; }); this.addEventListener('keydown', (ev) => { if (this.loading) { return; } switch (ev.key) { case 'ArrowDown': case 'PageDown': ev.preventDefault(); this.nativeClick = true; this.current--; break; case 'ArrowUp': case 'PageUp': ev.preventDefault(); this.nativeClick = true; this.current++; break; case 'ArrowLeft': this.focusPrev(); break; case 'ArrowRight': this.focusNext(); break; default: break; } }); this.left.addEventListener('click', () => { this.nativeClick = true; this.current--; this.left.focus(); }); this.right.addEventListener('click', () => { this.nativeClick = true; this.current++; this.right.focus(); }); // 全局事件 this.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-pagination' } })); this.isConnectedCallback = true; // 分页内容准备完毕 this.dispatchEvent(new CustomEvent('DOMContentLoaded')); } attributeChangedCallback (name, oldValue, newValue) { if (!this.page || oldValue === newValue) { return; } let eleTrigger = this.element && this.element.trigger; if (name == 'per') { this.render(newValue, this.total); // 普通元素分页数据per数据同步 if (eleTrigger) { eleTrigger.dataset.per = newValue; } } else if (name == 'total') { this.render(this.per, newValue); // 普通元素分页数据total数据同步 if (eleTrigger) { eleTrigger.dataset.total = newValue; } } else if (name == 'loading') { this.wrap.disabled = newValue !== null; // 普通元素分页数据loading状态同步 if (eleTrigger) { eleTrigger.dataset.loading = newValue !== null; } } else if (name == 'current' && oldValue !== newValue) { // 一定程度上避免冗余的渲染 clearTimeout(this.timerRender); this.timerRender = setTimeout(() => { this.updatePage(Number(newValue)); }); // 普通元素分页数据信息的实时同步 if (eleTrigger) { eleTrigger.dataset.current = newValue; } if (this.nativeClick) { this.nativeClick = false; this.dispatchEvent(new CustomEvent('change', { detail: { current: Number(newValue), per: this.per, total: this.total } })); if (eleTrigger && eleTrigger != this) { eleTrigger.dispatchEvent(new CustomEvent('change', { detail: { current: Number(newValue), per: this.per, total: this.total } })); } } } } } if (!customElements.get('ui-pagination')) { customElements.define('ui-pagination', Pagination); } window.Pagination = Pagination; // 给 HTML 元素扩展 pagination 方法 HTMLElement.prototype.pagination = function (options) { if (this.matches('ui-pagination') || this['ui-pagination']) { return this; } const { total = 0, current = 1, per = 15, href = null, loading = false } = this.dataset; let objParams = Object.assign({}, { per, total, href, loading }, options || {}); const pagination = new Pagination(objParams); const strId = this.id || ('lulu_' + Math.random()).replace('0.', ''); this.innerHTML = ''; this.id = strId; this['ui-pagination'] = pagination; pagination.htmlFor = strId; pagination.setAttribute('current', current); // 删除自定义元素,隐藏不必要的细节 pagination.addEventListener('connected', () => { // 所有普通元素也触发 connected 生命周期事件 this.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-pagination' } })); // 原分页从页面中删除 pagination.remove(); // DOM 执行完毕 this.dispatchEvent(new CustomEvent('DOMContentLoaded')); }); document.body.append(pagination); // 转移shadow dom const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.append(pagination.shadowRoot); // 设置定义完毕标志量 this.setAttribute('defined', ''); return this; }; (function () { const initAllPagination = (ele) => { const elePaginations = ele || document.querySelectorAll('[is-pagination]'); elePaginations.forEach(item => { item.pagination(); }); }; /** * 初始化并监听页面包含is-pagination属性的DOM节点变化 */ const autoInitAndWatchingIsPaginationAttr = () => { // 先实例化已有is-pagination属性的DOM节点,再监听后续的节点变化 initAllPagination(); const observer = new MutationObserver(mutationsList => { mutationsList.forEach(mutation => { mutation.addedNodes && mutation.addedNodes.forEach(eleAdd => { if (!eleAdd.tagName) { return; } if (eleAdd.hasAttribute('is-pagination')) { initAllPagination([eleAdd]); } else { initAllPagination(eleAdd.querySelectorAll('[is-pagination]')); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); }; if (document.readyState != 'loading') { autoInitAndWatchingIsPaginationAttr(); } else { window.addEventListener('DOMContentLoaded', autoInitAndWatchingIsPaginationAttr); } })(); export default Pagination;