@mooncellwiki/prts-design-css 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/prts-design.css +6762 -0
- package/dist/prts-design.min.css +2 -0
- package/package.json +55 -0
- package/src/arknights/archive.css +21 -0
- package/src/arknights/attrs.css +11 -0
- package/src/arknights/camp.css +8 -0
- package/src/arknights/dialogue.css +7 -0
- package/src/arknights/dossier.css +13 -0
- package/src/arknights/elite.css +5 -0
- package/src/arknights/enemy.css +12 -0
- package/src/arknights/event.css +14 -0
- package/src/arknights/hero.css +11 -0
- package/src/arknights/index.css +41 -0
- package/src/arknights/item.css +32 -0
- package/src/arknights/kv.css +15 -0
- package/src/arknights/level.css +9 -0
- package/src/arknights/materials.css +6 -0
- package/src/arknights/module.css +49 -0
- package/src/arknights/news.css +6 -0
- package/src/arknights/op-card.css +26 -0
- package/src/arknights/op-row.css +6 -0
- package/src/arknights/phase-tabs.css +8 -0
- package/src/arknights/pot-list.css +7 -0
- package/src/arknights/potential.css +4 -0
- package/src/arknights/profession.css +16 -0
- package/src/arknights/range.css +10 -0
- package/src/arknights/rarity.css +21 -0
- package/src/arknights/rich-text.css +18 -0
- package/src/arknights/skill-levels.css +13 -0
- package/src/arknights/skill-matrix.css +25 -0
- package/src/arknights/skill-sheet.css +30 -0
- package/src/arknights/skill.css +28 -0
- package/src/arknights/sp.css +15 -0
- package/src/arknights/stage.css +22 -0
- package/src/arknights/table-numerals.css +7 -0
- package/src/arknights/talent-table.css +30 -0
- package/src/arknights/talent.css +7 -0
- package/src/arknights/trust.css +4 -0
- package/src/arknights/voice.css +21 -0
- package/src/base/catlinks.css +64 -0
- package/src/base/collapsible.css +10 -0
- package/src/base/forms.css +121 -0
- package/src/base/index.css +20 -0
- package/src/base/media.css +26 -0
- package/src/base/notices.css +17 -0
- package/src/base/print.css +11 -0
- package/src/base/references.css +9 -0
- package/src/base/root.css +16 -0
- package/src/base/special-pages.css +43 -0
- package/src/base/tabber.css +31 -0
- package/src/base/tables.css +45 -0
- package/src/base/toc.css +19 -0
- package/src/base/typography.css +133 -0
- package/src/bridge-codex.css +98 -0
- package/src/chrome/body.css +13 -0
- package/src/chrome/footer.css +34 -0
- package/src/chrome/header.css +104 -0
- package/src/chrome/index.css +29 -0
- package/src/chrome/keyart.css +15 -0
- package/src/chrome/layout.css +15 -0
- package/src/chrome/local-nav.css +17 -0
- package/src/chrome/page-header.css +68 -0
- package/src/chrome/responsive.css +127 -0
- package/src/chrome/search-palette.css +121 -0
- package/src/chrome/shell.css +4 -0
- package/src/chrome/sidebar-tree.css +36 -0
- package/src/chrome/sidebar.css +39 -0
- package/src/chrome/special-pages.css +4 -0
- package/src/chrome/theme-toggle.css +10 -0
- package/src/chrome/toc.css +38 -0
- package/src/components/accordion.css +10 -0
- package/src/components/avatar.css +8 -0
- package/src/components/badge.css +16 -0
- package/src/components/breadcrumb.css +7 -0
- package/src/components/button.css +45 -0
- package/src/components/card.css +28 -0
- package/src/components/cbox.css +18 -0
- package/src/components/chip.css +9 -0
- package/src/components/dialog.css +14 -0
- package/src/components/divider.css +8 -0
- package/src/components/dropdown.css +30 -0
- package/src/components/empty.css +5 -0
- package/src/components/fab.css +9 -0
- package/src/components/form.css +66 -0
- package/src/components/heading.css +11 -0
- package/src/components/index.css +38 -0
- package/src/components/keyframes.css +14 -0
- package/src/components/message.css +21 -0
- package/src/components/pagination.css +11 -0
- package/src/components/panel.css +15 -0
- package/src/components/progress.css +19 -0
- package/src/components/skeleton.css +5 -0
- package/src/components/spinner.css +23 -0
- package/src/components/stat.css +12 -0
- package/src/components/stepper.css +14 -0
- package/src/components/table.css +17 -0
- package/src/components/tabs.css +29 -0
- package/src/components/tag.css +26 -0
- package/src/components/timeline.css +13 -0
- package/src/components/title-reset.css +8 -0
- package/src/components/toast.css +14 -0
- package/src/components/tooltip.css +31 -0
- package/src/decor/bilingual.css +6 -0
- package/src/decor/brackets.css +6 -0
- package/src/decor/chevrons.css +5 -0
- package/src/decor/corner.css +12 -0
- package/src/decor/glyph.css +7 -0
- package/src/decor/halftone.css +13 -0
- package/src/decor/index.css +18 -0
- package/src/decor/inverse.css +4 -0
- package/src/decor/patterns.css +7 -0
- package/src/decor/stripes.css +10 -0
- package/src/decor/type.css +10 -0
- package/src/forced-colors.css +54 -0
- package/src/index.css +18 -0
- package/src/scope.css +120 -0
- package/src/search-palette.js +549 -0
- package/src/sidebar-tree.js +220 -0
- package/src/standalone.css +12 -0
- package/src/tokens.css +665 -0
- package/src/utilities.css +43 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/*! ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
* PRTS Design · search-palette.js — 悬浮搜索面板(Command Palette)
|
|
3
|
+
* 皮肤(skins.akds.js 通过 require 引入)与 preview 共用;无依赖,不带任何数据源 ——
|
|
4
|
+
* 数据由调用方以 providers 注入(MW:skin/resources/search-providers.js;预览:preview/search-mock.js)。
|
|
5
|
+
*
|
|
6
|
+
* 交互模型(参考 Citizen 的 Command Palette,视觉换成 PRTS Design):
|
|
7
|
+
* · 页眉里的 form.ak-header__search 被换成 button.ak-search-trigger(长得像输入框);真表单被搬进面板顶部,
|
|
8
|
+
* #searchInput / action / hidden title 都还在 → Gadget 与无 JS 提交不受影响。
|
|
9
|
+
* · 开:点触发器 / .ak-header__search-toggle / 键 "/"、Ctrl(⌘)K、accesskey F。
|
|
10
|
+
* 关:Esc(有字先清空,再按退出模式,再按关闭)/ 点遮罩 / 关闭按钮 / 选中结果。
|
|
11
|
+
* · 空查询:最近访问(localStorage)+ 提示 + 快捷入口;有字:providers.search() 分组结果 + 末尾固定「全文搜索」行;
|
|
12
|
+
* "/" 开头:命令列表;命中触发符(>、#、@、~)或选中命令:进入模式(斜切 chip),退格空输入 / ← 退出。
|
|
13
|
+
* · ↑↓ 循环高亮,回车打开(⌘/Ctrl 回车新标签),⇧回车全文搜索;结果还没到就回车 → urls.go(q)(MW 原生 Go:精确跳转,无则全文)。
|
|
14
|
+
* · 输入框始终持有焦点(列表 mousedown 阻止夺焦),aria-activedescendant 播报高亮项;Tab 在面板内循环。
|
|
15
|
+
*
|
|
16
|
+
* API:window.akdsSearchPalette.init( options ) → { open, close, toggle, refresh, isOpen }
|
|
17
|
+
* options = {
|
|
18
|
+
* trigger: 'form.ak-header__search', // 会被换成按钮并搬进面板;缺省时面板自建输入框
|
|
19
|
+
* toggles: '.ak-header__search-toggle', // 其它打开按钮(手机图标等)
|
|
20
|
+
* placeholder, messages: {…}, // 文案(默认中文;MW 侧用 mw.msg 覆盖)
|
|
21
|
+
* urls: { go(q), fulltext(q) }, // 回车兜底 / 全文搜索
|
|
22
|
+
* providers: {
|
|
23
|
+
* search(q, signal, ctx) → Promise<Group[]> | Group[] // Group = { id, label, en?, items: Item[] }
|
|
24
|
+
* empty(ctx) → Promise<Group[]> | Group[] // 空查询时最近访问之后的分组(可选)
|
|
25
|
+
* shortcuts() → [{ label, url }] // 空态快捷入口(可选)
|
|
26
|
+
* },
|
|
27
|
+
* modes: [ { id, trigger: '/action', alias: '>', label, desc, placeholder, fulltext:false,
|
|
28
|
+
* search(q, signal, ctx) → Group[] } ],
|
|
29
|
+
* recent: { key: 'akds-recent', max: 8 } | false,
|
|
30
|
+
* onSelect(item, event) → false 阻止默认导航
|
|
31
|
+
* }
|
|
32
|
+
* Item = { id?, type: 'page'|'operator'|'item'|'category'|'action'|…, label, url?, desc?, thumb?(url), icon?(svg id: 'page'|'search'|'category'|'user'|'file'|'action'|'clock'),
|
|
33
|
+
* glyph?(单字,深底), meta?: [{ text?, html?, kbd? }], match?: boolean(默认 true:高亮已输入部分), en?, keepOpen?, noRecent?, onSelect?() ,
|
|
34
|
+
* actions?: [{ id, label, icon: 'edit'|'close'|'external', url?, onClick?(item) }] } ← 行内动作始终占位、高亮时可见;设计上只给最近访问用「移除」,搜索结果不放额外点击动作
|
|
35
|
+
* ═══════════════════════════════════════════════════════════════════════════ */
|
|
36
|
+
( function () {
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const ICONS = {
|
|
40
|
+
search: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M10.5 3a7.5 7.5 0 1 1 0 15 7.5 7.5 0 0 1 0-15zM16 16l5 5"/></svg>',
|
|
41
|
+
fulltext: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M4 4h16v16H4zM8 9h8M8 13h5"/><path fill="none" stroke="currentColor" stroke-width="2" d="M14 14.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zm1.8 4.3L18 21"/></svg>',
|
|
42
|
+
page: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M6 3h8l5 5v13H6zM14 3v5h5M9 13h6M9 17h6"/></svg>',
|
|
43
|
+
category: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z"/></svg>',
|
|
44
|
+
user: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M12 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM4 21c0-4 3.6-7 8-7s8 3 8 7"/></svg>',
|
|
45
|
+
file: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4"/><circle cx="16" cy="9" r="1.5" fill="currentColor"/></svg>',
|
|
46
|
+
action: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M13 3 5 14h6l-1 7 8-11h-6z"/></svg>',
|
|
47
|
+
clock: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="2"/><path fill="none" stroke="currentColor" stroke-width="2" d="M12 7v5l3 2"/></svg>',
|
|
48
|
+
redirect: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M4 6h9a5 5 0 0 1 0 10H8m3-3-3 3 3 3"/></svg>',
|
|
49
|
+
back: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M15 5l-7 7 7 7"/></svg>',
|
|
50
|
+
close: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M6 6l12 12M18 6 6 18"/></svg>',
|
|
51
|
+
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M4 20h4l11-11-4-4L4 16zM13 7l4 4"/></svg>',
|
|
52
|
+
external: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M14 4h6v6M20 4l-9 9M18 14v6H4V6h6"/></svg>',
|
|
53
|
+
command: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M9 5 5 12l4 7M15 5l4 7-4 7"/></svg>'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const MSG = {
|
|
57
|
+
label: '搜索',
|
|
58
|
+
placeholder: '搜索 PRTS…',
|
|
59
|
+
recent: '最近访问', recentEn: 'Recent',
|
|
60
|
+
modes: '命令', modesEn: 'Commands',
|
|
61
|
+
fulltext: '全文搜索 “$1”',
|
|
62
|
+
fulltextDesc: '在全站正文中查找',
|
|
63
|
+
go: '前往 “$1”',
|
|
64
|
+
emptyTitle: '搜索 PRTS',
|
|
65
|
+
emptyDesc: '干员、敌人、道具、关卡或任意页面名;输入 $1 查看命令',
|
|
66
|
+
shortcuts: '快捷入口', shortcutsEn: 'Shortcuts',
|
|
67
|
+
noResults: '没有标题匹配 “$1”',
|
|
68
|
+
noResultsDesc: '检查拼写,或试试全文搜索',
|
|
69
|
+
error: '搜索服务暂时不可用,回车执行全文搜索',
|
|
70
|
+
results: '$1 条结果',
|
|
71
|
+
hintNavigate: '选择', hintOpen: '打开', hintFulltext: '全文', hintClose: '关闭', hintClear: '清空', hintBack: '返回', hintCommands: '命令',
|
|
72
|
+
close: '取消', clear: '清空', back: '返回', remove: '从最近访问中移除', edit: '编辑', newTab: '在新标签页打开',
|
|
73
|
+
brand: 'PRTS · Search'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function h( tag, cls, attrs ) {
|
|
77
|
+
const el = document.createElement( tag );
|
|
78
|
+
if ( cls ) { el.className = cls; }
|
|
79
|
+
if ( attrs ) { Object.keys( attrs ).forEach( ( k ) => { if ( attrs[ k ] != null ) { el.setAttribute( k, attrs[ k ] ); } } ); }
|
|
80
|
+
return el;
|
|
81
|
+
}
|
|
82
|
+
function svg( name, cls ) {
|
|
83
|
+
const wrap = h( 'span' ); wrap.innerHTML = ICONS[ name ] || ICONS.page;
|
|
84
|
+
const el = wrap.firstChild; if ( cls ) { el.setAttribute( 'class', cls ); }
|
|
85
|
+
return el;
|
|
86
|
+
}
|
|
87
|
+
function fmt( s, a ) { return String( s ).replace( /\$1/g, a == null ? '' : a ); }
|
|
88
|
+
function isMac() { return /Mac|iPhone|iPad/.test( navigator.platform || '' ); }
|
|
89
|
+
function isFormField( el ) {
|
|
90
|
+
if ( !el || el.nodeType !== 1 ) { return false; }
|
|
91
|
+
const n = el.nodeName.toLowerCase(), t = ( el.getAttribute( 'type' ) || '' ).toLowerCase();
|
|
92
|
+
return n === 'select' || n === 'textarea' || ( n === 'input' && ![ 'submit', 'reset', 'checkbox', 'radio', 'button' ].includes( t ) ) || el.isContentEditable;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* 标题高亮:已输入部分 <mark>(常规字重),其余加粗 —— Codex SearchResultTitle 的约定 */
|
|
96
|
+
function titleNode( label, query, match ) {
|
|
97
|
+
const bdi = h( 'bdi' );
|
|
98
|
+
const q = ( query || '' ).trim();
|
|
99
|
+
const i = ( match !== false && q ) ? label.toLowerCase().indexOf( q.toLowerCase() ) : -1;
|
|
100
|
+
if ( i < 0 ) { bdi.textContent = label; return bdi; }
|
|
101
|
+
bdi.append( label.slice( 0, i ) );
|
|
102
|
+
const m = h( 'mark' ); m.textContent = label.slice( i, i + q.length ); bdi.append( m );
|
|
103
|
+
bdi.append( label.slice( i + q.length ) );
|
|
104
|
+
return bdi;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function init( options ) {
|
|
108
|
+
const opt = options || {};
|
|
109
|
+
const M = Object.assign( {}, MSG, opt.messages || {} );
|
|
110
|
+
const providers = opt.providers || {};
|
|
111
|
+
const modes = ( opt.modes || [] ).slice();
|
|
112
|
+
const urls = Object.assign( {
|
|
113
|
+
go: ( q ) => 'Special:Search?search=' + encodeURIComponent( q ) + '&go=Go',
|
|
114
|
+
fulltext: ( q ) => 'Special:Search?search=' + encodeURIComponent( q ) + '&fulltext=1'
|
|
115
|
+
}, opt.urls || {} );
|
|
116
|
+
const recentCfg = opt.recent === false ? null : Object.assign( { key: 'akds-recent', max: 8 }, opt.recent || {} );
|
|
117
|
+
const placeholder = opt.placeholder || M.placeholder;
|
|
118
|
+
|
|
119
|
+
/* ── 触发器:把页眉表单换成按钮,表单本身留给面板 ── */
|
|
120
|
+
let form = null, input = null, triggerBtn = null;
|
|
121
|
+
const oldForm = opt.trigger ? document.querySelector( opt.trigger ) : null;
|
|
122
|
+
if ( oldForm ) {
|
|
123
|
+
form = oldForm;
|
|
124
|
+
input = form.querySelector( 'input[type="search"], input[type="text"], input:not([type])' );
|
|
125
|
+
triggerBtn = h( 'button', 'ak-search-trigger ' + ( form.classList.contains( 'ak-header__search' ) ? 'ak-header__search' : '' ), { type: 'button', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', 'aria-keyshortcuts': '/ ' + ( isMac() ? 'Meta+K' : 'Control+K' ), 'aria-label': M.label } );
|
|
126
|
+
triggerBtn.append( svg( 'search', 'ak-search-trigger__icon' ) );
|
|
127
|
+
const lab = h( 'span', 'ak-search-trigger__label' ); lab.textContent = ( input && input.placeholder ) || placeholder; triggerBtn.append( lab );
|
|
128
|
+
const kbd = h( 'kbd', 'ak-kbd' ); kbd.textContent = '/'; triggerBtn.append( kbd );
|
|
129
|
+
form.parentNode.replaceChild( triggerBtn, form );
|
|
130
|
+
form.classList.remove( 'ak-header__search', 'ak-search', 'is-open' );
|
|
131
|
+
}
|
|
132
|
+
if ( !form ) {
|
|
133
|
+
form = h( 'form', '', { role: 'search', action: typeof urls.form === 'string' ? urls.form : '' } );
|
|
134
|
+
input = h( 'input', '', { type: 'search', name: 'search', id: 'searchInput' } );
|
|
135
|
+
form.append( input );
|
|
136
|
+
}
|
|
137
|
+
if ( !input ) { input = h( 'input', '', { type: 'search', name: 'search' } ); form.append( input ); }
|
|
138
|
+
form.classList.add( 'ak-palette__form' );
|
|
139
|
+
form.setAttribute( 'autocomplete', 'off' );
|
|
140
|
+
input.setAttribute( 'autocomplete', 'off' ); input.setAttribute( 'autocapitalize', 'off' ); input.setAttribute( 'spellcheck', 'false' );
|
|
141
|
+
input.setAttribute( 'role', 'combobox' ); input.setAttribute( 'aria-autocomplete', 'list' ); input.setAttribute( 'aria-expanded', 'false' ); input.setAttribute( 'aria-controls', 'ak-palette-listbox' );
|
|
142
|
+
input.setAttribute( 'aria-label', M.label );
|
|
143
|
+
input.placeholder = placeholder;
|
|
144
|
+
input.removeAttribute( 'accesskey' ); // MW 给的 accesskey F 由我们接管(否则聚焦到看不见的输入框)
|
|
145
|
+
|
|
146
|
+
/* ── 面板 DOM ── */
|
|
147
|
+
const backdrop = h( 'div', 'ak-palette-backdrop', { hidden: '' } );
|
|
148
|
+
const root = h( 'div', 'ak-palette', { role: 'dialog', 'aria-modal': 'true', 'aria-label': M.label, hidden: '' } );
|
|
149
|
+
const head = h( 'div', 'ak-palette__head' );
|
|
150
|
+
const backBtn = h( 'button', 'ak-palette__back', { type: 'button', 'aria-label': M.back, title: M.back } ); backBtn.append( svg( 'back' ) );
|
|
151
|
+
const icon = svg( 'search', 'ak-palette__icon' );
|
|
152
|
+
const chip = h( 'span', 'ak-palette__chip' );
|
|
153
|
+
const clearBtn = h( 'button', 'ak-palette__clear', { type: 'button', 'aria-label': M.clear, title: M.clear } ); clearBtn.append( svg( 'close' ) );
|
|
154
|
+
const closeBtn = h( 'button', 'ak-palette__close', { type: 'button', 'aria-label': M.close } );
|
|
155
|
+
const closeKbd = h( 'kbd', 'ak-kbd' ); closeKbd.textContent = 'Esc'; const closeTxt = h( 'span' ); closeTxt.textContent = M.close; closeBtn.append( closeKbd, closeTxt );
|
|
156
|
+
const loading = h( 'div', 'ak-palette__loading ak-progress ak-progress--indeterminate', { 'aria-hidden': 'true' } );
|
|
157
|
+
head.append( backBtn, icon, chip, form, clearBtn, closeBtn, loading );
|
|
158
|
+
const body = h( 'div', 'ak-palette__body' );
|
|
159
|
+
const viewport = h( 'div', 'ak-palette__viewport' );
|
|
160
|
+
const list = h( 'div', 'ak-palette__list', { role: 'listbox', id: 'ak-palette-listbox', 'aria-label': M.label } );
|
|
161
|
+
const empty = h( 'div', 'ak-palette__empty', { hidden: '' } );
|
|
162
|
+
viewport.append( list, empty ); body.append( viewport );
|
|
163
|
+
const foot = h( 'div', 'ak-palette__foot' );
|
|
164
|
+
const footLeft = h( 'div', 'ak-palette__foot-left' );
|
|
165
|
+
const brand = h( 'span', 'ak-en' ); brand.textContent = M.brand;
|
|
166
|
+
const cmdBtn = h( 'button', 'ak-palette__foot-btn', { type: 'button' } ); const cmdKbd = h( 'kbd', 'ak-kbd' ); cmdKbd.textContent = '/'; cmdBtn.append( cmdKbd, document.createTextNode( M.hintCommands ) );
|
|
167
|
+
footLeft.append( brand ); if ( modes.length ) { footLeft.append( cmdBtn ); }
|
|
168
|
+
const hints = h( 'div', 'ak-palette__hints', { 'aria-hidden': 'true' } );
|
|
169
|
+
foot.append( footLeft, hints );
|
|
170
|
+
const live = h( 'div', 'ak-palette__live', { 'aria-live': 'polite', 'aria-atomic': 'true' } );
|
|
171
|
+
root.append( head, body, foot, live );
|
|
172
|
+
document.body.append( backdrop, root );
|
|
173
|
+
|
|
174
|
+
/* ── 状态 ── */
|
|
175
|
+
let isOpen = false, closing = false, closeTimer = 0;
|
|
176
|
+
let query = '', mode = null;
|
|
177
|
+
let items = []; // 扁平项(与 DOM 顺序一致)
|
|
178
|
+
let active = -1;
|
|
179
|
+
let seq = 0, ctrl = null, debounceTimer = 0, pendingTimer = 0;
|
|
180
|
+
let lastGroups = null; // keepStale:新结果没到之前保留旧列表
|
|
181
|
+
let lastResizeH = 0;
|
|
182
|
+
const ro = window.ResizeObserver ? new ResizeObserver( () => syncHeight() ) : null;
|
|
183
|
+
|
|
184
|
+
function syncHeight() {
|
|
185
|
+
if ( !isOpen ) { return; }
|
|
186
|
+
const hgt = viewport.offsetHeight;
|
|
187
|
+
if ( hgt === lastResizeH ) { return; }
|
|
188
|
+
lastResizeH = hgt;
|
|
189
|
+
body.style.height = hgt + 'px';
|
|
190
|
+
}
|
|
191
|
+
function setLoading( on ) {
|
|
192
|
+
clearTimeout( pendingTimer );
|
|
193
|
+
if ( on ) { pendingTimer = setTimeout( () => root.classList.add( 'is-loading' ), 160 ); } // 快返回不闪进度条
|
|
194
|
+
else { root.classList.remove( 'is-loading' ); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ── 最近访问 ── */
|
|
198
|
+
function readRecent() { if ( !recentCfg ) { return []; } try { return JSON.parse( localStorage.getItem( recentCfg.key ) ) || []; } catch ( e ) { return []; } }
|
|
199
|
+
function writeRecent( arr ) { if ( !recentCfg ) { return; } try { localStorage.setItem( recentCfg.key, JSON.stringify( arr.slice( 0, recentCfg.max ) ) ); } catch ( e ) { /* private mode */ } }
|
|
200
|
+
function pushRecent( item ) {
|
|
201
|
+
if ( !recentCfg || !item.url || item.noRecent || item.type === 'search' || item.type === 'mode' ) { return; }
|
|
202
|
+
const rec = { type: item.type || 'page', label: item.label, url: item.url, desc: item.desc || '', thumb: item.thumb || '', icon: item.icon || '', en: item.en || '' };
|
|
203
|
+
writeRecent( [ rec ].concat( readRecent().filter( ( r ) => r.url !== rec.url ) ) );
|
|
204
|
+
}
|
|
205
|
+
function removeRecent( url ) { writeRecent( readRecent().filter( ( r ) => r.url !== url ) ); }
|
|
206
|
+
|
|
207
|
+
/* ── 渲染 ── */
|
|
208
|
+
function clearList() { list.textContent = ''; items = []; active = -1; }
|
|
209
|
+
function renderGroups( groups, q, opts ) {
|
|
210
|
+
opts = opts || {};
|
|
211
|
+
clearList();
|
|
212
|
+
empty.hidden = true;
|
|
213
|
+
( groups || [] ).forEach( ( g ) => {
|
|
214
|
+
if ( !g || !g.items || !g.items.length ) { return; }
|
|
215
|
+
const gid = 'ak-palette-g-' + ( g.id || Math.random().toString( 36 ).slice( 2 ) );
|
|
216
|
+
const grp = h( 'div', 'ak-palette__group' + ( g.tail ? ' ak-palette__group--tail' : '' ), { role: 'group', 'aria-labelledby': g.label ? gid : null } );
|
|
217
|
+
if ( g.label ) {
|
|
218
|
+
const lab = h( 'div', 'ak-palette__label', { id: gid } ); lab.textContent = g.label;
|
|
219
|
+
if ( g.en ) { const en = h( 'span', 'ak-en' ); en.textContent = g.en; lab.append( en ); }
|
|
220
|
+
if ( g.hint ) { const sm = h( 'small' ); sm.textContent = g.hint; lab.append( sm ); }
|
|
221
|
+
grp.append( lab );
|
|
222
|
+
}
|
|
223
|
+
g.items.forEach( ( it ) => grp.append( renderItem( it, q ) ) );
|
|
224
|
+
list.append( grp );
|
|
225
|
+
} );
|
|
226
|
+
if ( opts.notice ) { const n = h( 'div', 'ak-palette__notice' + ( opts.error ? ' ak-palette__notice--error' : '' ) ); n.textContent = opts.notice; list.append( n ); }
|
|
227
|
+
input.setAttribute( 'aria-expanded', items.length ? 'true' : 'false' );
|
|
228
|
+
setActive( opts.highlight != null ? opts.highlight : ( q ? 0 : -1 ), false );
|
|
229
|
+
live.textContent = items.length ? fmt( M.results, items.length ) : '';
|
|
230
|
+
syncHeight();
|
|
231
|
+
}
|
|
232
|
+
function renderItem( it, q ) {
|
|
233
|
+
const idx = items.length; items.push( it );
|
|
234
|
+
const row = h( 'div', 'ak-palette__item', { role: 'option', id: 'ak-palette-item-' + idx, 'aria-selected': 'false', 'data-index': idx } );
|
|
235
|
+
const link = it.url ? h( 'a', 'ak-palette__link', { href: it.url } ) : h( 'button', 'ak-palette__link', { type: 'button' } );
|
|
236
|
+
link.tabIndex = -1;
|
|
237
|
+
// 缩略图 / 图标 / 单字
|
|
238
|
+
const th = h( 'span', 'ak-palette__thumb' + ( it.thumb ? '' : ( it.glyph ? ' ak-palette__thumb--dark ak-palette__thumb--mono' : ( it.dark ? ' ak-palette__thumb--dark' : ' ak-palette__thumb--icon' ) ) ) );
|
|
239
|
+
if ( it.thumb ) { const img = h( 'img', '', { src: it.thumb, alt: '', loading: 'lazy' } ); th.append( img ); }
|
|
240
|
+
else if ( it.glyph ) { th.textContent = it.glyph; }
|
|
241
|
+
else { th.append( svg( it.icon || ( it.type === 'category' ? 'category' : it.type === 'user' ? 'user' : it.type === 'file' ? 'file' : it.type === 'action' ? 'action' : it.type === 'search' ? 'fulltext' : it.type === 'recent' ? 'clock' : it.type === 'mode' ? 'command' : 'page' ) ) ); }
|
|
242
|
+
// 文字
|
|
243
|
+
const text = h( 'span', 'ak-palette__text' );
|
|
244
|
+
const title = h( 'span', 'ak-palette__title' ); title.append( titleNode( it.label, q, it.match ) );
|
|
245
|
+
if ( it.en ) { const en = h( 'span', 'ak-en' ); en.textContent = it.en; title.append( en ); }
|
|
246
|
+
text.append( title );
|
|
247
|
+
if ( it.desc || it.redirect ) {
|
|
248
|
+
const d = h( 'span', 'ak-palette__desc' );
|
|
249
|
+
if ( it.redirect ) { d.append( svg( 'redirect', 'ak-icon' ) ); d.append( it.redirect ); if ( it.desc ) { d.append( ' · ' ); } }
|
|
250
|
+
if ( it.desc ) { d.append( it.desc ); }
|
|
251
|
+
text.append( d );
|
|
252
|
+
}
|
|
253
|
+
link.append( th, text );
|
|
254
|
+
// 右侧元数据
|
|
255
|
+
if ( it.meta && it.meta.length ) {
|
|
256
|
+
const meta = h( 'span', 'ak-palette__meta' );
|
|
257
|
+
it.meta.forEach( ( m ) => {
|
|
258
|
+
if ( m.kbd ) { const k = h( 'kbd', 'ak-kbd' ); k.textContent = m.kbd; meta.append( k ); }
|
|
259
|
+
else if ( m.html ) { const s = h( 'span' ); s.innerHTML = m.html; meta.append( s ); } // 受信任来源(皮肤/预览自己拼的稀有度、职业图标)
|
|
260
|
+
else if ( m.text ) { const s = h( 'span', m.cls || '' ); s.textContent = m.text; meta.append( s ); }
|
|
261
|
+
} );
|
|
262
|
+
link.append( meta );
|
|
263
|
+
}
|
|
264
|
+
row.append( link );
|
|
265
|
+
// 行内动作(高亮 / 聚焦时可见;Tab 可达)
|
|
266
|
+
const acts = ( it.actions || [] ).slice();
|
|
267
|
+
if ( it.type === 'recent' ) { acts.push( { id: 'remove', label: M.remove, icon: 'close', onClick: ( x ) => { removeRecent( x.url ); showEmpty(); } } ); }
|
|
268
|
+
if ( acts.length ) {
|
|
269
|
+
const box = h( 'div', 'ak-palette__actions' );
|
|
270
|
+
acts.forEach( ( a ) => {
|
|
271
|
+
const b = a.url ? h( 'a', 'ak-palette__action', { href: a.url, 'aria-label': a.label, title: a.label } ) : h( 'button', 'ak-palette__action', { type: 'button', 'aria-label': a.label, title: a.label } );
|
|
272
|
+
b.append( svg( a.icon || 'edit' ) );
|
|
273
|
+
b.addEventListener( 'click', ( e ) => { if ( a.onClick ) { e.preventDefault(); a.onClick( it, e ); } else { close(); } e.stopPropagation(); } );
|
|
274
|
+
box.append( b );
|
|
275
|
+
} );
|
|
276
|
+
row.append( box );
|
|
277
|
+
}
|
|
278
|
+
return row;
|
|
279
|
+
}
|
|
280
|
+
function setActive( i, scroll ) {
|
|
281
|
+
const rows = list.querySelectorAll( '.ak-palette__item' );
|
|
282
|
+
if ( !rows.length ) { active = -1; input.removeAttribute( 'aria-activedescendant' ); return; }
|
|
283
|
+
if ( i < 0 ) { i = -1; } else { i = ( ( i % rows.length ) + rows.length ) % rows.length; }
|
|
284
|
+
rows.forEach( ( r, k ) => { r.classList.toggle( 'is-active', k === i ); r.setAttribute( 'aria-selected', k === i ? 'true' : 'false' ); } );
|
|
285
|
+
active = i;
|
|
286
|
+
if ( i >= 0 ) {
|
|
287
|
+
input.setAttribute( 'aria-activedescendant', rows[ i ].id );
|
|
288
|
+
if ( scroll !== false ) { rows[ i ].scrollIntoView( { block: 'nearest' } ); }
|
|
289
|
+
} else { input.removeAttribute( 'aria-activedescendant' ); }
|
|
290
|
+
}
|
|
291
|
+
function renderHints( arr ) {
|
|
292
|
+
hints.textContent = '';
|
|
293
|
+
arr.forEach( ( x ) => { const s = h( 'span' ); const k = h( 'kbd', 'ak-kbd' ); k.textContent = x[ 0 ]; s.append( k, document.createTextNode( x[ 1 ] ) ); hints.append( s ); } );
|
|
294
|
+
}
|
|
295
|
+
/* 页脚提示随状态变:Esc 的含义 = 有字「清空」→ 模式中「返回」→ 否则「关闭」 */
|
|
296
|
+
function updateHints() {
|
|
297
|
+
const q = query.trim();
|
|
298
|
+
const esc = q ? M.hintClear : ( mode ? M.hintBack : M.hintClose );
|
|
299
|
+
const arr = [];
|
|
300
|
+
if ( q && !mode && q[ 0 ] !== '/' ) { arr.push( [ '⇧↵', M.hintFulltext ] ); }
|
|
301
|
+
arr.push( [ '↑↓', M.hintNavigate ], [ '↵', M.hintOpen ], [ 'Esc', esc ] );
|
|
302
|
+
renderHints( arr );
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ── 空态 / 模式列表 / 搜索 ── */
|
|
306
|
+
function fulltextItem( q ) { return { type: 'search', label: fmt( M.fulltext, q ), desc: M.fulltextDesc, url: urls.fulltext( q ), match: false, dark: true, icon: 'fulltext', noRecent: true }; }
|
|
307
|
+
function modeItems( filter ) {
|
|
308
|
+
const f = ( filter || '' ).toLowerCase();
|
|
309
|
+
return modes.filter( ( m ) => !f || m.id.includes( f ) || ( m.trigger || '' ).toLowerCase().includes( f ) || ( m.label || '' ).toLowerCase().includes( f ) )
|
|
310
|
+
.map( ( m ) => ( { type: 'mode', label: m.trigger || ( '/' + m.id ), en: m.label, desc: m.desc, glyph: m.alias || '/', match: false, meta: m.alias ? [ { kbd: m.alias } ] : [], onSelect: () => enterMode( m, '' ), mode: m } ) );
|
|
311
|
+
}
|
|
312
|
+
let emptySeq = 0;
|
|
313
|
+
function showEmpty() {
|
|
314
|
+
const my = ++emptySeq;
|
|
315
|
+
const groups = [];
|
|
316
|
+
const rec = readRecent();
|
|
317
|
+
if ( rec.length ) { groups.push( { id: 'recent', label: M.recent, en: M.recentEn, items: rec.map( ( r ) => Object.assign( { match: false }, r, { type: 'recent', icon: r.icon || ( r.type === 'category' ? 'category' : r.type === 'file' ? 'file' : r.type === 'user' ? 'user' : 'clock' ) } ) ) } ); }
|
|
318
|
+
const finish = ( more ) => {
|
|
319
|
+
if ( my !== emptySeq || query || mode ) { return; }
|
|
320
|
+
const all = groups.concat( more || [] );
|
|
321
|
+
renderGroups( all, '', { highlight: -1 } );
|
|
322
|
+
renderEmptyCard( !all.length );
|
|
323
|
+
updateHints();
|
|
324
|
+
};
|
|
325
|
+
if ( providers.empty ) { Promise.resolve( providers.empty( { mode: null } ) ).then( finish, () => finish( [] ) ); } else { finish( [] ); }
|
|
326
|
+
}
|
|
327
|
+
function renderEmptyCard( noList ) {
|
|
328
|
+
empty.textContent = '';
|
|
329
|
+
const t = h( 'p', 'ak-palette__empty-title' ); t.textContent = M.emptyTitle;
|
|
330
|
+
const d = h( 'p', 'ak-palette__empty-desc' );
|
|
331
|
+
const parts = fmt( M.emptyDesc, '' ).split( '' );
|
|
332
|
+
d.append( parts[ 0 ] ); if ( parts.length > 1 ) { const k = h( 'kbd', 'ak-kbd' ); k.textContent = '/'; d.append( k, parts[ 1 ] ); }
|
|
333
|
+
if ( noList ) { empty.append( t, d ); } // 有最近访问时只保留快捷入口(更短),没有时显示完整提示
|
|
334
|
+
const sc = providers.shortcuts ? ( providers.shortcuts() || [] ) : [];
|
|
335
|
+
if ( sc.length ) {
|
|
336
|
+
const lab = h( 'div', 'ak-palette__label' ); lab.textContent = M.shortcuts; const en = h( 'span', 'ak-en' ); en.textContent = M.shortcutsEn; lab.append( en );
|
|
337
|
+
const box = h( 'div', 'ak-palette__shortcuts' );
|
|
338
|
+
sc.forEach( ( s ) => { const a = h( 'a', 'ak-palette__shortcut', { href: s.url } ); a.textContent = s.label; a.addEventListener( 'click', () => { close(); } ); box.append( a ); } );
|
|
339
|
+
empty.append( lab, box );
|
|
340
|
+
}
|
|
341
|
+
empty.hidden = !( noList || sc.length );
|
|
342
|
+
empty.classList.toggle( 'ak-palette__empty--compact', !noList );
|
|
343
|
+
syncHeight();
|
|
344
|
+
}
|
|
345
|
+
function showModes( filter ) {
|
|
346
|
+
const its = modeItems( filter );
|
|
347
|
+
renderGroups( [ { id: 'modes', label: M.modes, en: M.modesEn, items: its } ], '', { highlight: 0, notice: its.length ? null : fmt( M.noResults, '/' + filter ) } );
|
|
348
|
+
updateHints();
|
|
349
|
+
}
|
|
350
|
+
function runSearch( q ) {
|
|
351
|
+
const my = ++seq;
|
|
352
|
+
if ( ctrl ) { ctrl.abort(); }
|
|
353
|
+
ctrl = window.AbortController ? new AbortController() : null;
|
|
354
|
+
const fn = mode ? mode.search : providers.search;
|
|
355
|
+
if ( !fn ) { renderGroups( mode || !q ? [] : [ { id: 'ft', tail: true, items: [ fulltextItem( q ) ] } ], q ); return; }
|
|
356
|
+
setLoading( true );
|
|
357
|
+
Promise.resolve().then( () => fn( q, ctrl && ctrl.signal, { mode: mode } ) ).then( ( groups ) => {
|
|
358
|
+
if ( my !== seq ) { return; }
|
|
359
|
+
setLoading( false );
|
|
360
|
+
groups = ( groups || [] ).filter( ( g ) => g && g.items && g.items.length );
|
|
361
|
+
lastGroups = groups;
|
|
362
|
+
const withFt = ( !mode || mode.fulltext !== false ) && q ? groups.concat( [ { id: 'ft', tail: true, items: [ fulltextItem( q ) ] } ] ) : groups;
|
|
363
|
+
const none = !groups.length;
|
|
364
|
+
renderGroups( withFt, q, { highlight: 0, notice: none ? fmt( M.noResults, q ) + ( mode ? '' : ' · ' + M.noResultsDesc ) : null } );
|
|
365
|
+
updateHints();
|
|
366
|
+
}, ( err ) => {
|
|
367
|
+
if ( my !== seq || ( err && err.name === 'AbortError' ) ) { return; }
|
|
368
|
+
setLoading( false );
|
|
369
|
+
renderGroups( q ? [ { id: 'ft', tail: true, items: [ fulltextItem( q ) ] } ] : [], q, { highlight: 0, notice: M.error, error: true } );
|
|
370
|
+
} );
|
|
371
|
+
}
|
|
372
|
+
function refresh( immediate ) {
|
|
373
|
+
clearTimeout( debounceTimer );
|
|
374
|
+
const q = query.trim();
|
|
375
|
+
if ( !mode && !q ) { seq++; setLoading( false ); showEmpty(); return; }
|
|
376
|
+
if ( !mode && q[ 0 ] === '/' ) { seq++; setLoading( false ); showModes( q.slice( 1 ) ); return; }
|
|
377
|
+
empty.hidden = true;
|
|
378
|
+
if ( immediate ) { runSearch( q ); } else { debounceTimer = setTimeout( () => runSearch( q ), 120 ); }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ── 模式 ── */
|
|
382
|
+
function enterMode( m, rest ) {
|
|
383
|
+
mode = m;
|
|
384
|
+
root.classList.add( 'has-mode' );
|
|
385
|
+
chip.textContent = m.label || m.id;
|
|
386
|
+
input.placeholder = m.placeholder || placeholder;
|
|
387
|
+
setQuery( rest || '' );
|
|
388
|
+
refresh( true );
|
|
389
|
+
input.focus();
|
|
390
|
+
}
|
|
391
|
+
function exitMode() {
|
|
392
|
+
mode = null;
|
|
393
|
+
root.classList.remove( 'has-mode' );
|
|
394
|
+
input.placeholder = placeholder;
|
|
395
|
+
setQuery( '' );
|
|
396
|
+
refresh( true );
|
|
397
|
+
input.focus();
|
|
398
|
+
}
|
|
399
|
+
function setQuery( v ) { query = v; input.value = v; root.classList.toggle( 'has-query', !!v ); }
|
|
400
|
+
function detectTrigger( v ) {
|
|
401
|
+
if ( mode || !v ) { return false; }
|
|
402
|
+
// 单字触发符(> # @ ~):只在开头且只有一个字符时进入,避免误伤正常输入
|
|
403
|
+
const m1 = modes.find( ( m ) => m.alias && v[ 0 ] === m.alias );
|
|
404
|
+
if ( m1 && v.length === 1 ) { enterMode( m1, '' ); return true; }
|
|
405
|
+
// /action: 或 /action␣ → 进入
|
|
406
|
+
const m2 = modes.find( ( m ) => m.trigger && ( v.toLowerCase().indexOf( m.trigger.toLowerCase() + ':' ) === 0 || v.toLowerCase().indexOf( m.trigger.toLowerCase() + ' ' ) === 0 ) );
|
|
407
|
+
if ( m2 ) { enterMode( m2, v.slice( m2.trigger.length + 1 ) ); return true; }
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ── 选择 ── */
|
|
412
|
+
function selectIndex( i, ev ) {
|
|
413
|
+
const it = items[ i ]; if ( !it ) { return; }
|
|
414
|
+
if ( it.onSelect ) { const r = it.onSelect( it, ev ); if ( r !== 'navigate' ) { return; } }
|
|
415
|
+
if ( opt.onSelect && opt.onSelect( it, ev ) === false ) { pushRecent( it ); return; }
|
|
416
|
+
if ( !it.url ) { return; }
|
|
417
|
+
pushRecent( it );
|
|
418
|
+
const nt = ev && ( ev.metaKey || ev.ctrlKey );
|
|
419
|
+
if ( nt ) { window.open( it.url, '_blank' ); return; }
|
|
420
|
+
close();
|
|
421
|
+
location.href = it.url;
|
|
422
|
+
}
|
|
423
|
+
function goto( url ) { close(); location.href = url; }
|
|
424
|
+
|
|
425
|
+
/* ── 开关 ── */
|
|
426
|
+
function open( prefill ) {
|
|
427
|
+
if ( closing ) { clearTimeout( closeTimer ); finishClose(); }
|
|
428
|
+
if ( isOpen ) { input.focus(); if ( typeof prefill === 'string' ) { setQuery( prefill ); refresh( true ); } return; }
|
|
429
|
+
isOpen = true;
|
|
430
|
+
backdrop.hidden = false; root.hidden = false;
|
|
431
|
+
backdrop.classList.remove( 'is-closing' ); root.classList.remove( 'is-closing' );
|
|
432
|
+
if ( triggerBtn ) { triggerBtn.setAttribute( 'aria-expanded', 'true' ); }
|
|
433
|
+
document.documentElement.classList.add( 'ak-palette-open' );
|
|
434
|
+
mode = null; root.classList.remove( 'has-mode' ); input.placeholder = placeholder;
|
|
435
|
+
setQuery( typeof prefill === 'string' ? prefill : '' );
|
|
436
|
+
lastResizeH = 0; body.style.transition = 'none'; body.style.height = '';
|
|
437
|
+
refresh( true );
|
|
438
|
+
requestAnimationFrame( () => { syncHeight(); requestAnimationFrame( () => { body.style.transition = ''; } ); } );
|
|
439
|
+
if ( ro ) { ro.observe( viewport ); }
|
|
440
|
+
input.focus(); input.select();
|
|
441
|
+
document.dispatchEvent( new CustomEvent( 'akds:palette-open' ) );
|
|
442
|
+
}
|
|
443
|
+
function finishClose() {
|
|
444
|
+
closing = false;
|
|
445
|
+
backdrop.hidden = true; root.hidden = true;
|
|
446
|
+
backdrop.classList.remove( 'is-closing' ); root.classList.remove( 'is-closing' );
|
|
447
|
+
if ( ro ) { ro.unobserve( viewport ); }
|
|
448
|
+
body.style.height = '';
|
|
449
|
+
}
|
|
450
|
+
function close( restoreFocus ) {
|
|
451
|
+
if ( !isOpen ) { return; }
|
|
452
|
+
isOpen = false; closing = true;
|
|
453
|
+
seq++; emptySeq++; setLoading( false ); clearTimeout( debounceTimer ); if ( ctrl ) { ctrl.abort(); }
|
|
454
|
+
document.documentElement.classList.remove( 'ak-palette-open' );
|
|
455
|
+
if ( triggerBtn ) { triggerBtn.setAttribute( 'aria-expanded', 'false' ); }
|
|
456
|
+
backdrop.classList.add( 'is-closing' ); root.classList.add( 'is-closing' );
|
|
457
|
+
const reduce = window.matchMedia && matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
|
|
458
|
+
closeTimer = setTimeout( finishClose, reduce ? 0 : 160 );
|
|
459
|
+
if ( restoreFocus !== false && triggerBtn && triggerBtn.offsetParent ) { triggerBtn.focus(); }
|
|
460
|
+
else if ( restoreFocus !== false ) { const t = document.querySelector( opt.toggles || '.ak-header__search-toggle' ); if ( t && t.offsetParent ) { t.focus(); } }
|
|
461
|
+
document.dispatchEvent( new CustomEvent( 'akds:palette-close' ) );
|
|
462
|
+
}
|
|
463
|
+
function toggle() { if ( isOpen ) { close(); } else { open(); } }
|
|
464
|
+
|
|
465
|
+
/* ── 事件 ── */
|
|
466
|
+
if ( triggerBtn ) { triggerBtn.addEventListener( 'click', () => open() ); }
|
|
467
|
+
document.addEventListener( 'click', ( e ) => {
|
|
468
|
+
const t = e.target.closest( opt.toggles || '.ak-header__search-toggle' );
|
|
469
|
+
if ( t ) { e.preventDefault(); toggle(); return; }
|
|
470
|
+
const trig = e.target.closest( '.ak-search-trigger[data-prefill], [data-akds-search]' );
|
|
471
|
+
if ( trig && trig !== triggerBtn ) { e.preventDefault(); open( trig.dataset.prefill || trig.dataset.akdsSearch || '' ); }
|
|
472
|
+
} );
|
|
473
|
+
backdrop.addEventListener( 'click', () => close() );
|
|
474
|
+
closeBtn.addEventListener( 'click', () => close() );
|
|
475
|
+
clearBtn.addEventListener( 'click', () => { setQuery( '' ); refresh( true ); input.focus(); } );
|
|
476
|
+
backBtn.addEventListener( 'click', () => exitMode() );
|
|
477
|
+
cmdBtn.addEventListener( 'click', () => { if ( mode ) { exitMode(); } setQuery( '/' ); refresh( true ); input.focus(); } );
|
|
478
|
+
form.addEventListener( 'submit', ( e ) => {
|
|
479
|
+
// 表单原生提交只在「没有高亮项」时才有意义(= MW 的 Go);有高亮项则由 keydown 处理
|
|
480
|
+
if ( active >= 0 && items[ active ] ) { e.preventDefault(); return; }
|
|
481
|
+
if ( urls.go && query.trim() ) { e.preventDefault(); goto( urls.go( query.trim() ) ); }
|
|
482
|
+
} );
|
|
483
|
+
input.addEventListener( 'input', () => {
|
|
484
|
+
const v = input.value;
|
|
485
|
+
query = v; root.classList.toggle( 'has-query', !!v );
|
|
486
|
+
if ( detectTrigger( v ) ) { return; }
|
|
487
|
+
updateHints();
|
|
488
|
+
refresh( false );
|
|
489
|
+
} );
|
|
490
|
+
list.addEventListener( 'mousedown', ( e ) => { if ( !e.target.closest( '.ak-palette__action' ) ) { e.preventDefault(); } } ); // 保住输入框焦点
|
|
491
|
+
list.addEventListener( 'mousemove', ( e ) => { const row = e.target.closest( '.ak-palette__item' ); if ( row ) { const i = +row.dataset.index; if ( i !== active ) { setActive( i, false ); } } } );
|
|
492
|
+
list.addEventListener( 'click', ( e ) => {
|
|
493
|
+
if ( e.target.closest( '.ak-palette__action' ) ) { return; }
|
|
494
|
+
const row = e.target.closest( '.ak-palette__item' ); if ( !row ) { return; }
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
selectIndex( +row.dataset.index, e );
|
|
497
|
+
} );
|
|
498
|
+
root.addEventListener( 'keydown', ( e ) => {
|
|
499
|
+
if ( e.key === 'ArrowDown' ) { e.preventDefault(); setActive( active + 1 ); return; }
|
|
500
|
+
if ( e.key === 'ArrowUp' ) { e.preventDefault(); setActive( active < 0 ? -1 : active - 1 ); return; }
|
|
501
|
+
if ( e.key === 'Home' && e.target === input && !query ) { e.preventDefault(); setActive( 0 ); return; }
|
|
502
|
+
if ( e.key === 'End' && e.target === input && !query ) { e.preventDefault(); setActive( items.length - 1 ); return; }
|
|
503
|
+
if ( e.key === 'Enter' ) {
|
|
504
|
+
if ( e.target !== input && e.target.closest( '.ak-palette__action, .ak-palette__shortcut, button, a' ) ) { return; } // 让按钮自己处理
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
const q = query.trim();
|
|
507
|
+
if ( e.shiftKey && q && !mode && q[ 0 ] !== '/' ) { goto( urls.fulltext( q ) ); return; }
|
|
508
|
+
if ( active >= 0 && items[ active ] ) { selectIndex( active, e ); return; }
|
|
509
|
+
if ( q && urls.go && !mode ) { goto( urls.go( q ) ); }
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if ( e.key === 'Escape' ) {
|
|
513
|
+
e.preventDefault(); e.stopPropagation();
|
|
514
|
+
if ( query ) { setQuery( '' ); refresh( true ); return; }
|
|
515
|
+
if ( mode ) { exitMode(); return; }
|
|
516
|
+
close();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if ( e.key === 'Backspace' && mode && !input.value && e.target === input ) { e.preventDefault(); exitMode(); return; }
|
|
520
|
+
if ( e.key === 'ArrowLeft' && mode && !input.value && e.target === input ) { e.preventDefault(); exitMode(); return; }
|
|
521
|
+
if ( e.key === 'Tab' ) {
|
|
522
|
+
// 焦点在面板内循环:输入框 → 清空 → 关闭 → 高亮行的动作 → 快捷入口 → 页脚按钮 → 回到输入框
|
|
523
|
+
const f = Array.from( root.querySelectorAll( 'input, button, a[href], [tabindex="0"]' ) ).filter( ( el ) => el.offsetParent !== null && !el.disabled && el.tabIndex !== -1 && !el.closest( '[hidden]' ) );
|
|
524
|
+
if ( !f.length ) { return; }
|
|
525
|
+
let i = f.indexOf( document.activeElement ); if ( i < 0 ) { i = 0; }
|
|
526
|
+
i = ( i + ( e.shiftKey ? -1 : 1 ) + f.length ) % f.length;
|
|
527
|
+
e.preventDefault(); f[ i ].focus();
|
|
528
|
+
}
|
|
529
|
+
} );
|
|
530
|
+
// 全局快捷键:"/"(非表单焦点)、Ctrl/⌘K、accesskey F(Alt(+Shift)+F / Ctrl+Alt+F)
|
|
531
|
+
window.addEventListener( 'keydown', ( e ) => {
|
|
532
|
+
if ( isOpen ) { return; }
|
|
533
|
+
const mac = isMac();
|
|
534
|
+
const slash = e.key === '/' && !e.metaKey && !e.ctrlKey && !e.altKey;
|
|
535
|
+
const ctrlK = ( e.ctrlKey || e.metaKey ) && !e.altKey && !e.shiftKey && ( e.key === 'k' || e.key === 'K' );
|
|
536
|
+
const accessF = ( e.key === 'f' || e.key === 'F' || e.code === 'KeyF' ) && ( mac ? ( e.ctrlKey && e.altKey ) : ( e.altKey && ( e.shiftKey || !e.ctrlKey ) ) );
|
|
537
|
+
if ( ( slash || ctrlK || accessF ) && !isFormField( e.target ) ) { e.preventDefault(); open(); }
|
|
538
|
+
else if ( ctrlK && isFormField( e.target ) && e.target !== input ) { e.preventDefault(); open(); }
|
|
539
|
+
}, true );
|
|
540
|
+
|
|
541
|
+
const api = { open, close, toggle, refresh: () => refresh( true ), isOpen: () => isOpen, root, input, form, trigger: triggerBtn };
|
|
542
|
+
document.dispatchEvent( new CustomEvent( 'akds:palette-ready', { detail: api } ) );
|
|
543
|
+
return api;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const mod = { init, ICONS, MSG };
|
|
547
|
+
window.akdsSearchPalette = mod;
|
|
548
|
+
if ( typeof module !== 'undefined' && module.exports ) { module.exports = mod; }
|
|
549
|
+
}() );
|