UNPKG

@wiajs/ui

Version:

wia app ui packages

501 lines (500 loc) 16.7 kB
/** @jsxImportSource @wiajs/core */ import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime"; import { Event } from '@wiajs/core'; import { log as Log } from '@wiajs/util'; const log = Log({ m: 'navTab' }) // 创建日志实例 ; /** * @typedef {{path?:string, title?:string, owner?:string,appName?:string,name?:string,param?:*, active?: boolean}} Tab */ /** @type {*} */ const { $ } = window; // 缺省值 const def = { selector: '.data-table', active: 'prj/', domProp: 'wiaNavTab' }; /** * 历史页面的导航条 * 共用部件,不要使用全局变量! */ export default class NavTab extends Event { /** * * @param {*} page 页面实例 * @param {*} opts 选项 */ constructor(page, opts){ const opt = { ...def, ...opts }; super(opt, [ page ]), /** @type {Tab[]} */ this.tabs = [], /** @type {{li:HTMLElement, tab: Tab, dist: number}[]} */ this.leftTabs = [], /** @type {{li:HTMLElement, tab: Tab, dist: number}[]} */ this.rightTabs = []; const _ = this; _.page = page; _.view = page.view; _.opt = opt; // 容器 const $el = opt.el || _.view.findNode(opt.selector); if ($el.length === 0) return undefined; _.el = $el; if (opt.tabs?.length) _.tabs = opt.tabs; // 已创建,直接返回 if (_.el.dom[opt.domProp]) { const instance = _.el.dom[opt.domProp]; _.destroy(); return instance; } _.el.dom[opt.domProp] = _; _.init(); _.render(); _.bind(); } init() { const _ = this; _.intObs = _.createObserver(); } /** * 绑定点击事件 */ bind() { const _ = this; try { const { el, tabs, opt } = _; const { btnLeft, btnRight, lsLeft, lsRight } = opt; btnLeft?.click((ev)=>{ if (_.leftTabs.length === 0) return; let cnt = el.find('li.nav-item.visible').length; cnt = Math.min(cnt, _.leftTabs.length); _.intoView(_.leftTabs[cnt - 1].li); }); btnRight?.click((ev)=>{ if (_.rightTabs.length === 0) return; let cnt = el.find('li.nav-item.visible').length; cnt = Math.min(cnt, _.rightTabs.length); // 获取最接近右侧的元素 _.intoView(_.rightTabs[cnt - 1].li); }); // 点击下拉菜单 lsLeft.click((ev)=>{ const mu = lsLeft.find('.dropdown-menu'); if (!mu?.hasClass('show')) mu?.addClass('show'); else mu?.removeClass('show'); }); lsRight.click((ev)=>{ const mu = lsRight.find('.dropdown-menu'); if (!mu?.hasClass('show')) mu?.addClass('show'); else mu?.removeClass('show'); }); const muLeft = lsLeft.find('.dropdown-menu'); muLeft?.click((ev)=>{ const el = $(ev).upper('a.dropdown-item'); if (el.dom) { const path = el.attr('path'); if (path) { _.active(path); $.go(path); } } }); const muRight = lsRight.find('.dropdown-menu'); muRight?.click((ev)=>{ const el = $(ev).upper('a.dropdown-item'); if (el.dom) { const path = el.attr('path'); if (path) { _.active(path); $.go(path); } } }); // tab点击 el.click((ev)=>{ const btn = $(ev).upper('.btn'); if (btn.dom) { const path = btn.upper('.nav-item').find('.nav-link').data('path'); if (path) _.delTab(path); } else { const link = $(ev).upper('.nav-link'); if (link.dom) { const path = link.data('path'); const r = tabs.find((v)=>v.path === path); if (path) $.go(path, r?.param); } } }); // 路由事件标签页 $.router.on('show', (ev)=>{ _.showTab(ev); }); $.router.on('back', (ev)=>{ _.showTab(ev); }); $.router.on('hide', (ev)=>{ // log(ev, 'router hide') // hideTab(ev) }); // 捕获应用级别页面事件; $.app.on('pageShow', (p)=>{ if (p?.path) { console.log('pageShow:', p.path); // _.active(p.path) } }); // 捕获应用级别页面事件; $.app.on('pageBack', (p)=>{ if (p?.path) { console.log('pageBack:', p.path); // _.active(p.path) } }); } catch (e) { log.err(e, 'bind'); } } createObserver() { let R; const _ = this; try { // 返回一个防抖函数,更新隐藏元素列表 const upHandler = debounce(()=>{ _.upTabs(); _.upButs(); }, 300); // 创建观察器 const obs = new IntersectionObserver((entries)=>{ for (const entry of entries){ const li = entry.target; // const id = li.dataset.id const { intersectionRatio: visibleRatio, isIntersecting } = entry; const { path } = li.tab; if (path === 'prj/index' || path === 'prj/detail') { console.log({ len: entries.length, visibleRatio, isIntersecting }, 'IntObserver'); } if (isIntersecting && visibleRatio >= 0.8) li.classList.add('visible'); else if (!isIntersecting) li.classList.remove('visible'); } // 更新隐藏元素列表 upHandler(); }, { root: _.el.dom, threshold: [ 0.8, 1 ] }); R = obs; log({ R }, 'createObserver'); } catch (e) { log.err(e, 'createObserver'); } return R; } /** * 更新隐藏元素列表 */ upTabs() { const _ = this; try { const { el, tabs, opt } = _; const { lsLeft, lsRight } = opt; _.leftTabs = []; _.rightTabs = []; const root = el.dom.getBoundingClientRect(); const ls = el.find('li.nav-item:not(.visible)').get(); for (const li of ls){ const rect = li.getBoundingClientRect(); // 计算元素相对于navbar的位置 const left = rect.left - root.left; const right = rect.right - root.right; /** @type {Tab} */ const { tab } = li; if (left < 0) { // 元素在可视区域左侧 _.leftTabs.push({ li, tab, dist: Math.abs(right) }); } else if (right > 0) { // 元素在可视区域右侧 _.rightTabs.push({ li, tab: li.tab, dist: left - root.width }); } } // 按距离排序(距离近的在前) _.leftTabs.sort((a, b)=>a.dist - b.dist); _.rightTabs.sort((a, b)=>a.dist - b.dist); let mu = lsLeft.find('.dropdown-menu'); mu.empty(); for (const r of _.leftTabs){ const { tab } = r; mu.append(/*#__PURE__*/ _jsx("a", { path: tab.path, class: "dropdown-item", children: tab.title })); } mu = lsRight.find('.dropdown-menu'); mu.empty(); for (const r of _.rightTabs){ const { tab } = r; mu.append(/*#__PURE__*/ _jsx("a", { path: tab.path, class: "dropdown-item", children: tab.title })); } // 更新UI // updateHiddenPanels() log({ leftTabs: _.leftTabs, rightTabs: _.rightTabs }, 'upTabs'); } catch (e) { log.err(e, 'upTabs'); } } // 更新按钮状态 upButs() { const _ = this; const { opt, leftTabs, rightTabs } = _; // opt.btnLeft.dom.visible = if (leftTabs.length === 0) { opt.btnLeft?.hide(); opt.lsLeft?.hide(); } else { opt.btnLeft?.show(); opt.lsLeft?.show(); } // opt.btnRight.dom.visible = if (rightTabs.length === 0) { opt.btnRight.hide(); opt.lsRight.hide(); } else { opt.btnRight.show(); opt.lsRight.show(); } } showBtns() { const _ = this; const { opt } = _; opt.btnLeft?.show(); opt.btnRight?.show(); opt.lsLeft?.show(); opt.lsRight?.show(); } /** * 显示tab,不存在创建,存在则显示并激活 * @param {Tab} tab * @param {boolean} [init] */ showTab(tab, init = false) { const _ = this; try { const { el, tabs } = _; const { name, path, appName, param, title, active } = tab; if (path === 'master') return; let r; if (!init) { r = tabs.find((v)=>v.path === path); tab.active = true; if (!r) tabs.push(tab); } // _.showBtns() // 后续显示按钮,可视区缩小,激活的标签可能被隐藏 if (r) _.active(r.path); else { let li = /*#__PURE__*/ _jsxs("li", { class: "nav-item", children: [ /*#__PURE__*/ _jsx("a", { class: "nav-link active", "data-path": path, children: title }), /*#__PURE__*/ _jsx("button", { type: "button", class: "btn btn-tool", children: /*#__PURE__*/ _jsx("i", { class: "icon f7icon", children: "delete_md" }) }) ] }); el.find('a.nav-link.active').removeClass('active'); el.append(li); li = el.dom.lastElementChild; li.tab = tab // 保存到 li中 ; // 渲染完成后启动子元素li观察,避免监视失败 setTimeout(()=>{ _.observe(li); _.intoView(li); }, 100); } } catch (e) { log.err(e, 'navTab'); } } /** * * @param {HTMLElement} li */ intoView(li) { const _ = this; try { if (!_.inView(li)) { _.showBtns() // 后续显示按钮,可视区缩小,激活的标签可能被隐藏 ; $.nextTick(()=>li.scrollIntoView(), 0); } // log.info({li}, 'intoView') } catch (e) { log.err(e, 'intoView'); } } /** * 是否在可视区域 * @param {HTMLElement} li * @returns {boolean} */ inView(li) { let R = false; const _ = this; try { const { el } = _; const root = el.dom.getBoundingClientRect(); const rect = li.getBoundingClientRect(); const left = rect.left - root.left; const right = rect.right - root.right; R = left >= 0 && right <= 0; } catch (e) { log.err(e, 'inView'); } return R; } /** * * @param {*} item */ observe(item) { const _ = this; _.intObs.observe(item); } /** * 删除tab * @param {string} path */ delTab(path) { const _ = this; try { const { el, tabs } = _; if (path === 'master') return; const i = tabs.findIndex((v)=>v.path === path); const r = tabs[i]; if (r) { tabs.splice(i, 1); el.find(`a.nav-link[data-path="${path}"]`).upper('li.nav-item').remove(); if (r.active) $.back(); } } catch (e) { log.err(e, 'delTab'); } } /** * * @param {*} param */ hideTab({ name, path, appName, param, title } = {}) { const _ = this; try { if (path === 'master') return; const { el, tabs } = _; const id = tabs.findIndex((v)=>v.path === path); if (id > -1) { tabs.splice(id, 1); const menu = /*#__PURE__*/ _jsx("li", { class: "nav-item", children: /*#__PURE__*/ _jsx("a", { class: "nav-link", "data-widget": "pushmenu", role: "button", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) }); const nav = tabs.map((v)=>{ return /*#__PURE__*/ _jsx("li", { class: "nav-item d-none d-sm-inline-block", "data-path": path, children: /*#__PURE__*/ _jsx("a", { class: `nav-link ${v.active && 'active'}`, children: v.title }) }); }); nav.unshift(menu); el.empty().html(nav.join('')); } } catch (e) { log.err(e, 'hideTab'); } } /** * * @returns */ render() { const _ = this; try { const { el, tabs, opt } = _; if (tabs?.length) for (const tab of tabs)_.showTab(tab, true); else el.empty(); if (opt.active) _.active(opt.active); // 获取所有导航项 // const ls = el.find('li.nav-item').get() // for (const li of ls) _.observe(li) // _.bind() } catch (e) { log(e, 'render'); } } /** * 激活tab * @param {*} path */ active(path) { const _ = this; try { const { el, tabs } = _; if (!path || !tabs.length) return; let tab = el.find('a.nav-link.active'); if (tab?.data('path') !== path) { tab.removeClass('active'); tab = el.find(`a.nav-link[data-path="${path}"]`); if (tab.dom) tab.addClass('active'); } const li = tab.upper('li.nav-item'); if (li.dom) _.intoView(li.dom); } catch (ex) { console.log('active exp:', ex.message); } } } /** * 防抖,返回一个回调函数,限定时间内,多次执行时,仅执行一次 * 使用时,需多次执行 该函数返回的函数 * @param {() => void} func * @param {number} [wait] * @returns {(...args: any[]) => void} */ const debounce = (func, wait = 300)=>{ /** @type {NodeJS.Timeout} */ let timer; return function(...args) { clearTimeout(timer); // 设置新的定时器 timer = setTimeout(()=>{ func.apply(this, args); }, wait); }; };