@ryuu-64/dsh-find-all 0.1.4 → 0.1.6
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/CHANGELOG.md +143 -119
- package/LICENSE +22 -22
- package/README.fork-origin.md +48 -48
- package/README.md +38 -7
- package/docs/compatibility.md +68 -0
- package/lib/client.js +584 -163
- package/package.json +83 -76
package/lib/client.js
CHANGED
|
@@ -21,19 +21,19 @@
|
|
|
21
21
|
// * In whole-conversation mode a search pages the older history in through
|
|
22
22
|
// the session service (ctx.sessions.binding(id).session.loadOlder()),
|
|
23
23
|
// re-running the match pass after every page — so the count and the
|
|
24
|
-
// highlight set
|
|
24
|
+
// highlight set cover the history the host currently makes available.
|
|
25
25
|
// * Progress is visible ("正在加载更早的历史… 已 3 页"), clickable to stop,
|
|
26
26
|
// capped at MAX_PAGES, and it stops by itself when the history stops
|
|
27
|
-
// growing
|
|
27
|
+
// growing. Host termination does not establish historical completeness.
|
|
28
28
|
//
|
|
29
29
|
// How it works without fighting React (unchanged from the fork):
|
|
30
30
|
// * Matches are collected as DOM Range objects (start/end offsets inside
|
|
31
|
-
// text nodes)
|
|
31
|
+
// text nodes) from block-local text snapshots, including shadow roots.
|
|
32
32
|
// * Highlights use the CSS Custom Highlight API (CSS.highlights + Highlight)
|
|
33
33
|
// which paints ranges WITHOUT mutating the DOM — React keeps full
|
|
34
34
|
// ownership of the tree. Chromium/Electron support this since 105.
|
|
35
|
-
// *
|
|
36
|
-
// window.find()
|
|
35
|
+
// * Without CSS Highlights, navigation still scrolls only scoped ranges.
|
|
36
|
+
// Browser-wide window.find() is deliberately never used.
|
|
37
37
|
//
|
|
38
38
|
// Keyboard:
|
|
39
39
|
// Cmd/Ctrl+F open (prefilled with the current selection)
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
window.__ModuleLoader__.load({
|
|
45
45
|
id: "@ryuu-64/dsh-find-all",
|
|
46
|
-
factory: () => {
|
|
46
|
+
factory: (require) => {
|
|
47
47
|
var module = { exports: {} };
|
|
48
48
|
var exports = module.exports;
|
|
49
49
|
|
|
@@ -59,6 +59,16 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
/** How often the open bar notices that the user switched session. */
|
|
60
60
|
var SESSION_POLL_MS = 600;
|
|
61
61
|
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1 };
|
|
62
|
+
// Structural boundaries, not a computed-style visibility policy. In
|
|
63
|
+
// particular, host message divs and code-content/toolbars stay separate.
|
|
64
|
+
var BLOCK_TAGS = {
|
|
65
|
+
ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DD: 1, DETAILS: 1,
|
|
66
|
+
DIALOG: 1, DIV: 1, DL: 1, DT: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1,
|
|
67
|
+
FOOTER: 1, FORM: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1,
|
|
68
|
+
HEADER: 1, HR: 1, LI: 1, MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1,
|
|
69
|
+
SECTION: 1, SUMMARY: 1, TABLE: 1, TBODY: 1, TD: 1, TFOOT: 1,
|
|
70
|
+
TH: 1, THEAD: 1, TR: 1, UL: 1
|
|
71
|
+
};
|
|
62
72
|
|
|
63
73
|
var ZH = typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "");
|
|
64
74
|
var L = ZH ? {
|
|
@@ -72,11 +82,14 @@ window.__ModuleLoader__.load({
|
|
|
72
82
|
scopeToPage: "搜索范围:整段会话。点击改为只搜当前页面",
|
|
73
83
|
scopeToWhole: "搜索范围:仅当前页面。点击改为搜索整段会话",
|
|
74
84
|
loading: "正在加载更早的历史… 已 {n} 页",
|
|
75
|
-
|
|
76
|
-
capped: "
|
|
77
|
-
stalled: "没有更早的历史了",
|
|
85
|
+
available: "已搜索当前可用历史;宿主未提供更早页,无法确认历史完整性",
|
|
86
|
+
capped: "历史尚未搜完:已到加载上限({n} 页)",
|
|
78
87
|
stopTip: "点击停止加载",
|
|
79
|
-
noService: "
|
|
88
|
+
noService: "会话接口不可用,仅保留已显示的部分结果",
|
|
89
|
+
noTarget: "请先选择可见的会话,再打开查找",
|
|
90
|
+
noRoot: "当前会话的搜索范围不可用",
|
|
91
|
+
incomplete: "历史尚未搜完,保留部分结果",
|
|
92
|
+
find: "查找会话"
|
|
80
93
|
} : {
|
|
81
94
|
placeholder: "Find in conversation…",
|
|
82
95
|
prev: "Previous match",
|
|
@@ -88,31 +101,42 @@ window.__ModuleLoader__.load({
|
|
|
88
101
|
scopeToPage: "Scope: the whole conversation. Click to search only the rendered page",
|
|
89
102
|
scopeToWhole: "Scope: the rendered page only. Click to search the whole conversation",
|
|
90
103
|
loading: "Loading earlier history… {n} pages",
|
|
91
|
-
|
|
92
|
-
capped: "
|
|
93
|
-
stalled: "No earlier history",
|
|
104
|
+
available: "Searched currently available history; the host offers no earlier pages, so completeness cannot be confirmed",
|
|
105
|
+
capped: "History search incomplete: page cap reached ({n})",
|
|
94
106
|
stopTip: "Click to stop loading",
|
|
95
|
-
noService: "Session service unavailable
|
|
107
|
+
noService: "Session service unavailable; rendered results only",
|
|
108
|
+
noTarget: "Select a visible conversation before opening find",
|
|
109
|
+
noRoot: "Search scope unavailable for this conversation",
|
|
110
|
+
incomplete: "History search incomplete; partial results retained",
|
|
111
|
+
find: "Find in conversation"
|
|
96
112
|
};
|
|
97
113
|
|
|
98
114
|
//#region pure helpers (exported for tests)
|
|
99
115
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
116
|
+
// Literal, locale-independent Unicode simple folding on the ORIGINAL
|
|
117
|
+
// text. RegExp returns UTF-16 offsets, exactly as DOM Range requires.
|
|
118
|
+
// No normalization, full case folding, or transformed-string offsets.
|
|
119
|
+
function findMatches(text, query) {
|
|
103
120
|
if (!query) return [];
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
// This is the entire pattern (not interpolated into another regex).
|
|
122
|
+
// Escape regex syntax only; '-' and '/' are literal in this context.
|
|
123
|
+
// Do not require the newer RegExp.escape() API in supported hosts.
|
|
124
|
+
var literal = String(query).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
125
|
+
if (!literal) return [];
|
|
126
|
+
var haystack = String(text);
|
|
127
|
+
var matcher = new RegExp(literal, "giu");
|
|
106
128
|
var out = [];
|
|
107
|
-
var
|
|
108
|
-
while (
|
|
109
|
-
out.push(
|
|
110
|
-
if (out.length >= MAX_MATCHES) break;
|
|
111
|
-
i = haystack.indexOf(needle, i + needle.length);
|
|
129
|
+
var match;
|
|
130
|
+
while (out.length < MAX_MATCHES && (match = matcher.exec(haystack)) !== null) {
|
|
131
|
+
out.push({ start: match.index, end: match.index + match[0].length });
|
|
112
132
|
}
|
|
113
133
|
return out;
|
|
114
134
|
}
|
|
115
135
|
|
|
136
|
+
function findIndices(text, query) {
|
|
137
|
+
return findMatches(text, query).map(function (match) { return match.start; });
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
// Keyboard routing, isolated so tests can drive it with spies.
|
|
117
141
|
function createHandlers(ops) {
|
|
118
142
|
return function onKeydown(event) {
|
|
@@ -121,7 +145,7 @@ window.__ModuleLoader__.load({
|
|
|
121
145
|
if (meta && (key === "f" || key === "F")) {
|
|
122
146
|
event.preventDefault();
|
|
123
147
|
event.stopPropagation();
|
|
124
|
-
ops.open();
|
|
148
|
+
ops.open(event.target);
|
|
125
149
|
return;
|
|
126
150
|
}
|
|
127
151
|
if (!ops.isOpen()) return;
|
|
@@ -143,41 +167,44 @@ window.__ModuleLoader__.load({
|
|
|
143
167
|
};
|
|
144
168
|
}
|
|
145
169
|
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* Every dependency is injected, so the loop is testable without a DOM or
|
|
150
|
-
* a host: the caller supplies how to read `hasMore`, how to load a page,
|
|
151
|
-
* how to measure the rendered history, and how to wait for that measure
|
|
152
|
-
* to change.
|
|
153
|
-
*
|
|
154
|
-
* @param opts - { maxPages, isCancelled, hasMore, loadOlder, signature,
|
|
155
|
-
* waitChange, onProgress }.
|
|
156
|
-
* `hasMore` returns true/false/null (null = the host cannot say, so the
|
|
157
|
-
* pager keeps going until a page changes nothing).
|
|
158
|
-
* @returns { pages, reason } where reason is one of done | cancelled |
|
|
159
|
-
* capped | stalled | error.
|
|
170
|
+
/** Page only while the public host state is ready; false is not completeness.
|
|
171
|
+
* Dependencies supply cancellable, bounded readiness/load/DOM waits.
|
|
172
|
+
* @returns { pages, reason } with done, cancelled, capped, stalled, unknown or error.
|
|
160
173
|
*/
|
|
161
174
|
async function runPageIn(opts) {
|
|
162
175
|
var pages = 0;
|
|
163
|
-
|
|
176
|
+
function result(reason) { return { pages: pages, reason: reason }; }
|
|
164
177
|
for (;;) {
|
|
165
|
-
if (opts.isCancelled())
|
|
166
|
-
|
|
167
|
-
if (
|
|
178
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
179
|
+
var ready = await opts.waitReady();
|
|
180
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
181
|
+
if (ready.reason) return result(ready.reason);
|
|
182
|
+
if (ready.hasMore === false) return result("done");
|
|
183
|
+
if (pages >= opts.maxPages) return result("capped");
|
|
168
184
|
var before = opts.signature();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
var loaded;
|
|
186
|
+
try { loaded = await opts.loadOlder(); }
|
|
187
|
+
catch (error) { loaded = { reason: "error" }; }
|
|
188
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
189
|
+
ready = await opts.waitReady(!!(loaded && loaded.reason));
|
|
190
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
191
|
+
if (loaded && loaded.reason) return result(loaded.reason);
|
|
192
|
+
if (ready.reason) return result(ready.reason);
|
|
193
|
+
if (loaded && loaded.deferred) continue;
|
|
194
|
+
if (ready.hasMore !== false) await opts.waitChange(before);
|
|
195
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
196
|
+
ready = await opts.waitReady();
|
|
197
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
198
|
+
if (ready.reason) return result(ready.reason);
|
|
199
|
+
var changed = opts.signature() !== before;
|
|
200
|
+
if (changed) {
|
|
201
|
+
pages += 1;
|
|
202
|
+
if (opts.onProgress) opts.onProgress(pages);
|
|
174
203
|
}
|
|
175
|
-
if (opts.isCancelled())
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
if (opts.onProgress) opts.onProgress(pages);
|
|
204
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
205
|
+
if (ready.hasMore === false) return result("done");
|
|
206
|
+
if (!changed) return result("stalled");
|
|
179
207
|
}
|
|
180
|
-
return { pages: pages, reason: reason };
|
|
181
208
|
}
|
|
182
209
|
//#endregion
|
|
183
210
|
|
|
@@ -213,8 +240,8 @@ window.__ModuleLoader__.load({
|
|
|
213
240
|
return typeof CSS !== "undefined" && CSS.highlights && typeof Highlight === "function";
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
243
|
+
// Keep the existing pager signature collector unchanged. Matching below
|
|
244
|
+
// uses separate block snapshots; page-settlement semantics are out of scope.
|
|
218
245
|
function collectTextNodes(root, into) {
|
|
219
246
|
if (!root) return into;
|
|
220
247
|
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
@@ -239,17 +266,62 @@ window.__ModuleLoader__.load({
|
|
|
239
266
|
return into;
|
|
240
267
|
}
|
|
241
268
|
|
|
242
|
-
|
|
269
|
+
// Join inline Text nodes only within one structural block and DOM root.
|
|
270
|
+
// These snapshots live for one scan. Preserve every original code unit,
|
|
271
|
+
// including whitespace-only nodes and code newlines; never trim or pad.
|
|
272
|
+
function collectTextBlocks(root) {
|
|
273
|
+
var blocks = [];
|
|
274
|
+
var text = "";
|
|
275
|
+
var segments = [];
|
|
276
|
+
function flush() {
|
|
277
|
+
if (segments.length) blocks.push({ text: text, segments: segments });
|
|
278
|
+
text = "";
|
|
279
|
+
segments = [];
|
|
280
|
+
}
|
|
281
|
+
function visit(node) {
|
|
282
|
+
if (node.nodeType === 3) {
|
|
283
|
+
if (node.data.length) {
|
|
284
|
+
segments.push({ node: node, start: text.length, end: text.length + node.data.length });
|
|
285
|
+
text += node.data;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (node.nodeType !== 1 && node.nodeType !== 11) return;
|
|
290
|
+
if (SKIP_TAGS[node.tagName] || node.id === BAR_ID || node.tagName === "BR") {
|
|
291
|
+
// Excluded subtrees and br are hard boundaries, not empty text.
|
|
292
|
+
flush();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
var boundary = BLOCK_TAGS[node.tagName] || node.nodeType === 11 || node.shadowRoot;
|
|
296
|
+
if (boundary) flush();
|
|
297
|
+
for (var child = node.firstChild; child; child = child.nextSibling) visit(child);
|
|
298
|
+
if (boundary) flush();
|
|
299
|
+
// No Range may span a shadow root and its surrounding light DOM.
|
|
300
|
+
if (node.shadowRoot) visit(node.shadowRoot);
|
|
301
|
+
}
|
|
302
|
+
visit(root);
|
|
303
|
+
flush();
|
|
304
|
+
return blocks;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function collectRanges(query, root) {
|
|
243
308
|
var ranges = [];
|
|
244
|
-
if (!query || typeof document === "undefined"
|
|
245
|
-
var
|
|
246
|
-
for (var i = 0; i <
|
|
247
|
-
var
|
|
248
|
-
var
|
|
249
|
-
|
|
309
|
+
if (!query || !root || typeof document === "undefined") return ranges;
|
|
310
|
+
var blocks = collectTextBlocks(root);
|
|
311
|
+
for (var i = 0; i < blocks.length && ranges.length < MAX_MATCHES; i++) {
|
|
312
|
+
var block = blocks[i];
|
|
313
|
+
var matches = findMatches(block.text, query);
|
|
314
|
+
var start = 0, end = 0;
|
|
315
|
+
for (var j = 0; j < matches.length && ranges.length < MAX_MATCHES; j++) {
|
|
316
|
+
var match = matches[j];
|
|
317
|
+
// At a node junction, start at the following node and end at
|
|
318
|
+
// the preceding node. Non-overlapping matches move forwards.
|
|
319
|
+
while (match.start >= block.segments[start].end) start++;
|
|
320
|
+
while (match.end > block.segments[end].end) end++;
|
|
321
|
+
var first = block.segments[start], last = block.segments[end];
|
|
250
322
|
var range = document.createRange();
|
|
251
|
-
range.setStart(node,
|
|
252
|
-
range.setEnd(node,
|
|
323
|
+
range.setStart(first.node, match.start - first.start);
|
|
324
|
+
range.setEnd(last.node, match.end - last.start);
|
|
253
325
|
ranges.push(range);
|
|
254
326
|
}
|
|
255
327
|
}
|
|
@@ -270,56 +342,232 @@ window.__ModuleLoader__.load({
|
|
|
270
342
|
}
|
|
271
343
|
}
|
|
272
344
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
345
|
+
//#region host adapter
|
|
346
|
+
// Official session-scoped header utility props supply identity. DOM selectors
|
|
347
|
+
// are intentionally isolated here: see docs/compatibility.md for tag sources.
|
|
348
|
+
// No global current-session state and no page-wide search fallback.
|
|
349
|
+
function visible(element) {
|
|
350
|
+
if (!element || !element.isConnected || !element.getClientRects().length) return false;
|
|
351
|
+
for (var node = element; node; node = node.parentElement) {
|
|
352
|
+
if (node.hidden || node.getAttribute("aria-hidden") === "true") return false;
|
|
353
|
+
var css = window.getComputedStyle(node);
|
|
354
|
+
if (css.display === "none" || css.visibility === "hidden" || css.visibility === "collapse" || css.opacity === "0") return false;
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function conversationPanel(element) {
|
|
360
|
+
// The host composer has its own unrelated data-phase. A conversation
|
|
361
|
+
// root additionally owns the scrollport (official startup fixture).
|
|
362
|
+
for (var node = element; node && node !== document.body; node = node.parentElement) {
|
|
363
|
+
if (!node.hasAttribute || !node.hasAttribute("data-phase")) continue;
|
|
364
|
+
var phase = node.getAttribute("data-phase");
|
|
365
|
+
// Preserve even a broken declared panel boundary; never walk through
|
|
366
|
+
// it to a different ancestor conversation because its scroll is absent.
|
|
367
|
+
if (phase === "active" || phase === "hero" || phase === "settling" || node.querySelector('[data-conversation-scroll]')) return node;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
277
370
|
}
|
|
278
371
|
|
|
279
|
-
|
|
280
|
-
|
|
372
|
+
function resolveScope(anchor, sessionId) {
|
|
373
|
+
if (!anchor || typeof sessionId !== "string" || !sessionId || !visible(anchor)) return null;
|
|
374
|
+
var panel = conversationPanel(anchor);
|
|
375
|
+
if (!panel || panel.getAttribute("data-phase") !== "active" || panel === document.documentElement) return null;
|
|
376
|
+
var anchors = panel.querySelectorAll('[data-find-all-session]');
|
|
377
|
+
if (anchors.length !== 1 || anchors[0] !== anchor) return null;
|
|
378
|
+
var scrolls = panel.querySelectorAll('[data-conversation-scroll]');
|
|
379
|
+
if (scrolls.length !== 1) return null;
|
|
380
|
+
var content = scrolls[0].closest('[data-conversation-content]');
|
|
381
|
+
// 0.1.6-alpha.2 has the content wrapper but no identity attribute.
|
|
382
|
+
// Identity still comes from the unique session-scoped header anchor.
|
|
383
|
+
if (content && content.hasAttribute('data-conversation-session') && content.getAttribute('data-conversation-session') !== sessionId) return null;
|
|
384
|
+
var flows = scrolls[0].querySelectorAll('[data-chat-flow]');
|
|
385
|
+
if (flows.length !== 1 || !visible(flows[0])) return null;
|
|
386
|
+
return { panel: panel, root: flows[0] };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createHostAdapter(ctx, changed) {
|
|
390
|
+
var entries = new Map();
|
|
391
|
+
var scopeUnavailable = false;
|
|
392
|
+
// Only a never-selected adapter may discover an initial neutral context.
|
|
393
|
+
// Explicit rejection or loss of a selection must never select a substitute.
|
|
394
|
+
var selectionState = "pristine";
|
|
395
|
+
function candidates() {
|
|
396
|
+
var out = [];
|
|
397
|
+
entries.forEach(function (id, anchor) {
|
|
398
|
+
// Keep unsupported/invisible instances in the identity roster. Dropping
|
|
399
|
+
// them before routing could silently choose a different valid session.
|
|
400
|
+
out.push({ id: id, anchor: anchor, panel: conversationPanel(anchor), scope: resolveScope(anchor, id) });
|
|
401
|
+
});
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
function choose(entry) {
|
|
405
|
+
if (!entry) return null;
|
|
406
|
+
if (!entry.scope) { selectionState = "rejected"; scopeUnavailable = true; return null; }
|
|
407
|
+
selectionState = "selected";
|
|
408
|
+
return { id: entry.id, anchor: entry.anchor, panel: entry.scope.panel, root: entry.scope.root };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function visibleEntry(entry) {
|
|
412
|
+
var container = entry.panel || entry.anchor.closest('[data-phase], [data-conversation-content]');
|
|
413
|
+
return visible(entry.anchor) || visible(container);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function discoverInitial(all) {
|
|
417
|
+
var shown = all.filter(visibleEntry);
|
|
418
|
+
var nodes = Array.prototype.filter.call(document.querySelectorAll('[data-phase], [data-conversation-content], [data-conversation-scroll], [data-chat-flow]'), visible);
|
|
419
|
+
scopeUnavailable = false;
|
|
420
|
+
if (shown.length === 0) {
|
|
421
|
+
// An empty resident Hero is not a session choice. A known active
|
|
422
|
+
// or identified but unregistered view is unavailable, not pristine.
|
|
423
|
+
if (nodes.some(function (node) { return node.getAttribute('data-phase') === 'active' || node.getAttribute('data-content-phase') === 'active' || node.hasAttribute('data-conversation-session') || node.hasAttribute('data-chat-flow'); })) {
|
|
424
|
+
selectionState = "rejected"; scopeUnavailable = true;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
if (shown.length !== 1) { selectionState = "rejected"; return null; }
|
|
429
|
+
var entry = shown[0], scope = entry.scope;
|
|
430
|
+
if (!scope) return choose(entry);
|
|
431
|
+
var content = scope.root.closest('[data-conversation-content]');
|
|
432
|
+
var scroll = scope.root.closest('[data-conversation-scroll]');
|
|
433
|
+
// One main panel and its own content/scroll/flow count as one view.
|
|
434
|
+
// Any other visible known container, including a headerless embedded
|
|
435
|
+
// sidebar, makes neutral discovery ambiguous even if unsupported.
|
|
436
|
+
var other = nodes.some(function (node) {
|
|
437
|
+
if (node.hasAttribute('data-conversation-content') && node !== content) return true;
|
|
438
|
+
if (node.hasAttribute('data-conversation-scroll') && node !== scroll) return true;
|
|
439
|
+
if (node.hasAttribute('data-chat-flow') && node !== scope.root) return true;
|
|
440
|
+
return node.hasAttribute('data-phase') && conversationPanel(node) === node && node !== scope.panel;
|
|
441
|
+
});
|
|
442
|
+
if (other) { selectionState = "rejected"; return null; }
|
|
443
|
+
return choose(entry);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function refresh(entry) {
|
|
447
|
+
if (!entry) return null;
|
|
448
|
+
// Passive checks and repeated shortcuts may only retain this exact
|
|
449
|
+
// registered instance and identity. They never discover a substitute.
|
|
450
|
+
if (entries.get(entry.anchor) !== entry.id) { selectionState = "rejected"; scopeUnavailable = true; return null; }
|
|
451
|
+
scopeUnavailable = false;
|
|
452
|
+
return choose({ id: entry.id, anchor: entry.anchor, scope: resolveScope(entry.anchor, entry.id) });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
mount: function (anchor, id) {
|
|
457
|
+
entries.set(anchor, id);
|
|
458
|
+
changed();
|
|
459
|
+
return function () { entries.delete(anchor); changed(); };
|
|
460
|
+
},
|
|
461
|
+
refresh: refresh,
|
|
462
|
+
select: function (target, previous, retainNeutral) {
|
|
463
|
+
var all = candidates();
|
|
464
|
+
if (!target || !target.nodeType) target = document.activeElement;
|
|
465
|
+
if (target && state.bar && state.bar.contains(target)) return refresh(previous);
|
|
466
|
+
if (retainNeutral && (target === document.body || target === document.documentElement)) {
|
|
467
|
+
if (previous || selectionState !== "pristine") return refresh(previous);
|
|
468
|
+
return discoverInitial(all);
|
|
469
|
+
}
|
|
470
|
+
scopeUnavailable = false;
|
|
471
|
+
var direct = all.filter(function (entry) { return target && entry.anchor.contains(target); });
|
|
472
|
+
if (direct.length === 1) return choose(direct[0]);
|
|
473
|
+
var content = target && target.closest ? target.closest('[data-conversation-content]') : null;
|
|
474
|
+
var panel = conversationPanel(target) || content;
|
|
475
|
+
var hit = all.filter(function (entry) { return panel && entry.panel === panel; });
|
|
476
|
+
if (hit.length === 1) {
|
|
477
|
+
if (content && hit[0].scope && hit[0].scope.root.closest('[data-conversation-content]') !== content) {
|
|
478
|
+
selectionState = "rejected"; scopeUnavailable = true; return null;
|
|
479
|
+
}
|
|
480
|
+
return choose(hit[0]);
|
|
481
|
+
}
|
|
482
|
+
if (panel) {
|
|
483
|
+
// Before any session is selected, focus in the resident unbound
|
|
484
|
+
// Hero/settling composer is not a rejected conversation choice.
|
|
485
|
+
var phase = panel.getAttribute('data-phase') || panel.getAttribute('data-content-phase');
|
|
486
|
+
var unbound = !panel.hasAttribute('data-conversation-session') && !panel.querySelector('[data-conversation-session]');
|
|
487
|
+
if (selectionState === "pristine" && !previous && unbound && !all.some(visibleEntry) && (phase === "hero" || phase === "settling")) return null;
|
|
488
|
+
selectionState = "rejected"; scopeUnavailable = true; return null;
|
|
489
|
+
}
|
|
490
|
+
if (previous) selectionState = "rejected";
|
|
491
|
+
return null;
|
|
492
|
+
},
|
|
493
|
+
scopeUnavailable: function () { return scopeUnavailable; },
|
|
494
|
+
valid: function (entry) {
|
|
495
|
+
if (!entry || entries.get(entry.anchor) !== entry.id) return false;
|
|
496
|
+
var scope = resolveScope(entry.anchor, entry.id);
|
|
497
|
+
return !!scope && scope.root === entry.root;
|
|
498
|
+
},
|
|
499
|
+
face: function (entry) {
|
|
500
|
+
try {
|
|
501
|
+
if (!this.valid(entry) || !ctx.sessions || typeof ctx.sessions.binding !== "function") return null;
|
|
502
|
+
var binding = ctx.sessions.binding(entry.id);
|
|
503
|
+
var face = binding && binding.session;
|
|
504
|
+
return face && typeof face.loadOlder === "function" ? face : null;
|
|
505
|
+
} catch (error) { return null; }
|
|
506
|
+
},
|
|
507
|
+
clear: function () { entries.clear(); }
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function domSignature(root) {
|
|
512
|
+
return root ? collectTextNodes(root, []).length : 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function waitForPage(check, isCancelled) {
|
|
281
516
|
return new Promise(function (resolve) {
|
|
282
517
|
var started = Date.now();
|
|
518
|
+
function finish(value) {
|
|
519
|
+
clearTimeout(state.settleTimer);
|
|
520
|
+
state.settleTimer = 0;
|
|
521
|
+
state.settleCancel = null;
|
|
522
|
+
resolve(value);
|
|
523
|
+
}
|
|
524
|
+
state.settleCancel = function () { finish({ reason: "cancelled" }); };
|
|
283
525
|
(function poll() {
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
526
|
+
if (isCancelled()) { finish({ reason: "cancelled" }); return; }
|
|
527
|
+
var value = check();
|
|
528
|
+
if (value) { finish(value); return; }
|
|
529
|
+
if (Date.now() - started >= PAGE_SETTLE_MS) { finish({ reason: "stalled" }); return; }
|
|
530
|
+
state.settleTimer = setTimeout(poll, PAGE_POLL_MS);
|
|
287
531
|
})();
|
|
288
532
|
});
|
|
289
533
|
}
|
|
290
534
|
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
// The `sessions` client service is provided by
|
|
294
|
-
// @deepseek-ai/dsh-api-session-controller; the per-session face it binds
|
|
295
|
-
// exposes loadOlder() plus a snapshot carrying hasMore/loadingOlder.
|
|
296
|
-
// Everything here is defensive: a host that changes shape must degrade to
|
|
297
|
-
// page-only search, never break the bar.
|
|
298
|
-
|
|
299
|
-
function resolveFace(ctx) {
|
|
535
|
+
function facePageState(face) {
|
|
300
536
|
try {
|
|
301
|
-
var
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
} catch (error) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
537
|
+
var snapshot = typeof face.getSnapshot === "function" && face.getSnapshot();
|
|
538
|
+
if (!snapshot) return { reason: "unknown" };
|
|
539
|
+
if (snapshot.openError != null || snapshot.openState === "error") return { reason: "error" };
|
|
540
|
+
if (snapshot.openState === "cold" || snapshot.openState === "loading") return null;
|
|
541
|
+
if (snapshot.openState !== "open" || snapshot.openError !== null) return { reason: "unknown" };
|
|
542
|
+
if (snapshot.loadingOlder === true) return null;
|
|
543
|
+
if (snapshot.loadingOlder !== false) return { reason: "unknown" };
|
|
544
|
+
return { hasMore: typeof snapshot.hasMore === "boolean" ? snapshot.hasMore : null };
|
|
545
|
+
} catch (error) { return { reason: "unknown" }; }
|
|
313
546
|
}
|
|
314
547
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
548
|
+
// A cancelled UI task cannot abort SessionFace.loadOlder(). Keep its record
|
|
549
|
+
// until settlement so a later query/enable never starts a concurrent request.
|
|
550
|
+
var pendingLoads = new WeakMap();
|
|
551
|
+
async function loadPage(face, isCancelled) {
|
|
552
|
+
var status = facePageState(face);
|
|
553
|
+
if (!status || status.reason || status.hasMore === false || pendingLoads.has(face)) return { deferred: true };
|
|
554
|
+
var pending = { done: false, reason: null };
|
|
555
|
+
pendingLoads.set(face, pending);
|
|
556
|
+
var promise;
|
|
557
|
+
try { promise = Promise.resolve(face.loadOlder()); }
|
|
558
|
+
catch (error) { promise = Promise.reject(error); }
|
|
559
|
+
promise.then(function () {
|
|
560
|
+
pending.done = true;
|
|
561
|
+
pendingLoads.delete(face);
|
|
562
|
+
}, function () {
|
|
563
|
+
pending.done = true;
|
|
564
|
+
pending.reason = "error";
|
|
565
|
+
pendingLoads.delete(face);
|
|
566
|
+
});
|
|
567
|
+
await Promise.resolve();
|
|
568
|
+
return waitForPage(function () {
|
|
569
|
+
return pending.done ? { reason: pending.reason } : null;
|
|
570
|
+
}, isCancelled);
|
|
323
571
|
}
|
|
324
572
|
//#endregion
|
|
325
573
|
|
|
@@ -327,6 +575,10 @@ window.__ModuleLoader__.load({
|
|
|
327
575
|
|
|
328
576
|
var state = {
|
|
329
577
|
ctx: null,
|
|
578
|
+
adapter: null,
|
|
579
|
+
target: null,
|
|
580
|
+
settleTimer: 0,
|
|
581
|
+
settleCancel: null,
|
|
330
582
|
bar: null,
|
|
331
583
|
input: null,
|
|
332
584
|
count: null,
|
|
@@ -335,6 +587,7 @@ window.__ModuleLoader__.load({
|
|
|
335
587
|
query: "",
|
|
336
588
|
ranges: [],
|
|
337
589
|
index: -1,
|
|
590
|
+
selection: null,
|
|
338
591
|
capped: false,
|
|
339
592
|
observer: null,
|
|
340
593
|
debounce: 0,
|
|
@@ -370,6 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
370
623
|
function setStatus(text, busy) {
|
|
371
624
|
if (!state.status) return;
|
|
372
625
|
state.status.textContent = text || "";
|
|
626
|
+
state.status.title = busy ? L.stopTip : (text || "");
|
|
373
627
|
if (busy) state.status.setAttribute("data-busy", "1");
|
|
374
628
|
else state.status.removeAttribute("data-busy");
|
|
375
629
|
updateCount();
|
|
@@ -384,32 +638,122 @@ window.__ModuleLoader__.load({
|
|
|
384
638
|
else state.scopeBtn.removeAttribute("data-whole");
|
|
385
639
|
}
|
|
386
640
|
|
|
641
|
+
function unavailableStatus() {
|
|
642
|
+
return state.target || (state.adapter && state.adapter.scopeUnavailable()) ? L.noRoot : L.noTarget;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// The code content wrapper survives the host's plain-pre -> Shiki tree
|
|
646
|
+
// swap (0.2.0-rc.2 CodeBlock). Otherwise use the nearest block boundary.
|
|
647
|
+
function selectionBlock(range, root) {
|
|
648
|
+
var nearest = null;
|
|
649
|
+
for (var node = range.startContainer.parentNode; node; node = node.parentNode) {
|
|
650
|
+
if (node === root || node.nodeType === 11) return nearest || node;
|
|
651
|
+
if (node.hasAttribute && node.hasAttribute("data-code-block-content")) return node;
|
|
652
|
+
if (!nearest && BLOCK_TAGS[node.tagName]) nearest = node;
|
|
653
|
+
}
|
|
654
|
+
return nearest;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function positionInBlocks(range, blocks) {
|
|
658
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
659
|
+
var start = null, end = null;
|
|
660
|
+
for (var j = 0; j < blocks[i].segments.length; j++) {
|
|
661
|
+
var segment = blocks[i].segments[j];
|
|
662
|
+
if (segment.node === range.startContainer) start = segment.start + range.startOffset;
|
|
663
|
+
if (segment.node === range.endContainer) end = segment.start + range.endOffset;
|
|
664
|
+
}
|
|
665
|
+
if (start !== null && end !== null) return { run: i, start: start, end: end };
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function matchNodes(blocks, position) {
|
|
671
|
+
if (!position) return [];
|
|
672
|
+
return blocks[position.run].segments.filter(function (segment) {
|
|
673
|
+
return segment.end > position.start && segment.start < position.end;
|
|
674
|
+
}).map(function (segment) {
|
|
675
|
+
return {
|
|
676
|
+
node: segment.node, text: segment.node.data,
|
|
677
|
+
start: Math.max(position.start, segment.start) - segment.start,
|
|
678
|
+
end: Math.min(position.end, segment.end) - segment.start
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function rememberSelection() {
|
|
684
|
+
var range = state.ranges[state.index];
|
|
685
|
+
state.selection = null;
|
|
686
|
+
if (!range || !state.target) return;
|
|
687
|
+
var block = selectionBlock(range, state.target.root);
|
|
688
|
+
var blocks = block ? collectTextBlocks(block) : [];
|
|
689
|
+
var position = positionInBlocks(range, blocks);
|
|
690
|
+
var nodes = matchNodes(blocks, position);
|
|
691
|
+
state.selection = {
|
|
692
|
+
target: state.target, query: state.query, root: state.target.root,
|
|
693
|
+
startNode: range.startContainer, startOffset: range.startOffset,
|
|
694
|
+
endNode: range.endContainer, endOffset: range.endOffset,
|
|
695
|
+
block: block, position: position, nodes: nodes,
|
|
696
|
+
texts: blocks.map(function (item) { return item.text; })
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function retainedIndex(saved) {
|
|
701
|
+
if (!saved || saved.target !== state.target || saved.query !== state.query || saved.root !== state.target.root) return -1;
|
|
702
|
+
if (!saved.block || !saved.block.isConnected || !saved.position) return -1;
|
|
703
|
+
var blocks = collectTextBlocks(saved.block);
|
|
704
|
+
var i, range;
|
|
705
|
+
for (i = 0; i < state.ranges.length; i++) {
|
|
706
|
+
range = state.ranges[i];
|
|
707
|
+
if (range.startContainer !== saved.startNode || range.startOffset !== saved.startOffset ||
|
|
708
|
+
range.endContainer !== saved.endNode || range.endOffset !== saved.endOffset) continue;
|
|
709
|
+
// Compare the new match's actual ordered participants. Old nodes can
|
|
710
|
+
// remain connected with unchanged text after moving outside this hit.
|
|
711
|
+
var nodes = matchNodes(blocks, positionInBlocks(range, blocks));
|
|
712
|
+
if (nodes.length > 0 && nodes.length === saved.nodes.length && nodes.every(function (item, index) {
|
|
713
|
+
var previous = saved.nodes[index];
|
|
714
|
+
return item.node === previous.node && item.text === previous.text && item.start === previous.start && item.end === previous.end;
|
|
715
|
+
})) return i;
|
|
716
|
+
}
|
|
717
|
+
if (blocks.length !== saved.texts.length || blocks.some(function (item, index) { return item.text !== saved.texts[index]; })) return -1;
|
|
718
|
+
var segments = blocks[saved.position.run].segments;
|
|
719
|
+
var first = null, last = null;
|
|
720
|
+
for (i = 0; i < segments.length; i++) {
|
|
721
|
+
if (saved.position.start >= segments[i].start && saved.position.start < segments[i].end) first = segments[i];
|
|
722
|
+
if (saved.position.end > segments[i].start && saved.position.end <= segments[i].end) last = segments[i];
|
|
723
|
+
}
|
|
724
|
+
if (!first || !last) return -1;
|
|
725
|
+
for (i = 0; i < state.ranges.length; i++) {
|
|
726
|
+
range = state.ranges[i];
|
|
727
|
+
if (range.startContainer === first.node && range.startOffset === saved.position.start - first.start &&
|
|
728
|
+
range.endContainer === last.node && range.endOffset === saved.position.end - last.start) return i;
|
|
729
|
+
}
|
|
730
|
+
return -1;
|
|
731
|
+
}
|
|
732
|
+
|
|
387
733
|
function runSearch(keepIndex) {
|
|
388
|
-
state.
|
|
734
|
+
var valid = state.adapter && state.adapter.valid(state.target);
|
|
735
|
+
state.ranges = valid ? collectRanges(state.query, state.target.root) : [];
|
|
736
|
+
if (!valid) setStatus(unavailableStatus(), false);
|
|
389
737
|
state.capped = state.ranges.length >= MAX_MATCHES;
|
|
390
|
-
|
|
738
|
+
var retained = keepIndex && valid ? retainedIndex(state.selection) : -1;
|
|
739
|
+
if (!state.ranges.length) state.index = -1;
|
|
740
|
+
else if (!keepIndex) state.index = 0;
|
|
741
|
+
else state.index = retained >= 0 ? retained : Math.max(0, Math.min(state.index, state.ranges.length - 1));
|
|
742
|
+
rememberSelection();
|
|
391
743
|
paint();
|
|
392
744
|
updateCount();
|
|
393
|
-
|
|
745
|
+
|
|
394
746
|
}
|
|
395
747
|
|
|
396
748
|
function goTo(delta) {
|
|
749
|
+
if (!state.adapter || !state.adapter.valid(state.target)) { runSearch(false); return; }
|
|
397
750
|
var n = state.ranges.length;
|
|
398
751
|
if (n === 0) return;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
} else {
|
|
405
|
-
legacyFind(state.query, delta < 0);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function legacyFind(query, backwards) {
|
|
410
|
-
if (typeof window.find === "function") {
|
|
411
|
-
try { window.find(query, false, backwards, true); } catch (error) {}
|
|
412
|
-
}
|
|
752
|
+
state.index = ((state.index + delta) % n + n) % n;
|
|
753
|
+
rememberSelection();
|
|
754
|
+
paint();
|
|
755
|
+
updateCount();
|
|
756
|
+
scrollToRange(state.ranges[state.index]);
|
|
413
757
|
}
|
|
414
758
|
|
|
415
759
|
function scheduleRescan() {
|
|
@@ -418,14 +762,14 @@ window.__ModuleLoader__.load({
|
|
|
418
762
|
if (state.paging) return;
|
|
419
763
|
clearTimeout(state.debounce);
|
|
420
764
|
state.debounce = setTimeout(function () {
|
|
421
|
-
if (
|
|
765
|
+
if (isOpen()) runSearch(true);
|
|
422
766
|
}, 250);
|
|
423
767
|
}
|
|
424
768
|
|
|
425
769
|
function startObserver() {
|
|
426
|
-
if (state.observer || typeof MutationObserver === "undefined") return;
|
|
770
|
+
if (state.observer || !state.target || typeof MutationObserver === "undefined") return;
|
|
427
771
|
state.observer = new MutationObserver(scheduleRescan);
|
|
428
|
-
state.observer.observe(
|
|
772
|
+
state.observer.observe(state.target.root, { childList: true, subtree: true, characterData: true });
|
|
429
773
|
}
|
|
430
774
|
|
|
431
775
|
function stopObserver() {
|
|
@@ -438,6 +782,8 @@ window.__ModuleLoader__.load({
|
|
|
438
782
|
|
|
439
783
|
function cancelPageIn() {
|
|
440
784
|
state.pageToken += 1;
|
|
785
|
+
if (state.settleCancel) state.settleCancel();
|
|
786
|
+
clearTimeout(state.settleTimer);
|
|
441
787
|
clearTimeout(state.pageTimer);
|
|
442
788
|
state.pageTimer = 0;
|
|
443
789
|
state.paging = false;
|
|
@@ -453,57 +799,91 @@ window.__ModuleLoader__.load({
|
|
|
453
799
|
|
|
454
800
|
function startPageIn() {
|
|
455
801
|
if (state.scope !== "whole" || !state.query || !isOpen()) return;
|
|
456
|
-
|
|
457
|
-
var
|
|
458
|
-
|
|
459
|
-
|
|
802
|
+
cancelPageIn();
|
|
803
|
+
var token = state.pageToken;
|
|
804
|
+
var target = state.target;
|
|
805
|
+
if (!state.adapter || !state.adapter.valid(target)) {
|
|
806
|
+
setStatus(unavailableStatus(), false);
|
|
460
807
|
return;
|
|
461
808
|
}
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
setStatus(L.
|
|
809
|
+
var face = state.adapter.face(target);
|
|
810
|
+
if (!face) {
|
|
811
|
+
setStatus(L.noService, false);
|
|
465
812
|
return;
|
|
466
813
|
}
|
|
814
|
+
state.sessionId = target.id;
|
|
815
|
+
function isCancelled() {
|
|
816
|
+
return token !== state.pageToken || !isOpen() || state.target !== target || !state.adapter || state.adapter.face(target) !== face;
|
|
817
|
+
}
|
|
818
|
+
function waitReady(failFast) {
|
|
819
|
+
return waitForPage(function () {
|
|
820
|
+
var status = facePageState(face);
|
|
821
|
+
if (status && status.reason) return status;
|
|
822
|
+
if (pendingLoads.has(face) || !status) return failFast ? { reason: "stalled" } : null;
|
|
823
|
+
return status;
|
|
824
|
+
}, isCancelled);
|
|
825
|
+
}
|
|
467
826
|
state.paging = true;
|
|
468
827
|
state.pages = 0;
|
|
469
828
|
setStatus(L.loading.replace("{n}", "0"), true);
|
|
470
829
|
runPageIn({
|
|
471
830
|
maxPages: MAX_PAGES,
|
|
472
|
-
isCancelled:
|
|
473
|
-
|
|
474
|
-
loadOlder: function () { return
|
|
475
|
-
signature: domSignature,
|
|
476
|
-
waitChange:
|
|
831
|
+
isCancelled: isCancelled,
|
|
832
|
+
waitReady: waitReady,
|
|
833
|
+
loadOlder: function () { return loadPage(face, isCancelled); },
|
|
834
|
+
signature: function () { return domSignature(target.root); },
|
|
835
|
+
waitChange: function (before) {
|
|
836
|
+
return waitForPage(function () {
|
|
837
|
+
var status = facePageState(face);
|
|
838
|
+
return domSignature(target.root) !== before || !status || status.reason || status.hasMore === false ? {} : null;
|
|
839
|
+
}, isCancelled);
|
|
840
|
+
},
|
|
477
841
|
onProgress: function (pages) {
|
|
478
|
-
if (
|
|
842
|
+
if (isCancelled()) return;
|
|
479
843
|
state.pages = pages;
|
|
480
844
|
setStatus(L.loading.replace("{n}", String(pages)), true);
|
|
481
845
|
runSearch(true);
|
|
482
846
|
}
|
|
483
847
|
}).then(function (result) {
|
|
484
|
-
if (
|
|
848
|
+
if (isCancelled()) {
|
|
849
|
+
// Face replacement cancels this task even if the DOM target survived.
|
|
850
|
+
if (token === state.pageToken) { state.paging = false; setStatus(L.incomplete, false); }
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
var terminal = facePageState(face);
|
|
854
|
+
if (result.reason === "done" && (!terminal || terminal.reason || terminal.hasMore !== false || pendingLoads.has(face))) result.reason = "stalled";
|
|
485
855
|
state.paging = false;
|
|
486
856
|
state.pages = result.pages;
|
|
487
857
|
if (result.reason === "capped") setStatus(L.capped.replace("{n}", String(result.pages)), false);
|
|
488
|
-
else if (result.reason
|
|
489
|
-
else setStatus(
|
|
858
|
+
else if (result.reason !== "done") setStatus(L.incomplete, false);
|
|
859
|
+
else setStatus(L.available, false);
|
|
490
860
|
runSearch(true);
|
|
491
861
|
});
|
|
492
862
|
}
|
|
493
863
|
|
|
494
|
-
|
|
864
|
+
function syncTarget(target, passive, retainNeutral) {
|
|
865
|
+
if (!state.adapter) return;
|
|
866
|
+
var selected = passive ? state.adapter.refresh(state.target) : state.adapter.select(target || document.activeElement, state.target, retainNeutral);
|
|
867
|
+
if (selected && state.target && selected.id === state.target.id && selected.anchor === state.target.anchor && selected.root === state.target.root) return;
|
|
868
|
+
if (!selected && !state.target) {
|
|
869
|
+
if (isOpen()) runSearch(false);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
cancelPageIn();
|
|
873
|
+
stopObserver();
|
|
874
|
+
state.target = selected;
|
|
875
|
+
state.sessionId = selected ? selected.id : null;
|
|
876
|
+
if (!isOpen()) return;
|
|
877
|
+
setStatus(selected ? "" : unavailableStatus(), false);
|
|
878
|
+
runSearch(false);
|
|
879
|
+
startObserver();
|
|
880
|
+
if (selected && state.scope === "whole" && state.query) schedulePageIn();
|
|
881
|
+
}
|
|
882
|
+
|
|
495
883
|
function watchSession() {
|
|
496
884
|
clearInterval(state.sessionTimer);
|
|
497
885
|
state.sessionTimer = setInterval(function () {
|
|
498
|
-
if (
|
|
499
|
-
var resolved = resolveFace(state.ctx);
|
|
500
|
-
var id = resolved ? resolved.id : null;
|
|
501
|
-
if (id === state.sessionId) return;
|
|
502
|
-
state.sessionId = id;
|
|
503
|
-
cancelPageIn();
|
|
504
|
-
setStatus("", false);
|
|
505
|
-
runSearch(false);
|
|
506
|
-
if (state.scope === "whole" && state.query) schedulePageIn();
|
|
886
|
+
if (isOpen()) syncTarget(null, true);
|
|
507
887
|
}, SESSION_POLL_MS);
|
|
508
888
|
}
|
|
509
889
|
|
|
@@ -582,7 +962,7 @@ window.__ModuleLoader__.load({
|
|
|
582
962
|
status.addEventListener("click", function () {
|
|
583
963
|
if (!state.paging) return;
|
|
584
964
|
cancelPageIn();
|
|
585
|
-
setStatus(L.
|
|
965
|
+
setStatus(L.incomplete, false);
|
|
586
966
|
});
|
|
587
967
|
|
|
588
968
|
bar.appendChild(input);
|
|
@@ -602,16 +982,17 @@ window.__ModuleLoader__.load({
|
|
|
602
982
|
document.body.appendChild(bar);
|
|
603
983
|
}
|
|
604
984
|
|
|
605
|
-
function open() {
|
|
985
|
+
function open(target) {
|
|
606
986
|
if (typeof document === "undefined" || document.body === null) return;
|
|
987
|
+
syncTarget(target || document.activeElement, false, true);
|
|
607
988
|
if (!state.bar) buildBar();
|
|
608
989
|
state.bar.style.display = "flex";
|
|
609
990
|
var selection = "";
|
|
610
991
|
try { selection = String(window.getSelection()).trim(); } catch (error) {}
|
|
611
|
-
if (selection && selection.length <= 200 && selection !== state.query) {
|
|
612
|
-
state.input.value = selection;
|
|
992
|
+
if (state.target && selection && selection.length <= 200 && selection !== state.query) {
|
|
613
993
|
state.query = selection;
|
|
614
994
|
}
|
|
995
|
+
state.input.value = state.query;
|
|
615
996
|
state.input.focus();
|
|
616
997
|
state.input.select();
|
|
617
998
|
runSearch(false);
|
|
@@ -624,9 +1005,9 @@ window.__ModuleLoader__.load({
|
|
|
624
1005
|
cancelPageIn();
|
|
625
1006
|
stopWatchingSession();
|
|
626
1007
|
if (state.bar) state.bar.style.display = "none";
|
|
627
|
-
state.query = "";
|
|
628
1008
|
state.ranges = [];
|
|
629
1009
|
state.index = -1;
|
|
1010
|
+
state.selection = null;
|
|
630
1011
|
setStatus("", false);
|
|
631
1012
|
paint(); // clears registered highlights
|
|
632
1013
|
updateCount();
|
|
@@ -638,21 +1019,61 @@ window.__ModuleLoader__.load({
|
|
|
638
1019
|
}
|
|
639
1020
|
|
|
640
1021
|
function apply(ctx) {
|
|
1022
|
+
var react = require("react");
|
|
641
1023
|
state.ctx = ctx;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1024
|
+
state.adapter = createHostAdapter(ctx, function () {
|
|
1025
|
+
syncTarget(null, true);
|
|
1026
|
+
});
|
|
1027
|
+
function SessionFind(props) {
|
|
1028
|
+
var anchor = react.useRef(null);
|
|
1029
|
+
react.useEffect(function () {
|
|
1030
|
+
if (!anchor.current || !props.sessionId) return;
|
|
1031
|
+
return state.adapter.mount(anchor.current, props.sessionId);
|
|
1032
|
+
}, [props.sessionId]);
|
|
1033
|
+
return react.createElement("button", {
|
|
1034
|
+
ref: anchor, type: "button", title: L.find, "aria-label": L.find,
|
|
1035
|
+
"data-find-all-session": props.sessionId,
|
|
1036
|
+
onClick: function (event) { open(event.currentTarget); }
|
|
1037
|
+
}, "⌕");
|
|
1038
|
+
}
|
|
1039
|
+
ctx.slots.inject("conversation.session.header.utilities", function () {
|
|
1040
|
+
return ctx.slots.register({ name: "conversation.session.header.utilities", id: "dsh-find-all" }, SessionFind);
|
|
1041
|
+
});
|
|
1042
|
+
ctx.effect(function () {
|
|
1043
|
+
injectCss();
|
|
1044
|
+
var handler = createHandlers({
|
|
1045
|
+
open: open, close: close, goTo: goTo, isOpen: isOpen,
|
|
1046
|
+
isBarInput: function (target) { return target === state.input; }
|
|
1047
|
+
});
|
|
1048
|
+
function focus(event) {
|
|
1049
|
+
if (!state.bar || !state.bar.contains(event.target)) syncTarget(event.target);
|
|
1050
|
+
}
|
|
1051
|
+
window.addEventListener("keydown", handler, true);
|
|
1052
|
+
window.addEventListener("focusin", focus, true);
|
|
1053
|
+
window.addEventListener("pointerdown", focus, true);
|
|
1054
|
+
return function () {
|
|
1055
|
+
window.removeEventListener("keydown", handler, true);
|
|
1056
|
+
window.removeEventListener("focusin", focus, true);
|
|
1057
|
+
window.removeEventListener("pointerdown", focus, true);
|
|
1058
|
+
close();
|
|
1059
|
+
state.query = "";
|
|
1060
|
+
if (state.bar) state.bar.remove();
|
|
1061
|
+
var style = document.querySelector('style[data-plugin-css="dsh-find-all/bar.css"]');
|
|
1062
|
+
if (style) style.remove();
|
|
1063
|
+
state.bar = state.input = state.count = state.status = state.scopeBtn = null;
|
|
1064
|
+
state.adapter.clear();
|
|
1065
|
+
state.target = state.ctx = null;
|
|
1066
|
+
};
|
|
1067
|
+
}, "find-all: browser lifecycle");
|
|
651
1068
|
}
|
|
652
1069
|
|
|
653
1070
|
exports.apply = apply;
|
|
654
|
-
exports.inject = ["sessions"];
|
|
1071
|
+
exports.inject = ["sessions", "slots"];
|
|
1072
|
+
exports.resolveScope = resolveScope;
|
|
1073
|
+
exports.createHostAdapter = createHostAdapter;
|
|
655
1074
|
exports.findIndices = findIndices; // for tests
|
|
1075
|
+
exports.findMatches = findMatches; // for tests
|
|
1076
|
+
exports.collectRanges = collectRanges; // for tests
|
|
656
1077
|
exports.createHandlers = createHandlers; // for tests
|
|
657
1078
|
exports.runPageIn = runPageIn; // for tests
|
|
658
1079
|
return module.exports;
|