dsh-width-tiers 1.0.1 → 1.0.2
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/lib/client.js +145 -14
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -41,8 +41,14 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
*/
|
|
42
42
|
const KEY = "dsh.widthTier";
|
|
43
43
|
const OLD_KEY = "dsh.wide";
|
|
44
|
-
const SELF_HEAL_MS = 1000;
|
|
45
44
|
const READY_TIMEOUT_MS = 30000;
|
|
45
|
+
/* Self-heal cadence: DOM-driven (MutationObserver scheduling on the
|
|
46
|
+
* next animation frame — before paint, so the width never flashes
|
|
47
|
+
* default) with a low-frequency fallback and backoff retries while
|
|
48
|
+
* the width root is missing (conversation-switch remount). */
|
|
49
|
+
const ROOT_RETRY_MIN_MS = 200;
|
|
50
|
+
const ROOT_RETRY_MAX_MS = 2000;
|
|
51
|
+
const FALLBACK_HEAL_MS = 2000;
|
|
46
52
|
|
|
47
53
|
/** Dictionary namespace owned by this plugin. */
|
|
48
54
|
const NS = "width-tiers";
|
|
@@ -174,25 +180,62 @@ window.__ModuleLoader__.load({
|
|
|
174
180
|
|
|
175
181
|
/* Locate the element that defines --dsh-chat-content-width at
|
|
176
182
|
* runtime (first match wins; only one element declares it).
|
|
177
|
-
* Cached;
|
|
183
|
+
* Cached; the app remounts the chat tree on conversation switch,
|
|
184
|
+
* so re-location first walks the recorded child-index path from
|
|
185
|
+
* <body> (React keeps the tree shape — O(depth), one style
|
|
186
|
+
* read) and only falls back to a full scan. */
|
|
178
187
|
let widthRoot = null;
|
|
188
|
+
let widthPath = null;
|
|
179
189
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
function hasWidthVar(el) {
|
|
191
|
+
return getComputedStyle(el)
|
|
192
|
+
.getPropertyValue("--dsh-chat-content-width")
|
|
193
|
+
.trim() !== "";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function recordWidthPath(el) {
|
|
197
|
+
const path = [];
|
|
198
|
+
let node = el;
|
|
199
|
+
while (node && node !== document.body) {
|
|
200
|
+
const parent = node.parentNode;
|
|
201
|
+
if (!parent || !parent.children) break;
|
|
202
|
+
path.unshift(Array.prototype.indexOf.call(parent.children, node));
|
|
203
|
+
node = parent;
|
|
204
|
+
}
|
|
205
|
+
widthPath = path.length > 0 ? path : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function scanWidthRoot() {
|
|
183
209
|
const all = document.querySelectorAll("[class]");
|
|
184
210
|
for (let i = 0; i < all.length; i++) {
|
|
185
|
-
|
|
186
|
-
.getPropertyValue("--dsh-chat-content-width")
|
|
187
|
-
.trim();
|
|
188
|
-
if (v) {
|
|
211
|
+
if (hasWidthVar(all[i])) {
|
|
189
212
|
widthRoot = all[i];
|
|
213
|
+
recordWidthPath(widthRoot);
|
|
190
214
|
return widthRoot;
|
|
191
215
|
}
|
|
192
216
|
}
|
|
217
|
+
widthRoot = null;
|
|
218
|
+
widthPath = null;
|
|
193
219
|
return null;
|
|
194
220
|
}
|
|
195
221
|
|
|
222
|
+
function findWidthRoot(allowScan) {
|
|
223
|
+
if (widthRoot !== null && document.contains(widthRoot)) return widthRoot;
|
|
224
|
+
if (widthPath !== null) {
|
|
225
|
+
let el = document.body;
|
|
226
|
+
for (let i = 0; i < widthPath.length; i++) {
|
|
227
|
+
el = el.children[widthPath[i]];
|
|
228
|
+
if (!el) break;
|
|
229
|
+
}
|
|
230
|
+
if (el && el !== widthRoot && document.contains(el) && hasWidthVar(el)) {
|
|
231
|
+
widthRoot = el;
|
|
232
|
+
return widthRoot;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (allowScan === false) return null;
|
|
236
|
+
return scanWidthRoot();
|
|
237
|
+
}
|
|
238
|
+
|
|
196
239
|
function tierWidth(tier) {
|
|
197
240
|
switch (tier) {
|
|
198
241
|
case "medium": return "960px";
|
|
@@ -367,6 +410,7 @@ window.__ModuleLoader__.load({
|
|
|
367
410
|
syncButton();
|
|
368
411
|
const saved = getSavedTier();
|
|
369
412
|
if (saved !== "standard") applyTier(saved);
|
|
413
|
+
startHealing();
|
|
370
414
|
}
|
|
371
415
|
|
|
372
416
|
const readyTimer = setInterval(attach, 300);
|
|
@@ -378,20 +422,107 @@ window.__ModuleLoader__.load({
|
|
|
378
422
|
|
|
379
423
|
/* ---- self-heal: keep the details panel closed for non-standard
|
|
380
424
|
* tiers. The sidebar is never managed here — the user may expand
|
|
381
|
-
* it any time.
|
|
382
|
-
|
|
425
|
+
* it any time. Driven by DOM mutations, scheduled on the next
|
|
426
|
+
* animation frame (before the browser paints) instead of a fixed
|
|
427
|
+
* hot loop: conversation switches remount the chat tree, and the
|
|
428
|
+
* width override must land on the fresh root before its first
|
|
429
|
+
* paint — otherwise the default width flashes for a frame.
|
|
430
|
+
* While the new root has not appeared yet (mid-remount), full
|
|
431
|
+
* scans are owned by a backoff retry; mutation-driven heals only
|
|
432
|
+
* try the cheap path walk so DOM churn cannot storm. ---- */
|
|
433
|
+
let rootMissing = false;
|
|
434
|
+
let rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
435
|
+
let rootRetryTimer = null;
|
|
436
|
+
let healFrame = null;
|
|
437
|
+
let observer = null;
|
|
438
|
+
let fallbackTimer = null;
|
|
439
|
+
|
|
440
|
+
const scheduleFrame = typeof requestAnimationFrame === "function"
|
|
441
|
+
? (cb) => { healFrame = requestAnimationFrame(cb); }
|
|
442
|
+
: (cb) => { healFrame = window.setTimeout(cb, 0); };
|
|
443
|
+
const cancelFrame = typeof cancelAnimationFrame === "function"
|
|
444
|
+
? (id) => cancelAnimationFrame(id)
|
|
445
|
+
: (id) => window.clearTimeout(id);
|
|
446
|
+
|
|
447
|
+
function scheduleRootRetry() {
|
|
448
|
+
if (rootRetryTimer !== null) return;
|
|
449
|
+
rootRetryTimer = window.setTimeout(() => {
|
|
450
|
+
rootRetryTimer = null;
|
|
451
|
+
const tier = currentTier();
|
|
452
|
+
if (tier === "standard") return;
|
|
453
|
+
const prev = widthRoot;
|
|
454
|
+
const root = findWidthRoot();
|
|
455
|
+
if (root === null) {
|
|
456
|
+
rootRetryDelay = Math.min(rootRetryDelay * 2, ROOT_RETRY_MAX_MS);
|
|
457
|
+
scheduleRootRetry();
|
|
458
|
+
} else {
|
|
459
|
+
rootMissing = false;
|
|
460
|
+
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
461
|
+
if (root !== prev) applyContentWidth(tier);
|
|
462
|
+
}
|
|
463
|
+
}, rootRetryDelay);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function heal(fromObserver) {
|
|
383
467
|
const tier = currentTier();
|
|
384
468
|
if (tier === "standard") return;
|
|
385
469
|
closeDetails();
|
|
386
|
-
|
|
387
|
-
|
|
470
|
+
const prev = widthRoot;
|
|
471
|
+
let root;
|
|
472
|
+
if (rootMissing) {
|
|
473
|
+
/* Mid-remount: a mutation-driven heal only walks the
|
|
474
|
+
* recorded path (O(depth)) — the backoff retry owns
|
|
475
|
+
* full scans until the new root exists. */
|
|
476
|
+
root = fromObserver ? findWidthRoot(false) : findWidthRoot();
|
|
477
|
+
} else {
|
|
478
|
+
root = findWidthRoot();
|
|
479
|
+
}
|
|
480
|
+
if (root === null) {
|
|
481
|
+
if (!rootMissing) {
|
|
482
|
+
rootMissing = true;
|
|
483
|
+
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
484
|
+
scheduleRootRetry();
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (rootMissing) {
|
|
489
|
+
rootMissing = false;
|
|
490
|
+
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
491
|
+
}
|
|
492
|
+
if (root !== prev) applyContentWidth(tier);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function startHealing() {
|
|
496
|
+
if (typeof MutationObserver !== "undefined") {
|
|
497
|
+
observer = new MutationObserver(() => {
|
|
498
|
+
if (healFrame !== null) return;
|
|
499
|
+
scheduleFrame(() => {
|
|
500
|
+
healFrame = null;
|
|
501
|
+
heal(true);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
observer.observe(document.body, {
|
|
505
|
+
subtree: true,
|
|
506
|
+
childList: true,
|
|
507
|
+
attributes: true,
|
|
508
|
+
attributeFilter: ["data-details-collapsed", "data-sidebar-collapsed"]
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/* Low-frequency fallback so self-heal still runs if the
|
|
512
|
+
* observer misses something — cheap: no scan while the
|
|
513
|
+
* cached root is alive. */
|
|
514
|
+
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
515
|
+
}
|
|
388
516
|
|
|
389
517
|
/* ---- disposer ---- */
|
|
390
518
|
return () => {
|
|
391
519
|
unsubscribeLocale();
|
|
392
520
|
clearInterval(readyTimer);
|
|
393
|
-
clearInterval(
|
|
521
|
+
clearInterval(fallbackTimer);
|
|
394
522
|
clearTimeout(timeout);
|
|
523
|
+
clearTimeout(rootRetryTimer);
|
|
524
|
+
if (healFrame !== null) cancelFrame(healFrame);
|
|
525
|
+
if (observer !== null) observer.disconnect();
|
|
395
526
|
document.removeEventListener("click", onDocClick);
|
|
396
527
|
document.removeEventListener("keydown", onDocKey);
|
|
397
528
|
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-width-tiers",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "DSH Web client plugin: chat content width tiers (standard / medium / wide / ultra / full) with a floating picker button. Frees wide tables from the app's hard-coded 748px content cap. UI copy follows the app language (zh / en).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "aaronlei",
|