UNPKG

@suyouwanggang/p-ui

Version:

`p-ui`是一套使用原生`Web Components`规范开发的跨框架UI组件库,基于`lit-elment`库开发。 [github项目地址](https://github.com/suyouwanggang/p-ui)

443 lines (433 loc) 17.3 kB
import { css, customElement, html, LitElement, property, TemplateResult } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html'; /** * @event tab-change 页签改变事件 * @event tab-change-end 页签改变完成事件 * */ type tabPosition = 'top' | 'bottom' | 'left' | 'right'; type tabAgile = '' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-start' | 'flex-end'; @customElement('p-tab') class PTab extends LitElement { static get styles() { return css` :host{ display:block; text-align:unset; } .tab_container{ display: flex; flex-direction: column; } .tab_nav_con{ flex:0 0 auto; position:relative; overflow: hidden; display: flex; color:var(--tab-font-color); scroll-behavior: smooth; } .tab_content{ display:block; flex:1; padding:10px; border-top: 1px solid var(--tab-border-color, rgba(0,0,0,.2)); } .tab_tabs { white-space:nowrap; border-bottom:var(--tab-border-size,2px) solid #FFF; padding:0.4em 1em; cursor: pointer; } .tab_tabs .p-tab-icon{ margin-left:0.4em; } .tab_tabs[disabled]{ opacity: 0.4; cursor: not-allowed; } .tab_on{ border-bottom-color:var(--tab-border-on-color,var(--themeColor,#42b983)); } .tab_container[tabPosition=left], .tab_container[tabPosition=right]{ flex-direction: row; } .tab_container[tabPosition=left] .tab_nav_con, .tab_container[tabPosition=right] .tab_nav_con{ display: flex; flex-direction: column; overflow: hidden; } .tab_container[tabPosition=right] .tab_nav_con{ order:1; border-left:1px solid var(--tab-border-color,rgba(0,0,0,.2) ); } .tab_container[tabPosition=left] .tab_tabs{ display: block; padding:0.6em 0.3em ; border-right:var(--tab-border-size,2px) solid #FFF; } .tab_container[tabPosition=left] .tab_on{ border-bottom: none; border-right:var(--tab-border-size,2px) solid var(--tab-border-on-color,var(--themeColor,#42b983)); } .tab_container[tabPosition=right] .tab_tabs{ display: block; padding:0.6em 0.3em ; border-left:var(--tab-border-size,2px) solid #FFF; text-align: right; } .tab_container[tabPosition=right] .tab_on{ border-bottom: none; border-left:var(--tab-border-size,2px) solid var(--tab-border-on-color,var(--themeColor,#42b983)); } .tab_container[tabPosition=left] .tab_content{ flex:1; border-top:none; margin-left:-1px; border-left:1px solid var(--tab-border-color,rgba(0,0,0,.2)); } .tab_container[tabPosition=right] .tab_content{ flex:1; border-top:none; margin-right:-1px; order:0; } .tab_container[tabPosition=bottom] .tab_nav_con { order:1; border-bottom: none; } .tab_container[tabPosition=bottom] .tab_tabs{ border-top:var(--tab-border-size,2px) solid #FFF; } .tab_container[tabPosition=bottom] .tab_on{ border:none; border-top:var(--tab-border-size,2px) solid var(--tab-border-on-color,var(--themeColor,#42b983)); } .tab_container[tabPosition=bottom] .tab_content{ order:0; border-top:none; border-bottom:1px solid var(--tab-border-color,rgba(0,0,0,.2)); } ::slotted(p-tab-content){ display:none; } ::slotted(p-tab-content[active]){ display:block; } `; } protected renderTabTitle(tabContent: PTabContent): TemplateResult { const slots: HTMLSlotElement = tabContent.renderRoot.querySelector('#header'); const nodeList: Element[] = slots.assignedElements({ flatten: true }); const array = []; for (let i = 0, j = nodeList.length; i < j; i++) { const el: Element = nodeList[i]; if (el instanceof HTMLTemplateElement) { const template = el; const elClone = document.importNode(template.content, true); array.push(elClone); } else if (el.nodeType === 1) { const EL = el as HTMLElement; EL.style.display = ''; const elClone = el.cloneNode(true) as HTMLElement; elClone.removeAttribute('slot'); array.push(elClone); EL.style.display = 'none'; } } return html` <div key=${tabContent.key} ?disabled=${tabContent.disabled} class="tab_tabs ${tabContent.key === this.activeKey ? 'tab_on' : ''} tab_tabs_outer"> ${array.map((el: HTMLElement) => html`${el}`)} </div>`; } protected renderTab(): TemplateResult | Array<TemplateResult> { const xTab = this; const childchild = Array.from(this.children); const result: TemplateResult[] = []; childchild.forEach((element, index) => { if (element instanceof PTabContent) { // tslint:disable-next-line: no-unnecessary-type-assertion const tContent: PTabContent = element as PTabContent; if (tContent.key == null) { tContent.key = index + ''; } if (xTab.activeKey == null) { xTab.activeKey = tContent.key; } if (tContent.key === xTab.activeKey) { tContent.setAttribute('active', ''); } else { tContent.removeAttribute('active'); } result.push(this.renderTabTitle(tContent)); } }); return result; } @property({ type: String, reflect: true }) tabPosition: tabPosition = 'top'; @property({ type: String, reflect: true }) activeKey: string = null; @property({ type: String, reflect: true }) tabAgile: tabAgile = null; render() { return html` <div class="tab_container" tabPosition="${this.tabPosition}" > <div class="tab_nav_con" style="${this.tabAgile ? `justify-content:${this.tabAgile}` : ''}" id='tab_nav_con_id' > ${this.renderTab()} </div> <slot id="slots" class="tab_content" ></slot> </div>`; } firstUpdated() { const slots: HTMLSlotElement = <HTMLSlotElement>this.shadowRoot.getElementById('slots'); slots.addEventListener('slotchange', (event) => { this.requestUpdate(); }); const tab_nav = this.renderRoot.querySelector('#tab_nav_con_id'); tab_nav.addEventListener('click', (event: Event) => { let target = event.target as HTMLElement; if (target.nodeType !== 1) { target = target.parentElement; } if(target===tab_nav){ return ; } if (target !== tab_nav) { target = target.closest('div.tab_tabs[key]'); } const key = target.getAttribute('key'); if (key === this.activeKey) { return; } else { const tabContent = this.findTab(key); if (tabContent === null || tabContent.disabled) { return; } const beforeEvent = new CustomEvent('beforeChange', { cancelable: true, //标识可以取消 detail: { tabContent: tabContent, label: tabContent.label, key: tabContent.key } }); if (this.dispatchEvent(beforeEvent)) { this.activeKey = tabContent.key; this.dispatchChangeEvent(tabContent); } } }); const stopBodyTouch = (ev: TouchEvent) => { ev.preventDefault(); }; const options = { passive: false }; let startX: number = undefined; let startY: number = undefined; slots.addEventListener('touchstart', (event: TouchEvent) => { document.addEventListener('touchstart', stopBodyTouch, options); startX = event.changedTouches[0].pageX, startY = event.changedTouches[0].pageY; }, options); function GetSlideAngle(dx: number, dy: number) { return Math.atan2(dy, dx) * 180 / Math.PI; } //根据起点和终点返回方向 1:向上,2:向下,3:向左,4:向右,0:未滑动 function GetSlideDirection(startX: number, startY: number, endX: number, endY: number) { const dy = startY - endY; const dx = endX - startX; let result = 0; //如果滑动距离太短 if (Math.abs(dx) < 2 && Math.abs(dy) < 2) { return result; } const angle = GetSlideAngle(dx, dy); if (angle >= -45 && angle < 45) { result = 4; } else if (angle >= 45 && angle < 135) { result = 1; } else if (angle >= -135 && angle < -45) { result = 2; } else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) { result = 3; } return result; } let toutchDirect = 0; slots.addEventListener('touchmove', (event: TouchEvent) => { const endX = event.changedTouches[0].pageX; const endY = event.changedTouches[0].pageY; toutchDirect = GetSlideDirection(startX, startY, endX, endY); if (toutchDirect === 3 || toutchDirect === 4) { event.stopPropagation(); event.preventDefault(); document.addEventListener('touchstart', stopBodyTouch, options); } }, options); slots.addEventListener('touchend', (event: TouchEvent) => { document.removeEventListener('touchmove', stopBodyTouch); document.removeEventListener('touchstart', stopBodyTouch); if (toutchDirect === 3 || toutchDirect === 4) { event.preventDefault(); event.stopPropagation(); } const tabs: PTabContent[] = [...this.querySelectorAll('p-tab-content') as any]; let index = tabs.indexOf(this.activeTab); if (toutchDirect === 4) { //left 2 right; if (index > 0) { index--; const tab = tabs[index]; this.activeKey = tab.key; this.dispatchChangeEvent(tab); } } else if (toutchDirect === 3) { //right 2 left; index++; if (index < tabs.length) { const tab = tabs[index]; this.activeKey = tab.key; this.dispatchChangeEvent(tab); } } }); this.requestUpdate(); this.setHeaderScroll(); } dispatchChangeEvent(tabContent: PTabContent) { if (tabContent == null || tabContent.disabled) { return; } const detail = { tabContent: tabContent, label: tabContent.label, index: this.getTabIndex(tabContent), key: tabContent.key } this.dispatchEvent(new CustomEvent('tab-change', { bubbles:true, detail: detail })); this.activeKey = tabContent.key; this.setHeaderScroll(); this.dispatchEvent(new CustomEvent('tab-change-end', { bubbles:true, detail: detail })); } private setHeaderScroll() { const header: HTMLElement = this.renderRoot.querySelector('#tab_nav_con_id'); // tslint:disable-next-line: no-any const tabPos: any = {}; const isTopPosition = this.tabPosition === 'right' || this.tabPosition === 'left'; const items = header.querySelectorAll('div.tab_tabs[key]'); Array.from(items).forEach((item: HTMLElement, index: number) => { // tslint:disable-next-line: no-any const cache: any = { index: index }; if (isTopPosition) { cache.height = item.offsetHeight; cache.top = item.offsetTop; } else { cache.width = item.offsetWidth; cache.left = item.offsetLeft; } tabPos[item.getAttribute('key')] = cache; }); // tslint:disable-next-line: no-any const active: any = tabPos[this.activeKey]; if (isTopPosition) { header.scrollTop = Math.max(0, active.top + active.offsetHeight / 2 - header.offsetHeight / 2); } else { header.scrollLeft = Math.max(active.left + active.width / 2 - header.offsetWidth / 2); } } findTab(key: string): PTabContent { return this.querySelector(`p-tab-content[key="${key}"]`); } getTabIndex(tab: PTabContent) { const children = Array.from<PTabContent>(this.querySelectorAll('p-tab-content')); return children.indexOf(tab); } findTabByIndex(index: number): PTabContent { const children = this.querySelectorAll('p-tab-content'); return index < children.length ? <PTabContent>children[index] : null; } get activeTab(): PTabContent { return this.findTab(this.activeKey); } updated(changedProperties: Map<string | number | symbol, unknown>) { super.updated(changedProperties); if (this.isConnected && (changedProperties.has('tabPosition') || changedProperties.has('tabAgile'))) { this.updateComplete.then(() => { this.setHeaderScroll(); }); } } // attributeChangedCallback(name: string, oldvalue: string | null, newValue: string | null) { // super.attributeChangedCallback(name, oldvalue, newValue); //一定要调用super 方法哦 // if (name === 'activekey' && this.hasUpdated && oldvalue !== newValue && this.renderRoot != null) { // this.dispatchChangeEvent(this.findTab(newValue)); // } // } set activeTabByIndex(index: number) { const tab = this.findTabByIndex(index); if (tab) { this.activeKey = tab.key; } } } @customElement('p-tab-content') class PTabContent extends LitElement { static get styles() { return css` :host{ display:none ; } :host([active]){ display:block; } #header[name=header], ::slotted([slot=header]){ display:none !important; }`; } @property({ type: String, reflect: true }) label: string = null; @property({ type: String, reflect: true }) key: string = null; @property({ type: String, reflect: true }) icon: string = null; @property({ type: Boolean, reflect: true }) disabled: boolean = false; render() { const pTab: PTab = this.tab; return html` <slot id="header" name='header' style="display:none;"> <span class='tab_label'>${this.label}</span> ${this.icon ? html`<p-icon class='p-tab-icon' name=${this.icon}></p-icon>` : ''} </slot> <slot></slot>`; } get tab(): PTab { return this.closest('p-tab'); } updated(changeMap: Map<string | number | symbol, unknown>) { super.updated(changeMap); if (changeMap != null && (changeMap.has('label') || changeMap.has('icon') || changeMap.has('disabled'))) { this.updateTabHeader(); } } updateTabHeader() { this.tab.requestUpdate(); } firstUpdated() { const tabPanel = this; const slot: HTMLSlotElement = this.renderRoot.querySelector('#header'); slot.addEventListener('slotchange', () => { tabPanel.updateTabHeader(); }); } setActive() { this.tab.activeKey = this.key; } } export { PTab, PTabContent };