@objectivex666/dsh-settings-search 1.1.2 → 1.2.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 +3 -1
- package/README.md +3 -1
- package/lib/client.js +283 -59
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
## Features
|
|
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
|
+
- 🧭 **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
|
+
- 🌱 **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.
|
|
17
19
|
- 🌐 **i18n** — built-in Chinese/English dictionaries that follow the DSH localization system and are easy to extend.
|
|
18
20
|
- 🎨 **Theme-aware** — styled entirely with CSS variables, automatically following the DSH theme (light/dark).
|
|
19
21
|
- ⚡ **Reactive** — subscribes to settings entries being added, removed, or changed, and refreshes results automatically.
|
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
## 功能特性
|
|
14
14
|
|
|
15
15
|
- 🔍 **实时搜索**:在设置面板左侧导航顶部嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
|
|
16
|
-
-
|
|
16
|
+
- 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
|
|
17
|
+
- 🧭 **点击即达**:选中结果后自动点击对应的左侧导航项打开所在分区;若是页内标签页会继续点开对应标签,并高亮闪烁目标选项行。无法自动跳转时显示手动路径提示。
|
|
18
|
+
- 🌱 **渐进索引**:自绘界面的设置行会在其所在页面首次被访问时从渲染 DOM 中收割标题并加入索引(此后始终可搜)。
|
|
17
19
|
- 🌐 **多语言支持**:内置中英文国际化,可跟随 DSH 本地化系统扩展。
|
|
18
20
|
- 🎨 **主题适配**:完全使用 CSS 变量,自动跟随 DSH 主题(亮/暗)。
|
|
19
21
|
- ⚡ **动态响应**:实时订阅设置项的新增/删除/变更,搜索结果自动刷新。
|
package/lib/client.js
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
* and the client kernel adopts the exported `{ apply, inject }` as the
|
|
8
8
|
* plugin's browser half.
|
|
9
9
|
*
|
|
10
|
-
* Behavior
|
|
11
|
-
*
|
|
12
|
-
* (`settings.header`, priority -1)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - embeds a live search box at the top of the settings panel's left
|
|
12
|
+
* navigation (`settings.header`, priority -1);
|
|
13
|
+
* - indexes every settings PAGE (`settings.section`);
|
|
14
|
+
* - deep-indexes specific OPTIONS inside pages from machine-readable inner
|
|
15
|
+
* slots (`settings.plugins.tab`, `web-ui.plugin.item`,
|
|
16
|
+
* `settings.general.item`) — occupants that draw their own labels are read
|
|
17
|
+
* back from the rendered DOM via their `[data-slot]` anchors;
|
|
18
|
+
* - results show a breadcrumb path (页面 › 选项);
|
|
19
|
+
* - selecting a result really navigates: clicks the matching left-nav button
|
|
20
|
+
* (then the matching tab button), flashes the target row, and falls back to
|
|
21
|
+
* a breadcrumb hint when no DOM target exists.
|
|
15
22
|
*/
|
|
16
23
|
window.__ModuleLoader__.load({
|
|
17
24
|
id: '@objectivex666/dsh-settings-search',
|
|
@@ -21,28 +28,35 @@ window.__ModuleLoader__.load({
|
|
|
21
28
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
22
29
|
const React = require('react')
|
|
23
30
|
|
|
24
|
-
const NS = 'settings-search-
|
|
31
|
+
const NS = 'settings-search-v5'
|
|
25
32
|
const zh = {
|
|
26
33
|
title: '设置',
|
|
27
34
|
placeholder: '搜索设置…',
|
|
28
35
|
empty: '没有匹配的设置',
|
|
29
36
|
pages: '设置页',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
options: '页内选项',
|
|
38
|
+
page: '页面',
|
|
39
|
+
option: '选项',
|
|
40
|
+
jumpFail: '未能自动跳转,请手动打开:{path}',
|
|
34
41
|
}
|
|
35
42
|
const en = {
|
|
36
43
|
title: 'Settings',
|
|
37
44
|
placeholder: 'Search settings…',
|
|
38
45
|
empty: 'No matching settings',
|
|
39
46
|
pages: 'Settings pages',
|
|
40
|
-
|
|
41
|
-
guide: 'This setting lives in "{label}" — open it from the left navigation',
|
|
47
|
+
options: 'In-page options',
|
|
42
48
|
page: 'Page',
|
|
43
|
-
|
|
49
|
+
option: 'Option',
|
|
50
|
+
jumpFail: 'Could not navigate automatically — open it here: {path}',
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/** Inner-row slots that represent specific options inside one page. */
|
|
54
|
+
const DEEP_SLOTS = [
|
|
55
|
+
{ slot: 'settings.plugins.tab', parent: 'plugins' },
|
|
56
|
+
{ slot: 'web-ui.plugin.item', parent: 'web-ui-plugins' },
|
|
57
|
+
{ slot: 'settings.general.item', parent: 'general' },
|
|
58
|
+
]
|
|
59
|
+
|
|
46
60
|
const CSS = `
|
|
47
61
|
.sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
48
62
|
.sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
|
|
@@ -52,20 +66,24 @@ window.__ModuleLoader__.load({
|
|
|
52
66
|
.sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
53
67
|
.sss-clear { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1; cursor: pointer; }
|
|
54
68
|
.sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
55
|
-
.sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height:
|
|
69
|
+
.sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height: 320px; overflow: auto; }
|
|
56
70
|
.sss-groups { display: flex; flex-direction: column; gap: 8px; }
|
|
57
71
|
.sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
|
|
58
72
|
.sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
59
|
-
.sss-item { display: flex; align-items: center; gap:
|
|
73
|
+
.sss-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
|
60
74
|
.sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
61
|
-
.sss-item-
|
|
62
|
-
.sss-item-
|
|
75
|
+
.sss-item-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
76
|
+
.sss-item-label { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
77
|
+
.sss-item-path { font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
78
|
+
.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; }
|
|
63
79
|
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
64
80
|
.sss-guide { padding: 7px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
|
|
81
|
+
@keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
|
|
82
|
+
.sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
|
|
65
83
|
`
|
|
66
84
|
let stylesInstalled = false
|
|
67
85
|
function installStyles() {
|
|
68
|
-
if (stylesInstalled || typeof document === 'undefined') return
|
|
86
|
+
if (stylesInstalled || typeof document === 'undefined') return undefined
|
|
69
87
|
stylesInstalled = true
|
|
70
88
|
const tag = document.createElement('style')
|
|
71
89
|
tag.dataset.plugin = '@objectivex666/dsh-settings-search'
|
|
@@ -78,10 +96,183 @@ window.__ModuleLoader__.load({
|
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
const callSafe = (fn) => {
|
|
100
|
+
try { return fn() } catch { return undefined }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve a registrant-provided label option (string or thunk). */
|
|
104
|
+
const optionLabel = (raw) => {
|
|
105
|
+
const value = typeof raw === 'function' ? callSafe(raw) : raw
|
|
106
|
+
return typeof value === 'string' ? value : ''
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createModel(ctx) {
|
|
110
|
+
/** id → harvested visible title, per slot. */
|
|
111
|
+
const harvest = new Map()
|
|
112
|
+
let harvestSeq = 0
|
|
113
|
+
|
|
114
|
+
const entriesOf = (slot) => {
|
|
115
|
+
try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
|
|
116
|
+
}
|
|
117
|
+
const sortEntries = (list) => list.slice().sort((a, b) => ((a.options?.order ?? 0) - (b.options?.order ?? 0)))
|
|
118
|
+
|
|
119
|
+
/** Best-effort static label from occupant options/locale namespace. */
|
|
120
|
+
const staticLabel = (entry, slotConfig) => {
|
|
121
|
+
const o = entry.options ?? {}
|
|
122
|
+
const explicit = optionLabel(o.label)
|
|
123
|
+
if (explicit !== '') return explicit
|
|
124
|
+
if (typeof o.locale === 'string') {
|
|
125
|
+
const t = callSafe(() => ctx.locale.bind(o.locale))
|
|
126
|
+
if (typeof t === 'function') {
|
|
127
|
+
for (const key of ['tab', 'title', 'card.title', 'name']) {
|
|
128
|
+
const value = callSafe(() => t(key))
|
|
129
|
+
if (typeof value === 'string' && value !== '' && value !== key) return value
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
void slotConfig
|
|
134
|
+
return ''
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read visible titles for self-drawing occupants out of the live DOM:
|
|
139
|
+
* the shell marks every rendered inner-slot occupant with
|
|
140
|
+
* `[data-slot="<name>"]`; when DOM order matches entry order we can
|
|
141
|
+
* pair each rendered card with its registration id.
|
|
142
|
+
*/
|
|
143
|
+
const runHarvest = () => {
|
|
144
|
+
let grew = false
|
|
145
|
+
if (typeof document === 'undefined') return false
|
|
146
|
+
for (const cfg of DEEP_SLOTS) {
|
|
147
|
+
let nodes = []
|
|
148
|
+
try { nodes = [...document.querySelectorAll(`[data-slot="${cfg.slot}"]`)] } catch { continue }
|
|
149
|
+
const regs = sortEntries(entriesOf(cfg.slot))
|
|
150
|
+
if (nodes.length === 0 || nodes.length !== regs.length) continue
|
|
151
|
+
let map = harvest.get(cfg.slot)
|
|
152
|
+
if (map === undefined) { map = new Map(); harvest.set(cfg.slot, map) }
|
|
153
|
+
regs.forEach((entry, i) => {
|
|
154
|
+
const id = String(entry.options?.id ?? i)
|
|
155
|
+
if (map.has(id)) return
|
|
156
|
+
const title = titleFromNode(nodes[i])
|
|
157
|
+
if (title !== '') { map.set(id, title); grew = true }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
if (grew) harvestSeq += 1
|
|
161
|
+
return grew
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const titleFromNode = (node) => {
|
|
165
|
+
const pick = (el) => {
|
|
166
|
+
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
167
|
+
return text !== '' && text.length <= 60 ? text : ''
|
|
168
|
+
}
|
|
169
|
+
for (const sel of ['h1,h2,h3,h4,h5', '[class*="title"]', '[class*="heading"]', 'label', 'p']) {
|
|
170
|
+
const hit = node.querySelector(sel)
|
|
171
|
+
if (hit) { const text = pick(hit); if (text !== '') return text }
|
|
172
|
+
}
|
|
173
|
+
const first = node.firstElementChild
|
|
174
|
+
if (first) {
|
|
175
|
+
for (const child of first.children) { const text = pick(child); if (text !== '') return text }
|
|
176
|
+
}
|
|
177
|
+
return pick(node).split('·')[0].trim()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const readSections = () => sortEntries(entriesOf('settings.section'))
|
|
181
|
+
.filter((e) => e.options?.id !== 'settings-search')
|
|
182
|
+
.map((e) => ({
|
|
183
|
+
kind: 'page',
|
|
184
|
+
id: String(e.options?.id ?? ''),
|
|
185
|
+
order: e.options?.order ?? 0,
|
|
186
|
+
label: optionLabel(e.options?.label),
|
|
187
|
+
}))
|
|
188
|
+
.filter((e) => e.id !== '' && e.label !== '')
|
|
189
|
+
|
|
190
|
+
const readDeep = () => {
|
|
191
|
+
const sections = readSections()
|
|
192
|
+
const parentLabel = (parentId) =>
|
|
193
|
+
sections.find((s) => s.id === parentId)?.label ?? parentId
|
|
194
|
+
const out = []
|
|
195
|
+
for (const cfg of DEEP_SLOTS) {
|
|
196
|
+
const mapped = harvest.get(cfg.slot)
|
|
197
|
+
for (const entry of sortEntries(entriesOf(cfg.slot))) {
|
|
198
|
+
const id = String(entry.options?.id ?? '')
|
|
199
|
+
if (id === '') continue
|
|
200
|
+
const label = staticLabel(entry, cfg)
|
|
201
|
+
|| (mapped ? mapped.get(id) ?? '' : '')
|
|
202
|
+
if (label === '') continue // unmapped, unharvested — invisible until it renders once
|
|
203
|
+
if (out.some((e) => e.key === cfg.slot + '#' + id)) continue
|
|
204
|
+
out.push({
|
|
205
|
+
kind: 'option',
|
|
206
|
+
key: cfg.slot + '#' + id,
|
|
207
|
+
slot: cfg.slot,
|
|
208
|
+
id,
|
|
209
|
+
order: entry.options?.order ?? 0,
|
|
210
|
+
label,
|
|
211
|
+
parentId: cfg.parent,
|
|
212
|
+
parentLabel: parentLabel(cfg.parent),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out.sort((a, b) => a.parentLabel !== b.parentLabel
|
|
217
|
+
? a.parentLabel.localeCompare(b.parentLabel)
|
|
218
|
+
: a.order - b.order)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { readSections, readDeep, runHarvest, bump: () => harvestSeq }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ---------- DOM-level navigation into the settings shell ---------- */
|
|
225
|
+
|
|
226
|
+
const panelButtons = () => {
|
|
227
|
+
if (typeof document === 'undefined') return []
|
|
228
|
+
const root = document.querySelector('[role="dialog"]') ?? document.body
|
|
229
|
+
return [...root.querySelectorAll('button')]
|
|
230
|
+
}
|
|
231
|
+
const clickNavButton = (wantText) => {
|
|
232
|
+
const want = String(wantText ?? '').trim()
|
|
233
|
+
if (want === '') return false
|
|
234
|
+
const buttons = panelButtons()
|
|
235
|
+
const hit = buttons.find((b) => (b.textContent ?? '').trim() === want)
|
|
236
|
+
?? buttons.find((b) => (b.textContent ?? '').trim().startsWith(want))
|
|
237
|
+
if (!hit) return false
|
|
238
|
+
hit.click()
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
242
|
+
|
|
243
|
+
function createNavigator(ctx) {
|
|
244
|
+
/** Jump to a page result; falls back to showing a manual hint. */
|
|
245
|
+
const jumpToPage = async (item) => {
|
|
246
|
+
const ok = await Promise.resolve(clickNavButton(item.label))
|
|
247
|
+
return ok
|
|
248
|
+
}
|
|
249
|
+
/** Jump to an in-page option: open its page, select its tab, flash its row. */
|
|
250
|
+
const jumpToOption = async (item) => {
|
|
251
|
+
const opened = await Promise.resolve(clickNavButton(item.parentLabel))
|
|
252
|
+
await delay(160)
|
|
253
|
+
const label = item.label
|
|
254
|
+
const buttons = panelButtons()
|
|
255
|
+
const tab = buttons.find((b) => (b.textContent ?? '').trim() === label)
|
|
256
|
+
?? buttons.find((b) => (b.getAttribute('role') ?? '') === 'tab' && (b.textContent ?? '').includes(label))
|
|
257
|
+
if (tab) { tab.click(); await delay(120) }
|
|
258
|
+
flashOptionRow(item)
|
|
259
|
+
return opened
|
|
260
|
+
}
|
|
261
|
+
/** Highlight the rendered row card for one indexed option. */
|
|
262
|
+
const flashOptionRow = (item) => {
|
|
263
|
+
try {
|
|
264
|
+
const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
|
|
265
|
+
const target = nodes.find((n) => (n.textContent ?? '').includes(item.label)) ?? nodes[0]
|
|
266
|
+
if (!target) return
|
|
267
|
+
target.scrollIntoView({ block: 'nearest' })
|
|
268
|
+
target.classList.remove('sss-flash')
|
|
269
|
+
void target.offsetWidth // restart animation if re-flashed
|
|
270
|
+
target.classList.add('sss-flash')
|
|
271
|
+
setTimeout(() => target.classList.remove('sss-flash'), 1900)
|
|
272
|
+
} catch { /* visual nicety only */ }
|
|
273
|
+
}
|
|
274
|
+
void ctx
|
|
275
|
+
return { jumpToPage, jumpToOption }
|
|
85
276
|
}
|
|
86
277
|
|
|
87
278
|
function apply(ctx) {
|
|
@@ -92,13 +283,8 @@ window.__ModuleLoader__.load({
|
|
|
92
283
|
return () => { if (typeof remove === 'function') remove() }
|
|
93
284
|
}, 'settings-search: styles')
|
|
94
285
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
.map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
|
|
98
|
-
.sort((a, b) => a.order - b.order)
|
|
99
|
-
const readRows = () => ctx.slots.entries('settings.general.item')
|
|
100
|
-
.map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
|
|
101
|
-
.sort((a, b) => a.order - b.order)
|
|
286
|
+
const model = createModel(ctx)
|
|
287
|
+
const navigator = createNavigator(ctx)
|
|
102
288
|
|
|
103
289
|
const SearchIcon = React.createElement('svg', {
|
|
104
290
|
className: 'sss-icon',
|
|
@@ -121,36 +307,74 @@ window.__ModuleLoader__.load({
|
|
|
121
307
|
function SettingsHeaderSearch() {
|
|
122
308
|
const [query, setQuery] = React.useState('')
|
|
123
309
|
const [open, setOpen] = React.useState(false)
|
|
124
|
-
const [
|
|
125
|
-
const [
|
|
126
|
-
|
|
310
|
+
const [tick, setTick] = React.useState(0)
|
|
311
|
+
const [, forceTick] = React.useReducer((x) => x + 1, 0)
|
|
312
|
+
void tick
|
|
313
|
+
|
|
127
314
|
React.useEffect(() => {
|
|
128
|
-
const
|
|
129
|
-
setSections(readSections())
|
|
130
|
-
setRows(readRows())
|
|
131
|
-
}
|
|
315
|
+
const watched = ['settings.section', ...DEEP_SLOTS.map((c) => c.slot)]
|
|
132
316
|
const offs = [
|
|
133
|
-
ctx.slots.subscribe(
|
|
134
|
-
ctx.
|
|
135
|
-
ctx.locale.subscribe(refresh),
|
|
317
|
+
...watched.map((slot) => ctx.slots.subscribe(slot, forceTick)),
|
|
318
|
+
ctx.locale.subscribe(forceTick),
|
|
136
319
|
]
|
|
137
|
-
|
|
320
|
+
// Grow the index whenever the panel body re-renders (visits new pages).
|
|
321
|
+
const observer = typeof MutationObserver === 'function'
|
|
322
|
+
? new MutationObserver(() => {
|
|
323
|
+
if (model.runHarvest()) setTick((x) => x + 1)
|
|
324
|
+
})
|
|
325
|
+
: null
|
|
326
|
+
observer?.observe(document.body, { childList: true, subtree: true })
|
|
327
|
+
const warm = setTimeout(() => { model.runHarvest(); setTick((x) => x + 1) }, 800)
|
|
328
|
+
return () => {
|
|
329
|
+
for (const off of offs) off()
|
|
330
|
+
observer?.disconnect()
|
|
331
|
+
clearTimeout(warm)
|
|
332
|
+
}
|
|
138
333
|
}, [])
|
|
139
334
|
|
|
140
335
|
const q = query.trim().toLowerCase()
|
|
141
336
|
const tokens = q.split(/\s+/).filter(Boolean)
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
337
|
+
const matches = (haystack) => tokens.every((tok) => haystack.includes(tok))
|
|
338
|
+
|
|
339
|
+
const sections = model.readSections()
|
|
340
|
+
const deeps = q !== '' ? model.readDeep() : []
|
|
341
|
+
const shownPages = q !== ''
|
|
342
|
+
? sections.filter((s) => matches(`${s.label} ${s.id}`.toLowerCase()))
|
|
343
|
+
: []
|
|
344
|
+
const shownOptions = q !== ''
|
|
345
|
+
? deeps.filter((o) => matches(`${o.label} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
|
|
346
|
+
: []
|
|
347
|
+
const none = q !== '' && shownPages.length === 0 && shownOptions.length === 0
|
|
348
|
+
|
|
349
|
+
const [hint, setHint] = React.useState(null)
|
|
350
|
+
const pick = (item) => {
|
|
351
|
+
setOpen(false)
|
|
352
|
+
const jumped = item.kind === 'page'
|
|
353
|
+
? navigator.jumpToPage(item)
|
|
354
|
+
: navigator.jumpToOption(item)
|
|
355
|
+
Promise.resolve(jumped).then((ok) => {
|
|
356
|
+
if (ok === false) setHint(item.kind === 'page' ? item.label : `${item.parentLabel} › ${item.label}`)
|
|
357
|
+
})
|
|
358
|
+
}
|
|
146
359
|
|
|
147
|
-
const
|
|
148
|
-
key: item.id,
|
|
360
|
+
const renderPage = (item) => React.createElement('li', {
|
|
361
|
+
key: 'page:' + item.id,
|
|
149
362
|
className: 'sss-item',
|
|
150
|
-
onMouseDown: () =>
|
|
363
|
+
onMouseDown: () => pick(item),
|
|
151
364
|
}, [
|
|
152
|
-
React.createElement('span', { className: 'sss-item-label' }, item.label
|
|
153
|
-
React.createElement('span', { className: 'sss-item-
|
|
365
|
+
React.createElement('span', { className: 'sss-item-label' }, item.label),
|
|
366
|
+
React.createElement('span', { className: 'sss-item-kind' }, t('page')),
|
|
367
|
+
])
|
|
368
|
+
const renderOption = (item) => React.createElement('li', {
|
|
369
|
+
key: item.key,
|
|
370
|
+
className: 'sss-item',
|
|
371
|
+
onMouseDown: () => pick(item),
|
|
372
|
+
}, [
|
|
373
|
+
React.createElement('span', { className: 'sss-item-col' }, [
|
|
374
|
+
React.createElement('span', { className: 'sss-item-label' }, item.label),
|
|
375
|
+
React.createElement('span', { className: 'sss-item-path' }, item.parentLabel),
|
|
376
|
+
]),
|
|
377
|
+
React.createElement('span', { className: 'sss-item-kind' }, t('option')),
|
|
154
378
|
])
|
|
155
379
|
|
|
156
380
|
return React.createElement('div', { className: 'sss-wrap' }, [
|
|
@@ -162,32 +386,32 @@ window.__ModuleLoader__.load({
|
|
|
162
386
|
value: query,
|
|
163
387
|
placeholder: t('placeholder'),
|
|
164
388
|
spellCheck: false,
|
|
165
|
-
onChange: (e) => { setQuery(e.target.value); setOpen(true) },
|
|
389
|
+
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
|
|
166
390
|
onFocus: () => setOpen(true),
|
|
167
391
|
onBlur: () => setOpen(false),
|
|
168
392
|
}),
|
|
169
393
|
query !== '' ? React.createElement('button', {
|
|
170
394
|
className: 'sss-clear',
|
|
171
395
|
type: 'button',
|
|
172
|
-
onClick: () => { setQuery('');
|
|
396
|
+
onClick: () => { setQuery(''); setHint(null); setOpen(false) },
|
|
173
397
|
}, '✕') : null,
|
|
174
398
|
]),
|
|
175
|
-
open &&
|
|
399
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop' }, [
|
|
176
400
|
none
|
|
177
401
|
? React.createElement('p', { className: 'sss-empty' }, t('empty'))
|
|
178
402
|
: React.createElement('div', { className: 'sss-groups' }, [
|
|
179
|
-
|
|
403
|
+
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
180
404
|
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
181
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
405
|
+
React.createElement('ul', { className: 'sss-list' }, shownPages.map(renderPage)),
|
|
182
406
|
]) : null,
|
|
183
|
-
|
|
184
|
-
React.createElement('div', { className: 'sss-group-title' }, t('
|
|
185
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
407
|
+
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
408
|
+
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
409
|
+
React.createElement('ul', { className: 'sss-list' }, shownOptions.map(renderOption)),
|
|
186
410
|
]) : null,
|
|
187
411
|
]),
|
|
188
412
|
]) : null,
|
|
189
|
-
|
|
190
|
-
? React.createElement('div', { className: 'sss-guide' }, t('
|
|
413
|
+
hint !== null
|
|
414
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
191
415
|
: null,
|
|
192
416
|
])
|
|
193
417
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectivex666/dsh-settings-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|