portal-l1-select-flat-nav-demo.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>门户顶栏 · 一级下拉 + 中间平铺(更多溢出)Demo</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --header: #1ca851;
  13. --text: #0f172a;
  14. --muted: #64748b;
  15. --border: #e2e8f0;
  16. --bg: #f1f5f9;
  17. }
  18. * { box-sizing: border-box; }
  19. body {
  20. margin: 0;
  21. font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  22. color: var(--text);
  23. background: #f8fafc;
  24. line-height: 1.5;
  25. }
  26. .page { padding: 20px 24px 56px; max-width: 1280px; margin: 0 auto; }
  27. h1 { font-size: 22px; margin: 0 0 6px; }
  28. .lead { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
  29. .ok, .note {
  30. border-radius: 10px;
  31. padding: 10px 14px;
  32. font-size: 13px;
  33. margin-bottom: 12px;
  34. }
  35. .ok {
  36. background: var(--primary-bg);
  37. border: 1px solid #a7f3d0;
  38. color: #065f46;
  39. }
  40. .note {
  41. background: #fffbeb;
  42. border: 1px solid #fde68a;
  43. color: #92400e;
  44. }
  45. .controls {
  46. display: flex;
  47. flex-wrap: wrap;
  48. gap: 10px 18px;
  49. align-items: center;
  50. margin-bottom: 14px;
  51. padding: 12px 14px;
  52. background: #fff;
  53. border: 1px solid var(--border);
  54. border-radius: 12px;
  55. font-size: 13px;
  56. }
  57. .stage-label {
  58. display: flex;
  59. justify-content: space-between;
  60. align-items: baseline;
  61. gap: 12px;
  62. margin: 8px 0 8px;
  63. }
  64. .stage-label h2 { margin: 0; font-size: 16px; }
  65. .stage-label span { font-size: 12px; color: var(--muted); }
  66. .frame {
  67. border: 1px solid var(--border);
  68. border-radius: 12px;
  69. overflow: visible;
  70. background: #fff;
  71. box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  72. }
  73. .portal-header {
  74. position: relative;
  75. display: flex;
  76. align-items: center;
  77. gap: 8px;
  78. height: 56px;
  79. padding: 0 16px 0 0;
  80. background: var(--header);
  81. color: #fff;
  82. user-select: none;
  83. overflow: visible;
  84. }
  85. .brand {
  86. display: flex;
  87. align-items: center;
  88. gap: 10px;
  89. flex-shrink: 0;
  90. padding-left: 16px;
  91. z-index: 2;
  92. }
  93. .logo {
  94. width: 28px;
  95. height: 28px;
  96. border-radius: 8px;
  97. background: rgba(255, 255, 255, 0.2);
  98. display: grid;
  99. place-items: center;
  100. font-size: 12px;
  101. font-weight: 700;
  102. }
  103. .brand-title {
  104. margin: 0;
  105. font-size: 15px;
  106. font-weight: 600;
  107. white-space: nowrap;
  108. }
  109. .l1-select {
  110. position: relative;
  111. flex-shrink: 0;
  112. z-index: 5;
  113. }
  114. .l1-trigger {
  115. display: inline-flex;
  116. align-items: center;
  117. gap: 6px;
  118. height: 32px;
  119. padding: 0 10px 0 12px;
  120. border: 1px solid rgba(255, 255, 255, 0.35);
  121. border-radius: 8px;
  122. background: rgba(255, 255, 255, 0.12);
  123. color: #fff;
  124. font-size: 13px;
  125. font-weight: 600;
  126. cursor: pointer;
  127. white-space: nowrap;
  128. }
  129. .l1-trigger:hover,
  130. .l1-select.is-open .l1-trigger {
  131. background: rgba(255, 255, 255, 0.22);
  132. border-color: rgba(255, 255, 255, 0.55);
  133. }
  134. .l1-trigger .caret { opacity: 0.85; font-size: 11px; }
  135. .dropdown-panel {
  136. display: none;
  137. position: absolute;
  138. top: calc(100% + 6px);
  139. left: 0;
  140. min-width: 168px;
  141. max-height: 320px;
  142. overflow: auto;
  143. padding: 6px;
  144. background: #fff;
  145. border: 1px solid var(--border);
  146. border-radius: 10px;
  147. box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
  148. z-index: 40;
  149. }
  150. .dropdown-panel.align-right { left: auto; right: 0; }
  151. .is-open > .dropdown-panel { display: block; }
  152. .opt {
  153. display: block;
  154. width: 100%;
  155. text-align: left;
  156. border: 0;
  157. background: transparent;
  158. border-radius: 8px;
  159. padding: 8px 10px;
  160. font-size: 13px;
  161. color: #1e293b;
  162. cursor: pointer;
  163. white-space: nowrap;
  164. }
  165. .opt:hover { background: var(--primary-bg); color: var(--primary-dark); }
  166. .opt.is-active {
  167. background: var(--primary-bg);
  168. color: var(--primary-dark);
  169. font-weight: 600;
  170. }
  171. .opt-group {
  172. padding: 6px 10px 4px;
  173. font-size: 11px;
  174. font-weight: 700;
  175. color: var(--muted);
  176. letter-spacing: 0.02em;
  177. }
  178. .opt-sub {
  179. padding-left: 18px;
  180. color: #475569;
  181. }
  182. /* 中间平铺:可伸缩,禁止横向滚动 */
  183. .nav-slot {
  184. flex: 1 1 auto;
  185. min-width: 0;
  186. height: 100%;
  187. display: flex;
  188. align-items: stretch;
  189. justify-content: center;
  190. overflow: hidden;
  191. z-index: 3;
  192. }
  193. .l2-bar {
  194. display: flex;
  195. align-items: stretch;
  196. justify-content: center;
  197. gap: 2px;
  198. height: 100%;
  199. max-width: 100%;
  200. min-width: 0;
  201. overflow: hidden;
  202. }
  203. .l2-item,
  204. .more-item {
  205. position: relative;
  206. display: inline-flex;
  207. align-items: center;
  208. gap: 4px;
  209. height: 100%;
  210. padding: 0 12px;
  211. border: 0;
  212. background: transparent;
  213. color: rgba(255, 255, 255, 0.92);
  214. font-size: 13px;
  215. font-weight: 500;
  216. cursor: pointer;
  217. white-space: nowrap;
  218. border-bottom: 2px solid transparent;
  219. flex-shrink: 0;
  220. }
  221. .l2-item:hover,
  222. .more-item:hover,
  223. .more-wrap.is-open .more-item {
  224. background: rgba(255, 255, 255, 0.12);
  225. }
  226. .l2-item.is-active,
  227. .more-item.has-active {
  228. background: rgba(255, 255, 255, 0.16);
  229. border-bottom-color: #fff;
  230. font-weight: 600;
  231. }
  232. .mini-caret { font-size: 10px; opacity: 0.8; }
  233. .l2-flyout {
  234. display: none;
  235. position: absolute;
  236. top: calc(100% - 2px);
  237. left: 50%;
  238. transform: translateX(-50%);
  239. min-width: 160px;
  240. padding: 6px;
  241. background: #fff;
  242. border: 1px solid var(--border);
  243. border-radius: 10px;
  244. box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
  245. z-index: 45;
  246. }
  247. .l2-item.is-open .l2-flyout { display: block; }
  248. .more-wrap { position: relative; height: 100%; display: flex; align-items: stretch; }
  249. .more-wrap.is-open .dropdown-panel { display: block; }
  250. .header-end {
  251. margin-left: 4px;
  252. display: flex;
  253. align-items: center;
  254. gap: 8px;
  255. z-index: 2;
  256. flex-shrink: 0;
  257. }
  258. .ghost-btn {
  259. width: 32px;
  260. height: 32px;
  261. border-radius: 8px;
  262. border: 0;
  263. background: rgba(255, 255, 255, 0.12);
  264. color: #fff;
  265. cursor: pointer;
  266. }
  267. .avatar {
  268. width: 32px;
  269. height: 32px;
  270. border-radius: 50%;
  271. background: rgba(255, 255, 255, 0.25);
  272. display: grid;
  273. place-items: center;
  274. font-size: 12px;
  275. font-weight: 700;
  276. }
  277. .content {
  278. padding: 20px 22px 24px;
  279. min-height: 200px;
  280. background: var(--bg);
  281. border-radius: 0 0 12px 12px;
  282. }
  283. .crumb { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
  284. .content h3 { margin: 0 0 8px; font-size: 18px; }
  285. .content p { margin: 0; font-size: 14px; color: #334155; max-width: 62ch; }
  286. .meta {
  287. margin-top: 10px;
  288. font-size: 12px;
  289. color: var(--muted);
  290. }
  291. .meta strong { color: #334155; }
  292. .legend { margin-top: 18px; display: grid; gap: 10px; }
  293. .card {
  294. background: #fff;
  295. border: 1px solid var(--border);
  296. border-radius: 12px;
  297. padding: 14px 16px;
  298. }
  299. .card h2 { margin: 0 0 8px; font-size: 15px; }
  300. .card ul { margin: 0; padding-left: 18px; font-size: 13px; color: #334155; }
  301. .card code {
  302. font-size: 12px;
  303. background: #f8fafc;
  304. border: 1px solid var(--border);
  305. border-radius: 6px;
  306. padding: 1px 6px;
  307. }
  308. </style>
  309. </head>
  310. <body>
  311. <div class="page">
  312. <h1>门户顶栏 Demo:一级下拉 + 中间平铺(无横向滚动)</h1>
  313. <p class="lead">
  314. 中间最多平铺 5 个二级;超过 5 个的全部收入「更多」下拉选择。无横向滚动。
  315. (线上已改为按宽度动态适配,本 Demo 仍可作三级交互示意。)
  316. </p>
  317. <div class="ok">
  318. <strong>溢出策略(Demo):前 5 个平铺,第 6 个起进「更多」。</strong>
  319. 当前激活项若在「更多」内,「更多」按钮会高亮。
  320. </div>
  321. <div class="note">
  322. <strong>三级菜单已覆盖两种入口:</strong><br />
  323. ① 平铺区:二级带 ▾,点击展开三级小下拉(如「客户管理」);无三级的二级可直接点选(如「产品目录」)。<br />
  324. ② 「更多」内:无三级的二级直接可选;有三级的以「二级分组标题 + 三级选项」列出(如「活动管理」下的子页)。
  325. </div>
  326. <div class="controls">
  327. <div id="fitMeta">平铺 — · 更多 —</div>
  328. <div style="color: var(--muted); font-size: 12px;">阈值 <code>MAX_VISIBLE = 5</code>,可按产品再调</div>
  329. </div>
  330. <div class="stage-label">
  331. <h2>可交互原型</h2>
  332. <span>左侧切换一级 · 中间最多 5 个二级 · 超出进「更多」下拉</span>
  333. </div>
  334. <div class="frame">
  335. <header class="portal-header" id="header">
  336. <div class="brand" title="回到工作台">
  337. <div class="logo">峡</div>
  338. <p class="brand-title">山峡化工一体化门户</p>
  339. </div>
  340. <div class="l1-select" id="l1Select">
  341. <button type="button" class="l1-trigger" id="l1Trigger" aria-haspopup="listbox" aria-expanded="false">
  342. <span id="l1Label">CRM</span>
  343. <span class="caret">▾</span>
  344. </button>
  345. <div class="dropdown-panel" id="l1Menu" role="listbox"></div>
  346. </div>
  347. <div class="nav-slot" aria-label="当前模块二级菜单">
  348. <nav class="l2-bar" id="l2Bar"></nav>
  349. </div>
  350. <div class="header-end">
  351. <button type="button" class="ghost-btn" title="全屏" aria-label="全屏">⛶</button>
  352. <div class="avatar" title="用户">管</div>
  353. </div>
  354. </header>
  355. <main class="content">
  356. <div class="crumb" id="crumb">CRM / 客户管理 / 客户列表</div>
  357. <h3 id="pageTitle">客户列表</h3>
  358. <p id="pageDesc">示意内容区。</p>
  359. <p class="meta" id="overflowHint"></p>
  360. </main>
  361. </div>
  362. <div class="legend">
  363. <div class="card">
  364. <h2>三级菜单规则(已确认)</h2>
  365. <ul>
  366. <li><strong>平铺二级有三级</strong>:显示 ▾,点击<strong>只展开</strong>三级小下拉,不跳转页面;选中某个三级后才进入该页</li>
  367. <li><strong>平铺二级无三级</strong>:直接点击进入该页</li>
  368. <li><strong>「更多」内有三级</strong>:二级作分组标题(不可点跳转),下列三级供选择</li>
  369. <li><strong>「更多」内无三级</strong>:二级本身可点选</li>
  370. </ul>
  371. </div>
  372. <div class="card">
  373. <h2>仍待确认</h2>
  374. <ul>
  375. <li>切换一级后是否跳转该模块默认首页?</li>
  376. <li>「更多」阈值是否固定 5,还是可配置?</li>
  377. </ul>
  378. </div>
  379. </div>
  380. </div>
  381. <script>
  382. const MODULES = [
  383. {
  384. id: 'crm',
  385. title: 'CRM',
  386. children: [
  387. // 前 5 个平铺:含「有三级 / 无三级」两种
  388. { title: '客户管理', children: [{ title: '客户列表' }, { title: '客户公海' }] },
  389. { title: '商机管理', children: [{ title: '商机列表' }, { title: '跟进记录' }] },
  390. { title: '合同管理', children: [{ title: '合同列表' }] },
  391. { title: '产品目录' },
  392. { title: '拜访计划', children: [{ title: '计划列表' }, { title: '拜访记录' }] },
  393. // 第 6 个起进「更多」:同样覆盖有/无三级
  394. { title: '销售报表' },
  395. { title: '价格策略', children: [{ title: '价格表' }, { title: '折扣规则' }] },
  396. { title: '渠道管理' },
  397. { title: '线索池', children: [{ title: '线索列表' }, { title: '线索分配' }] },
  398. { title: '活动管理', children: [{ title: '活动列表' }, { title: '报名明细' }] }
  399. ]
  400. },
  401. {
  402. id: 'bx',
  403. title: '报销',
  404. children: [
  405. { title: '我的报销', children: [{ title: '报销申请' }, { title: '报销草稿' }] },
  406. { title: '报销审批' },
  407. { title: '费用统计' },
  408. { title: '科目配置' },
  409. { title: '预算中心' }
  410. ]
  411. },
  412. {
  413. id: 'db',
  414. title: '地磅',
  415. children: [
  416. { title: '过磅记录' },
  417. { title: '车辆档案' },
  418. { title: '称重统计', children: [{ title: '日报' }, { title: '月报' }] },
  419. { title: '黑名单' },
  420. { title: '设备监控' },
  421. { title: '异常复核' }
  422. ]
  423. },
  424. {
  425. id: 'pl',
  426. title: '配送',
  427. children: [
  428. { title: '配送单' },
  429. { title: '线路规划' },
  430. { title: '配送员' },
  431. { title: '运力看板' }
  432. ]
  433. },
  434. {
  435. id: 'workflow',
  436. title: '流程',
  437. children: [
  438. { title: '待办任务' },
  439. { title: '已办任务' },
  440. { title: '流程定义' },
  441. { title: '流程实例' }
  442. ]
  443. },
  444. {
  445. id: 'system',
  446. title: '系统',
  447. children: [
  448. { title: '用户管理' },
  449. { title: '角色权限' },
  450. { title: '菜单管理' },
  451. { title: '租户管理' },
  452. { title: '字典管理' },
  453. { title: '操作日志' },
  454. { title: '参数配置' },
  455. { title: '消息中心' }
  456. ]
  457. }
  458. ]
  459. const MAX_VISIBLE = 5
  460. let activeL1 = MODULES[0].id
  461. let activeL2 = MODULES[0].children[0].title
  462. let activeL3 = MODULES[0].children[0].children?.[0]?.title || null
  463. let openL2Title = null
  464. let moreOpen = false
  465. const fitMeta = document.getElementById('fitMeta')
  466. const l1Select = document.getElementById('l1Select')
  467. const l1Trigger = document.getElementById('l1Trigger')
  468. const l1Menu = document.getElementById('l1Menu')
  469. const l1Label = document.getElementById('l1Label')
  470. const l2Bar = document.getElementById('l2Bar')
  471. const crumb = document.getElementById('crumb')
  472. const pageTitle = document.getElementById('pageTitle')
  473. const pageDesc = document.getElementById('pageDesc')
  474. const overflowHint = document.getElementById('overflowHint')
  475. function currentModule() {
  476. return MODULES.find((m) => m.id === activeL1) || MODULES[0]
  477. }
  478. function hasKids(item) {
  479. return Array.isArray(item.children) && item.children.length > 0
  480. }
  481. function closePopovers() {
  482. l1Select.classList.remove('is-open')
  483. l1Trigger.setAttribute('aria-expanded', 'false')
  484. openL2Title = null
  485. moreOpen = false
  486. }
  487. function renderL1() {
  488. const mod = currentModule()
  489. l1Label.textContent = mod.title
  490. l1Menu.innerHTML = MODULES.map((m) => {
  491. const active = m.id === activeL1 ? ' is-active' : ''
  492. return `<button type="button" class="opt${active}" data-id="${m.id}" role="option">${m.title}</button>`
  493. }).join('')
  494. }
  495. function itemButtonHtml(item, { open = false } = {}) {
  496. const kids = hasKids(item)
  497. const isActive = item.title === activeL2
  498. const flyout = kids
  499. ? `<div class="l2-flyout">${item.children
  500. .map((c) => {
  501. const a = c.title === activeL3 && isActive ? ' is-active' : ''
  502. return `<button type="button" class="opt${a}" data-l2="${item.title}" data-l3="${c.title}">${c.title}</button>`
  503. })
  504. .join('')}</div>`
  505. : ''
  506. return `
  507. <button
  508. type="button"
  509. class="l2-item${isActive ? ' is-active' : ''}${open ? ' is-open' : ''}"
  510. data-l2="${item.title}"
  511. data-has-kids="${kids ? '1' : '0'}"
  512. >
  513. <span>${item.title}</span>
  514. ${kids ? `<span class="mini-caret">${open ? '▴' : '▾'}</span>` : ''}
  515. ${flyout}
  516. </button>
  517. `
  518. }
  519. function morePanelHtml(hiddenItems) {
  520. return hiddenItems
  521. .map((item) => {
  522. if (!hasKids(item)) {
  523. const a = item.title === activeL2 ? ' is-active' : ''
  524. return `<button type="button" class="opt${a}" data-l2="${item.title}" data-has-kids="0">${item.title}</button>`
  525. }
  526. const groupActive = item.title === activeL2
  527. const kids = item.children
  528. .map((c) => {
  529. const a = groupActive && c.title === activeL3 ? ' is-active' : ''
  530. return `<button type="button" class="opt opt-sub${a}" data-l2="${item.title}" data-l3="${c.title}">${c.title}</button>`
  531. })
  532. .join('')
  533. return `<div class="opt-group">${item.title}</div>${kids}`
  534. })
  535. .join('')
  536. }
  537. function renderL2() {
  538. const mod = currentModule()
  539. const items = mod.children
  540. const visible = items.slice(0, MAX_VISIBLE)
  541. const hidden = items.slice(MAX_VISIBLE)
  542. const hiddenHasActive = hidden.some((item) => item.title === activeL2)
  543. let html = visible
  544. .map((item) => itemButtonHtml(item, { open: openL2Title === item.title }))
  545. .join('')
  546. if (hidden.length) {
  547. html += `
  548. <div class="more-wrap${moreOpen ? ' is-open' : ''}" id="moreWrap">
  549. <button type="button" class="more-item${hiddenHasActive ? ' has-active' : ''}" id="moreBtn">
  550. <span>更多</span>
  551. <span class="mini-caret">${moreOpen ? '▴' : '▾'}</span>
  552. </button>
  553. <div class="dropdown-panel align-right" id="moreMenu">${morePanelHtml(hidden)}</div>
  554. </div>
  555. `
  556. }
  557. l2Bar.innerHTML = html
  558. fitMeta.textContent = `平铺 ${visible.length} · 更多 ${hidden.length}`
  559. overflowHint.textContent = hidden.length
  560. ? `二级超过 ${MAX_VISIBLE} 个:前 ${MAX_VISIBLE} 个平铺,另 ${hidden.length} 个在「更多」中下拉选择。`
  561. : `二级不超过 ${MAX_VISIBLE} 个,全部平铺,不显示「更多」。`
  562. }
  563. function syncContent() {
  564. const mod = currentModule()
  565. const leaf = activeL3 || activeL2
  566. crumb.textContent = [mod.title, activeL2, activeL3].filter(Boolean).join(' / ')
  567. pageTitle.textContent = leaf
  568. pageDesc.textContent =
  569. `当前一级「${mod.title}」,二级共 ${mod.children.length} 项。` +
  570. (activeL3 ? ` 已进入「${activeL3}」。` : ' 当前为二级直达页。')
  571. }
  572. function selectL1(id) {
  573. activeL1 = id
  574. const mod = currentModule()
  575. const first = mod.children[0]
  576. activeL2 = first.title
  577. activeL3 = first.children?.[0]?.title || null
  578. closePopovers()
  579. renderL1()
  580. renderL2()
  581. syncContent()
  582. }
  583. l1Trigger.addEventListener('click', (e) => {
  584. e.stopPropagation()
  585. const willOpen = !l1Select.classList.contains('is-open')
  586. openL2Title = null
  587. moreOpen = false
  588. renderL2()
  589. l1Select.classList.toggle('is-open', willOpen)
  590. l1Trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false')
  591. })
  592. l1Menu.addEventListener('click', (e) => {
  593. const btn = e.target.closest('.opt')
  594. if (!btn) return
  595. selectL1(btn.dataset.id)
  596. })
  597. l2Bar.addEventListener('click', (e) => {
  598. e.stopPropagation()
  599. const moreBtn = e.target.closest('#moreBtn')
  600. if (moreBtn) {
  601. moreOpen = !moreOpen
  602. openL2Title = null
  603. l1Select.classList.remove('is-open')
  604. renderL2()
  605. return
  606. }
  607. const fromMore = e.target.closest('#moreMenu .opt')
  608. if (fromMore) {
  609. activeL2 = fromMore.dataset.l2
  610. activeL3 = fromMore.dataset.l3 || null
  611. moreOpen = false
  612. openL2Title = null
  613. renderL2()
  614. syncContent()
  615. return
  616. }
  617. const l3 = e.target.closest('.l2-flyout .opt')
  618. if (l3) {
  619. activeL2 = l3.dataset.l2
  620. activeL3 = l3.dataset.l3
  621. openL2Title = null
  622. moreOpen = false
  623. renderL2()
  624. syncContent()
  625. return
  626. }
  627. const l2 = e.target.closest('.l2-item')
  628. if (!l2) return
  629. const title = l2.dataset.l2
  630. const kids = l2.dataset.hasKids === '1'
  631. const item = currentModule().children.find((c) => c.title === title)
  632. if (!item) return
  633. if (kids) {
  634. openL2Title = openL2Title === title ? null : title
  635. moreOpen = false
  636. l1Select.classList.remove('is-open')
  637. renderL2()
  638. return
  639. }
  640. activeL2 = title
  641. activeL3 = null
  642. openL2Title = null
  643. moreOpen = false
  644. renderL2()
  645. syncContent()
  646. })
  647. document.addEventListener('click', () => {
  648. closePopovers()
  649. renderL2()
  650. })
  651. document.addEventListener('keydown', (e) => {
  652. if (e.key === 'Escape') {
  653. closePopovers()
  654. renderL2()
  655. }
  656. })
  657. renderL1()
  658. renderL2()
  659. syncContent()
  660. </script>
  661. </body>
  662. </html>