@objectivex666/dsh-settings-search 1.4.4 → 1.5.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/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +167 -13
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
- 🔍 **Live search** — a search box at the top of the settings panel's left navigation filters every registered settings page (`settings.section`) and general item (`settings.general.item`) as you type.
|
|
16
16
|
- 📂 **In-page option search (v1.2.0)** — search beyond pages and reach the specific options inside them: Plugins-page tabs (`settings.plugins.tab`), Web UI plugin cards (`web-ui.plugin.item`), general rows, and more, each shown with a "page › option" breadcrumb.
|
|
17
|
+
- 🔤 **Pinyin association search (v1.5.0)** — type initials (`sz`) or full pinyin (`shezhi`) to surface Chinese settings items; navigate with `↑`/`↓`, confirm with `Enter`, dismiss with `Esc`, and matched text is highlighted.
|
|
17
18
|
- 🧭 **Click-to-jump** — selecting a result clicks the matching left-nav entry to open its section; for tab options it also opens the tab and flashes the target row. A manual-path hint appears if automatic navigation is not possible.
|
|
18
19
|
- 🌱 **Progressive indexing** — self-drawing settings rows are harvested from the rendered DOM (via their `[data-slot]` anchors) the first time their page is visited, then stay searchable.
|
|
19
20
|
- 🛠 **Built-in settings page (v1.4.0)** — a "Settings Search" page in the left navigation shows the current version and checks npm for a newer release on demand; when one exists it hands you a copy-ready `dsh plugin update` command.
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
- 🔍 **实时搜索**:在设置面板左侧导航顶部嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
|
|
16
16
|
- 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
|
|
17
|
+
- 🔤 **拼音联想搜索(v1.5.0)**:输入拼音首字母(如 `sz`)或全拼(如 `shezhi`)即可联想中文设置项;支持 `↑`/`↓` 选择、`Enter` 跳转、`Esc` 关闭,命中的文字自动高亮。
|
|
17
18
|
- 🧭 **点击即达**:选中结果后自动点击对应的左侧导航项打开所在分区;若是页内标签页会继续点开对应标签,并高亮闪烁目标选项行。无法自动跳转时显示手动路径提示。
|
|
18
19
|
- 🌱 **渐进索引**:自绘界面的设置行会在其所在页面首次被访问时从渲染 DOM 中收割标题并加入索引(此后始终可搜)。
|
|
19
20
|
- 🛠 **自带设置页(v1.4.0)**:左侧导航新增「设置搜索」页——展示当前版本,一键检查 npm 注册表上的最新版本;有更新时直接给出复制就用的 `dsh plugin update` 命令。
|
package/lib/client.js
CHANGED
|
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
|
|
31
31
|
const NS = 'settings-search-v5'
|
|
32
32
|
/** Single source of truth is package.json — scripts/check.mjs asserts this matches. */
|
|
33
|
-
const PKG_VERSION = '1.
|
|
33
|
+
const PKG_VERSION = '1.5.0'
|
|
34
34
|
const NPM_NAME = '@objectivex666/dsh-settings-search'
|
|
35
35
|
const zh = {
|
|
36
36
|
title: '设置',
|
|
@@ -84,6 +84,89 @@ window.__ModuleLoader__.load({
|
|
|
84
84
|
{ slot: 'settings.general.item', parent: 'general' },
|
|
85
85
|
]
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Compact zh→pinyin table (tone-less) for association search. Covers
|
|
89
|
+
* characters common in settings UI labels; anything outside the table
|
|
90
|
+
* simply falls back to plain substring matching. Entries: 字pinyin.
|
|
91
|
+
*/
|
|
92
|
+
const PINYIN_TABLE = `
|
|
93
|
+
设she 置zhi 通tong 用yong 插cha 件jian 搜sou 索suo 外wai 观guan 语yu 言yan
|
|
94
|
+
声sheng 音yin 知zhi 账zhang 户hu 安an 全quan 网wang 络luo 存cun 储chu 关guan
|
|
95
|
+
于yu 主zhu 题ti 字zi 体ti 显xian 示shi 隐yin 私si 帮bang 助zhu 反fan 馈kui
|
|
96
|
+
更geng 新xin 版ban 本ben 深shen 度du 思si 考kao 模mo 式shi 对dui 话hua 上shang
|
|
97
|
+
下xia 文wen 记ji 忆yi 提ti 词ci 快kuai 捷jie 键jian 界jie 面mian 颜yan 色se
|
|
98
|
+
背bei 景jing 号hao 密mi 数shu 据ju 同tong 步bu 备bei 份fen 导dao 出chu 入ru
|
|
99
|
+
重chong 清qing 除chu 登deng 录lu 销xiao 码ma 邮you 箱xiang 手shou 机ji 绑bang
|
|
100
|
+
定ding 验yan 证zheng 会hui 员yuan 订ding 阅yue 支zhi 付fu 单dan 发fa 票piao
|
|
101
|
+
应ying 权quan 限xian 推tui 荐jian 个ge 性xing 化hua 高gao 级ji 基ji 选xuan
|
|
102
|
+
项xiang 开kai 启qi 禁jin 自zi 动dong 默mo 认ren 义yi 夹jia 径jing 载zai 传chuan
|
|
103
|
+
享xiang 制zhi 粘nian 贴tie 剪jian 切qie 撤che 复fu 保bao 打da 加jia 编bian 辑ji
|
|
104
|
+
查cha 看kan 排pai 序xu 筛shai 刷shua 速su 质zhi 量liang 大da 小xiao 位wei 时shi
|
|
105
|
+
间jian 日ri 期qi 区qu 格ge 符fu 换huan 行hang 缩suo 进jin 空kong 电dian 池chi
|
|
106
|
+
亮liang 滚gun 桌zhuo 免mian 扰rao 屏ping 幕mu 源yuan 节jie 眠mian 器qi 目mu
|
|
107
|
+
标biao 签qian 窗chuang 口kou 工gong 具ju 栏lan 菜cai 群qun 频pin 道dao 乐yue
|
|
108
|
+
图tu 片pian 相xiang 册ce 拍pai 照zhao 头tou 麦mai 克ke 风feng 蓝lan 牙ya 连lian
|
|
109
|
+
接jie 印yin 扫sao 描miao 感gan 智zhi 能neng 家jia 居ju 灯deng 光guang 温wen
|
|
110
|
+
湿shi 门men 锁suo 帘lian 场chang 情qing 人ren 朋peng 友you 客ke 厅ting 房fang
|
|
111
|
+
厨chu 卫wei 生sheng 书shu 阳yang 台tai 车che 库ku 园yuan 院yuan 坪ping 树shu
|
|
112
|
+
木mu 草cao 增zeng 删shan 改gai 移yi 停ting 运yun 行xing 测ce 境jing 渠qu 官guan
|
|
113
|
+
方fang 正zheng 内nei 公gong 有you 效xiao 失shi 败bai 成cheng 功gong 完wan 毕bi
|
|
114
|
+
中zhong 英ying 韩han 法fa 德de 西xi 俄e 翻fan 译yi 简jian 繁fan 历li 史shi 聊liao
|
|
115
|
+
天tian 消xiao 息xi 醒xing 静jing 震zhen 红hong 点dian 角jiao 未wei 读du 部bu
|
|
116
|
+
为wei 已yi 好 hao 坏huai 多duo 少shao
|
|
117
|
+
`
|
|
118
|
+
const PINYIN = new Map(
|
|
119
|
+
PINYIN_TABLE.split(/\s+/).filter(Boolean).map((pair) => [pair[0], pair.slice(1)])
|
|
120
|
+
)
|
|
121
|
+
const CJK_RE = /[\u4e00-\u9fff]/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Transliterate a label into full-pinyin and initial-letter keys for
|
|
125
|
+
* association search. Returns null when any CJK character is outside
|
|
126
|
+
* the built-in table (then only substring matching applies).
|
|
127
|
+
*/
|
|
128
|
+
const pinyinKeys = (text) => {
|
|
129
|
+
let full = ''
|
|
130
|
+
let initials = ''
|
|
131
|
+
for (const ch of text) {
|
|
132
|
+
if (CJK_RE.test(ch)) {
|
|
133
|
+
const py = PINYIN.get(ch)
|
|
134
|
+
if (py === undefined) return null
|
|
135
|
+
full += py
|
|
136
|
+
initials += py[0]
|
|
137
|
+
} else if (/[a-zA-Z0-9]/.test(ch)) {
|
|
138
|
+
const lower = ch.toLowerCase()
|
|
139
|
+
full += lower
|
|
140
|
+
initials += lower
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return full === '' ? null : { full, initials }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Match one query token: raw substring, full pinyin, or initial letters. */
|
|
147
|
+
const tokenMatch = (haystackLower, keys, token) => {
|
|
148
|
+
if (haystackLower.includes(token)) return 'text'
|
|
149
|
+
if (keys !== null) {
|
|
150
|
+
if (keys.full.includes(token)) return 'pinyin'
|
|
151
|
+
if (keys.initials.startsWith(token)) return 'initials'
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Character count of the label consumed by a full-pinyin prefix. */
|
|
157
|
+
const pinyinPrefixChars = (label, pinyinLength) => {
|
|
158
|
+
let consumed = 0
|
|
159
|
+
let acc = 0
|
|
160
|
+
for (const ch of label) {
|
|
161
|
+
const py = CJK_RE.test(ch) ? PINYIN.get(ch) : (/[a-zA-Z0-9]/.test(ch) ? ch.toLowerCase() : '')
|
|
162
|
+
if (py === undefined || py === '') continue
|
|
163
|
+
acc += py.length
|
|
164
|
+
consumed += 1
|
|
165
|
+
if (acc >= pinyinLength) break
|
|
166
|
+
}
|
|
167
|
+
return consumed
|
|
168
|
+
}
|
|
169
|
+
|
|
87
170
|
const CSS = `
|
|
88
171
|
.sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
89
172
|
.sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
|
|
@@ -99,8 +182,10 @@ window.__ModuleLoader__.load({
|
|
|
99
182
|
.sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
100
183
|
.sss-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
|
101
184
|
.sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
185
|
+
.sss-item.is-active { background: var(--dsw-alias-interactive-bg-hover); }
|
|
102
186
|
.sss-item-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
103
187
|
.sss-item-label { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
188
|
+
.sss-item-label mark { background: transparent; color: inherit; font-weight: 600; }
|
|
104
189
|
.sss-item-path { font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
105
190
|
.sss-item-kind { flex-shrink: 0; font-size: 10px; color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 4px; padding: 1px 5px; }
|
|
106
191
|
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
@@ -420,19 +505,34 @@ window.__ModuleLoader__.load({
|
|
|
420
505
|
|
|
421
506
|
const q = query.trim().toLowerCase()
|
|
422
507
|
const tokens = q.split(/\s+/).filter(Boolean)
|
|
423
|
-
const matches = (haystack) => tokens.every((tok) => haystack.includes(tok))
|
|
424
508
|
|
|
425
509
|
const sections = model.readSections()
|
|
426
510
|
const deeps = q !== '' ? model.readDeep() : []
|
|
511
|
+
const matchItem = (item, haystackLower) =>
|
|
512
|
+
tokens.every((tok) => tokenMatch(haystackLower, item.keys, tok) !== null)
|
|
427
513
|
const shownPages = q !== ''
|
|
428
|
-
? sections
|
|
514
|
+
? sections
|
|
515
|
+
.map((s) => ({ ...s, keys: pinyinKeys(s.label) }))
|
|
516
|
+
.filter((s) => matchItem(s, `${s.label} ${s.id}`.toLowerCase()))
|
|
429
517
|
: []
|
|
430
518
|
const shownOptions = q !== ''
|
|
431
|
-
? deeps
|
|
519
|
+
? deeps
|
|
520
|
+
.map((o) => ({ ...o, keys: pinyinKeys(`${o.label} ${o.parentLabel}`) }))
|
|
521
|
+
.filter((o) => matchItem(o, `${o.hay} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
|
|
432
522
|
: []
|
|
433
523
|
const none = q !== '' && shownPages.length === 0 && shownOptions.length === 0
|
|
524
|
+
const results = [...shownPages, ...shownOptions]
|
|
434
525
|
|
|
435
526
|
const [hint, setHint] = React.useState(null)
|
|
527
|
+
const [active, setActive] = React.useState(0)
|
|
528
|
+
const popRef = React.useRef(null)
|
|
529
|
+
const activeIndex = Math.min(active, Math.max(results.length - 1, 0))
|
|
530
|
+
|
|
531
|
+
React.useEffect(() => { setActive(0) }, [query])
|
|
532
|
+
React.useEffect(() => {
|
|
533
|
+
const el = popRef.current?.querySelector('.sss-item.is-active')
|
|
534
|
+
el?.scrollIntoView?.({ block: 'nearest' })
|
|
535
|
+
}, [activeIndex, q])
|
|
436
536
|
const pick = (item) => {
|
|
437
537
|
setOpen(false)
|
|
438
538
|
const jumped = item.kind === 'page'
|
|
@@ -443,21 +543,59 @@ window.__ModuleLoader__.load({
|
|
|
443
543
|
})
|
|
444
544
|
}
|
|
445
545
|
|
|
446
|
-
const
|
|
546
|
+
const mark = (text) => React.createElement('mark', { className: 'sss-hit' }, text)
|
|
547
|
+
|
|
548
|
+
/** Highlight the matched substring, or the pinyin-covered prefix. */
|
|
549
|
+
const highlightLabel = (item) => {
|
|
550
|
+
const label = item.label
|
|
551
|
+
const lower = label.toLowerCase()
|
|
552
|
+
let bestStart = -1
|
|
553
|
+
let bestLen = 0
|
|
554
|
+
for (const tok of tokens) {
|
|
555
|
+
const at = lower.indexOf(tok)
|
|
556
|
+
if (at !== -1 && (bestStart === -1 || at < bestStart)) {
|
|
557
|
+
bestStart = at
|
|
558
|
+
bestLen = tok.length
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (bestStart !== -1) {
|
|
562
|
+
return [
|
|
563
|
+
label.slice(0, bestStart),
|
|
564
|
+
mark(label.slice(bestStart, bestStart + bestLen)),
|
|
565
|
+
label.slice(bestStart + bestLen),
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
const keys = item.keys
|
|
569
|
+
if (keys !== null && tokens.length > 0) {
|
|
570
|
+
const tok = tokens[0]
|
|
571
|
+
const consumed = keys.initials.startsWith(tok)
|
|
572
|
+
? tok.length
|
|
573
|
+
: keys.full.startsWith(tok) ? pinyinPrefixChars(label, tok.length) : 0
|
|
574
|
+
if (consumed > 0) {
|
|
575
|
+
const cut = Math.min(consumed, label.length)
|
|
576
|
+
return [mark(label.slice(0, cut)), label.slice(cut)]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return [label]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const renderPage = (item, flatIndex) => React.createElement('li', {
|
|
447
583
|
key: 'page:' + item.id,
|
|
448
|
-
className: 'sss-item',
|
|
584
|
+
className: 'sss-item' + (flatIndex === activeIndex ? ' is-active' : ''),
|
|
449
585
|
onMouseDown: () => pick(item),
|
|
586
|
+
onMouseEnter: () => setActive(flatIndex),
|
|
450
587
|
}, [
|
|
451
|
-
React.createElement('span', { className: 'sss-item-label' }, item
|
|
588
|
+
React.createElement('span', { className: 'sss-item-label' }, highlightLabel(item)),
|
|
452
589
|
React.createElement('span', { className: 'sss-item-kind' }, t('page')),
|
|
453
590
|
])
|
|
454
|
-
const renderOption = (item) => React.createElement('li', {
|
|
591
|
+
const renderOption = (item, flatIndex) => React.createElement('li', {
|
|
455
592
|
key: item.key,
|
|
456
|
-
className: 'sss-item',
|
|
593
|
+
className: 'sss-item' + (flatIndex === activeIndex ? ' is-active' : ''),
|
|
457
594
|
onMouseDown: () => pick(item),
|
|
595
|
+
onMouseEnter: () => setActive(flatIndex),
|
|
458
596
|
}, [
|
|
459
597
|
React.createElement('span', { className: 'sss-item-col' }, [
|
|
460
|
-
React.createElement('span', { className: 'sss-item-label' }, item
|
|
598
|
+
React.createElement('span', { className: 'sss-item-label' }, highlightLabel(item)),
|
|
461
599
|
React.createElement('span', { className: 'sss-item-path' }, item.parentLabel),
|
|
462
600
|
]),
|
|
463
601
|
React.createElement('span', { className: 'sss-item-kind' }, t('option')),
|
|
@@ -472,6 +610,20 @@ window.__ModuleLoader__.load({
|
|
|
472
610
|
value: query,
|
|
473
611
|
placeholder: t('placeholder'),
|
|
474
612
|
spellCheck: false,
|
|
613
|
+
onKeyDown: (e) => {
|
|
614
|
+
if (e.key === 'ArrowDown') {
|
|
615
|
+
e.preventDefault()
|
|
616
|
+
if (results.length > 0) setActive(Math.min(activeIndex + 1, results.length - 1))
|
|
617
|
+
} else if (e.key === 'ArrowUp') {
|
|
618
|
+
e.preventDefault()
|
|
619
|
+
setActive(Math.max(activeIndex - 1, 0))
|
|
620
|
+
} else if (e.key === 'Enter') {
|
|
621
|
+
const item = results[activeIndex]
|
|
622
|
+
if (item) { e.preventDefault(); pick(item) }
|
|
623
|
+
} else if (e.key === 'Escape') {
|
|
624
|
+
setOpen(false)
|
|
625
|
+
}
|
|
626
|
+
},
|
|
475
627
|
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
|
|
476
628
|
onFocus: () => setOpen(true),
|
|
477
629
|
onBlur: () => setOpen(false),
|
|
@@ -482,17 +634,19 @@ window.__ModuleLoader__.load({
|
|
|
482
634
|
onClick: () => { setQuery(''); setHint(null); setOpen(false) },
|
|
483
635
|
}, '✕') : null,
|
|
484
636
|
]),
|
|
485
|
-
open && q !== '' ? React.createElement('div', { className: 'sss-pop' }, [
|
|
637
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [
|
|
486
638
|
none
|
|
487
639
|
? React.createElement('p', { className: 'sss-empty' }, t('empty'))
|
|
488
640
|
: React.createElement('div', { className: 'sss-groups' }, [
|
|
489
641
|
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
490
642
|
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
491
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
643
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
644
|
+
shownPages.map((item, i) => renderPage(item, i))),
|
|
492
645
|
]) : null,
|
|
493
646
|
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
494
647
|
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
495
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
648
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
649
|
+
shownOptions.map((item, i) => renderOption(item, shownPages.length + i))),
|
|
496
650
|
]) : null,
|
|
497
651
|
]),
|
|
498
652
|
]) : null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectivex666/dsh-settings-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|