dsh-fonttune 0.3.6 → 0.4.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/CHANGELOG.en.md +15 -0
- package/CHANGELOG.md +15 -0
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/lib/client.js +10263 -6137
- package/lib/index.js +75 -3
- package/lib/shared.cjs +1262 -5
- package/package.json +1 -1
package/lib/shared.cjs
CHANGED
|
@@ -119,6 +119,19 @@ var NO_SYNTHETIC_ITALIC_FIELD = "noSyntheticItalic";
|
|
|
119
119
|
/** Disable synthetic (faux) bold, for faces without a real bold. */
|
|
120
120
|
var NO_SYNTHETIC_BOLD_FIELD = "noSyntheticBold";
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* How the browser draws the strokes: `SMOOTHING_AUTO` leaves the system's own
|
|
124
|
+
* rendering alone, `SMOOTHING_SHARP` asks for the crisp subpixel kind and
|
|
125
|
+
* `SMOOTHING_SMOOTH` for the soft grayscale kind. The same face reads firmer
|
|
126
|
+
* under one and softer under the other, so this is a look the reader feels
|
|
127
|
+
* rather than measures — durable, but never part of a preset snapshot.
|
|
128
|
+
*/
|
|
129
|
+
var SMOOTHING_FIELD = "fontSmoothing";
|
|
130
|
+
var SMOOTHING_AUTO = "auto";
|
|
131
|
+
var SMOOTHING_SHARP = "sharp";
|
|
132
|
+
var SMOOTHING_SMOOTH = "smooth";
|
|
133
|
+
var SMOOTHING_VALUES = [SMOOTHING_AUTO, SMOOTHING_SHARP, SMOOTHING_SMOOTH];
|
|
134
|
+
|
|
122
135
|
/** Whether the dark theme keeps its own value set (sparse overrides). */
|
|
123
136
|
var PER_THEME_FIELD = "perTheme";
|
|
124
137
|
|
|
@@ -143,6 +156,43 @@ var ACTIVE_PRESET_FIELD = "activePreset";
|
|
|
143
156
|
*/
|
|
144
157
|
var UI_FOLLOWS_FIELD = "uiFollowsDialog";
|
|
145
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The floating panel's master switch. Chrome, not typography: it never rides
|
|
161
|
+
* a preset snapshot, so switching presets cannot hide or move the panel.
|
|
162
|
+
*/
|
|
163
|
+
var PANEL_ENABLED_FIELD = "panelEnabled";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The floating panel's last position, as `"x,y"` in CSS pixels from the
|
|
167
|
+
* viewport's top-left (empty = the default corner). Chrome, not typography:
|
|
168
|
+
* kept out of presets for the same reason as the master switch.
|
|
169
|
+
*/
|
|
170
|
+
var PANEL_POS_FIELD = "panelPos";
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The corner the floating panel LIVES in: one of `"tl"`, `"tr"`, `"bl"`, `"br"`,
|
|
174
|
+
* or "" while no corner has been chosen yet.
|
|
175
|
+
*
|
|
176
|
+
* The corner is the dot's identity and the position is only its consequence. A
|
|
177
|
+
* region that changes shape — a sidebar folding away, a top bar appearing, a
|
|
178
|
+
* window resize — MOVES the corner the dot sits on, and the dot follows that
|
|
179
|
+
* corner; it does not re-decide which one it belongs to from the coordinates the
|
|
180
|
+
* old region gave it. Only a release the user made themselves re-decides (the
|
|
181
|
+
* nearest corner, see `floatNearestCorner`), and this field is what carries that
|
|
182
|
+
* decision across every later region change. Chrome, not typography: kept out of
|
|
183
|
+
* presets for the same reason as the master switch.
|
|
184
|
+
*/
|
|
185
|
+
var PANEL_CORNER_FIELD = "panelCorner";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The floating panel's last size, as `"w,h"` in CSS pixels (empty = default).
|
|
189
|
+
* Chrome, not typography: persisted next to the position, never snapshotted.
|
|
190
|
+
*/
|
|
191
|
+
var PANEL_SIZE_FIELD = "panelSize";
|
|
192
|
+
|
|
193
|
+
/** The four corners a remembered identity may name, in reading order. */
|
|
194
|
+
var PANEL_CORNERS = ["tl", "tr", "bl", "br"];
|
|
195
|
+
|
|
146
196
|
/* ------------------------------------------------------------------ *
|
|
147
197
|
* ranges and constants
|
|
148
198
|
* ------------------------------------------------------------------ */
|
|
@@ -288,11 +338,16 @@ var DEFAULTS = {
|
|
|
288
338
|
codeFeatures: "",
|
|
289
339
|
noSyntheticItalic: false,
|
|
290
340
|
noSyntheticBold: false,
|
|
341
|
+
fontSmoothing: SMOOTHING_AUTO,
|
|
291
342
|
perTheme: false,
|
|
292
343
|
darkValues: "{}",
|
|
293
344
|
presets: "[]",
|
|
294
345
|
activePreset: "",
|
|
295
346
|
uiFollowsDialog: true,
|
|
347
|
+
panelEnabled: true,
|
|
348
|
+
panelPos: "",
|
|
349
|
+
panelCorner: "",
|
|
350
|
+
panelSize: "",
|
|
296
351
|
};
|
|
297
352
|
|
|
298
353
|
/**
|
|
@@ -337,7 +392,14 @@ var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
|
|
|
337
392
|
* axis — a preset must never carry it, or applying that preset would re-label a
|
|
338
393
|
* document still holding absolute weights).
|
|
339
394
|
*/
|
|
340
|
-
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS, [
|
|
395
|
+
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS, [
|
|
396
|
+
WEIGHT_OFFSETS_FIELD,
|
|
397
|
+
PANEL_ENABLED_FIELD,
|
|
398
|
+
PANEL_POS_FIELD,
|
|
399
|
+
PANEL_CORNER_FIELD,
|
|
400
|
+
PANEL_SIZE_FIELD,
|
|
401
|
+
SMOOTHING_FIELD,
|
|
402
|
+
]);
|
|
341
403
|
|
|
342
404
|
/**
|
|
343
405
|
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
@@ -451,6 +513,1018 @@ function sanitizeFeatures(value) {
|
|
|
451
513
|
/** Characters a `font-feature-settings` value cannot contain. */
|
|
452
514
|
var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
|
|
453
515
|
|
|
516
|
+
/** The widest panel coordinate the position field accepts, in CSS pixels. */
|
|
517
|
+
var PANEL_POS_MAX = 5000;
|
|
518
|
+
|
|
519
|
+
/** The floating panel's default and clamped size, in CSS pixels. */
|
|
520
|
+
var PANEL_SIZE_DEFAULT_W = 264;
|
|
521
|
+
var PANEL_SIZE_DEFAULT_H = 0;
|
|
522
|
+
var PANEL_SIZE_MIN_W = 200;
|
|
523
|
+
var PANEL_SIZE_MAX_W = 520;
|
|
524
|
+
var PANEL_SIZE_MIN_H = 160;
|
|
525
|
+
var PANEL_SIZE_MAX_H = 800;
|
|
526
|
+
|
|
527
|
+
/** The dot's own diameter, the snap margin, and the click-vs-drag threshold. */
|
|
528
|
+
var FLOAT_DOT = 26;
|
|
529
|
+
var FLOAT_MARGIN = 16;
|
|
530
|
+
var FLOAT_DRAG_THRESHOLD = 6;
|
|
531
|
+
|
|
532
|
+
/** The window size a page that reports no usable viewport is measured against. */
|
|
533
|
+
var FLOAT_VIEWPORT_W = 1024;
|
|
534
|
+
var FLOAT_VIEWPORT_H = 768;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The smallest rectangle the dot can be snapped inside, in CSS pixels.
|
|
538
|
+
*
|
|
539
|
+
* One snapped dot needs its own diameter PLUS a margin on both sides, so
|
|
540
|
+
* anything smaller is not a region the dot can live in: the candidate is
|
|
541
|
+
* rejected and the next one (the window, last) stands in for it.
|
|
542
|
+
*/
|
|
543
|
+
var FLOAT_BOUNDS_MIN = FLOAT_DOT + 2 * FLOAT_MARGIN;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* The air the title keeps beside the round close button, in CSS pixels.
|
|
547
|
+
*
|
|
548
|
+
* The close is out of flow in the card's own top-right corner, so the header
|
|
549
|
+
* carries a plain strip on that side: no per-corner arithmetic, no slot for the
|
|
550
|
+
* dot (the dot is faded out while the card is open, so nothing has to be kept
|
|
551
|
+
* clear for it).
|
|
552
|
+
*/
|
|
553
|
+
var FLOAT_CLOSE_GAP = 8;
|
|
554
|
+
|
|
555
|
+
/** The round close button's diameter, in CSS pixels (its stylesheet uses it). */
|
|
556
|
+
var FLOAT_CLOSE = 22;
|
|
557
|
+
|
|
558
|
+
/** How far the close button sits from the card's own corner, in CSS pixels. */
|
|
559
|
+
var FLOAT_CLOSE_INSET = 8;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The air between the two round buttons in the card's head, in CSS pixels.
|
|
563
|
+
*
|
|
564
|
+
* Both are out of flow and pinned to the card's own top-right corner, so the
|
|
565
|
+
* settings entry sits one diameter plus this gap to the LEFT of the close. It
|
|
566
|
+
* is the same 8px the header already uses between its rows, so the pair reads
|
|
567
|
+
* as one group rather than as two unrelated circles.
|
|
568
|
+
*/
|
|
569
|
+
var FLOAT_SETTINGS_GAP = 8;
|
|
570
|
+
|
|
571
|
+
/** The card's own horizontal padding, in CSS pixels (mirrors `.dfp-floatCard`). */
|
|
572
|
+
var FLOAT_CARD_PAD = 12;
|
|
573
|
+
|
|
574
|
+
/** The radius the expanded card rests at, in CSS pixels (`.dfp-floatCard`). */
|
|
575
|
+
var FLOAT_CARD_RADIUS = 16;
|
|
576
|
+
|
|
577
|
+
/* ------------------------------------------------------------------ *
|
|
578
|
+
* reaching the settings screen
|
|
579
|
+
* ------------------------------------------------------------------ */
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* The setting that ships DSH's Plugins screen, as a stable `data-*` attribute.
|
|
583
|
+
*
|
|
584
|
+
* Measured on the shipped builds (0.2.0-rc.2): `ui-settings-plugins` registers
|
|
585
|
+
* its navigation entry with `id: "plugins"` and the settings shell renders each
|
|
586
|
+
* entry as `<button aria-current>`; the alpha line names the same entry
|
|
587
|
+
* `plugins.item` and opens it as a page. The attribute below is the one form a
|
|
588
|
+
* host actually SHIPS, and it is checked first because it is the only one that
|
|
589
|
+
* does not depend on a label's language.
|
|
590
|
+
*/
|
|
591
|
+
var SETTINGS_NAV_ITEM_ATTR = "data-section";
|
|
592
|
+
var SETTINGS_NAV_ITEM_VALUE = "plugins";
|
|
593
|
+
|
|
594
|
+
/** The attribute the settings shell marks its navigation cells with. */
|
|
595
|
+
var SETTINGS_NAV_CURRENT_ATTR = "aria-current";
|
|
596
|
+
|
|
597
|
+
/** The attribute the sidebar's settings launcher gets: it opens a dialog. */
|
|
598
|
+
var SETTINGS_LAUNCHER_HASPOPUP = "dialog";
|
|
599
|
+
|
|
600
|
+
/** The attribute a plugin's own row ships with, and this plugin's id inside it. */
|
|
601
|
+
var PLUGIN_ITEM_ATTR = "data-plugin-item";
|
|
602
|
+
var PLUGIN_ITEM_VALUE = "fonttune";
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Every spelling of this plugin's row, and of the tab that opens its settings.
|
|
606
|
+
*
|
|
607
|
+
* The row is found by its shipped id first (`data-plugin-item="fonttune"`); these
|
|
608
|
+
* labels are the fallback for a host that renders no such attribute, matched
|
|
609
|
+
* case-insensitively by substring so a decorated label still hits.
|
|
610
|
+
*/
|
|
611
|
+
var PLUGIN_TEXTS = ["字体增强", "font tune", "fonttune"];
|
|
612
|
+
var PLUGIN_CONFIG_TEXTS = ["插件配置", "plugin configuration", "plugin config"];
|
|
613
|
+
|
|
614
|
+
/** What the plugin row and its tab can be, before the predicate narrows it. */
|
|
615
|
+
var PLUGIN_ITEM_CANDIDATES = "[data-plugin-item], button, a, li, [role=button], [role=tab], [role=treeitem], [role=option], [role=link]";
|
|
616
|
+
var PLUGIN_TAB_CANDIDATES = "button, a, [role=tab], [role=button]";
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Every spelling of the Plugins navigation entry the supported languages use.
|
|
620
|
+
*
|
|
621
|
+
* A host that names its entry in the DOM ships it as translated text, so the
|
|
622
|
+
* text form is a genuine fallback and not the primary route (see
|
|
623
|
+
* `isSettingsNavItem`). Matched case-insensitively by substring, so a host that
|
|
624
|
+
* decorates the label ("Built-in plugins") still hits.
|
|
625
|
+
*/
|
|
626
|
+
var SETTINGS_NAV_TEXTS = [
|
|
627
|
+
"插件",
|
|
628
|
+
"Plugins",
|
|
629
|
+
"Plugin",
|
|
630
|
+
"プラグイン",
|
|
631
|
+
"플러그인",
|
|
632
|
+
"Plugins", // de/es/fr/pt all ship "Plugins"; kept explicit for the table's sake
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* The names the sidebar's settings launcher answers to, per language.
|
|
637
|
+
*
|
|
638
|
+
* The launcher is the LAST resort: it opens the settings dialog on whichever
|
|
639
|
+
* section was last active, so a caller that lands here still has to be told
|
|
640
|
+
* which cell to pick. It is the only route that cannot fail on a wrong name,
|
|
641
|
+
* because the attribute it is found by (`aria-haspopup="dialog"`) is structural.
|
|
642
|
+
*/
|
|
643
|
+
var SETTINGS_LAUNCHER_TEXTS = ["设置", "Settings", "設定", "설정", "Einstellungen"];
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Read an element's own text, tolerating a host without `textContent`.
|
|
647
|
+
* @param {object} node - the candidate element.
|
|
648
|
+
* @returns {string} its text, or "".
|
|
649
|
+
*/
|
|
650
|
+
function readNodeText(node) {
|
|
651
|
+
if (node === null || node === undefined) return "";
|
|
652
|
+
var text = node.textContent;
|
|
653
|
+
return typeof text === "string" ? text : "";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Read one attribute, tolerating a host without `getAttribute`.
|
|
658
|
+
* @param {object} node - the candidate element.
|
|
659
|
+
* @param {string} name - the attribute name.
|
|
660
|
+
* @returns {string} its value, or "".
|
|
661
|
+
*/
|
|
662
|
+
function readNodeAttr(node, name) {
|
|
663
|
+
if (node === null || node === undefined || typeof node.getAttribute !== "function") return "";
|
|
664
|
+
var value = node.getAttribute(name);
|
|
665
|
+
return value === null || value === undefined ? "" : String(value);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Read an element's classes as a space-separated string.
|
|
670
|
+
* @param {object} node - the candidate element.
|
|
671
|
+
* @returns {string} its classes, or "".
|
|
672
|
+
*/
|
|
673
|
+
function readNodeClass(node) {
|
|
674
|
+
if (node === null || node === undefined) return "";
|
|
675
|
+
var name = node.className;
|
|
676
|
+
return typeof name === "string" ? name : "";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Whether one element is a settings navigation cell that opens the Plugins
|
|
681
|
+
* screen.
|
|
682
|
+
*
|
|
683
|
+
* Two host shapes are accepted, in the order they can be trusted:
|
|
684
|
+
* 1. the entry's OWN id, rendered as a `data-section="plugins"` attribute
|
|
685
|
+
* (the settings shell's list of `settings.section` entries);
|
|
686
|
+
* 2. the entry's translated label, read from a cell the shell marked with
|
|
687
|
+
* `aria-current` — the same attribute the shell puts on every cell it
|
|
688
|
+
* renders, so an unrelated button cannot match by accident.
|
|
689
|
+
* @param {object} node - the candidate element.
|
|
690
|
+
* @returns {boolean} whether it is the Plugins cell.
|
|
691
|
+
*/
|
|
692
|
+
function isSettingsNavItem(node) {
|
|
693
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
694
|
+
if (readNodeAttr(node, SETTINGS_NAV_ITEM_ATTR) === SETTINGS_NAV_ITEM_VALUE) return true;
|
|
695
|
+
if (readNodeAttr(node, SETTINGS_NAV_CURRENT_ATTR) === "") return false;
|
|
696
|
+
var text = readNodeText(node).trim().toLowerCase();
|
|
697
|
+
if (text === "") return false;
|
|
698
|
+
for (var index = 0; index < SETTINGS_NAV_TEXTS.length; index += 1) {
|
|
699
|
+
if (text.indexOf(SETTINGS_NAV_TEXTS[index].toLowerCase()) >= 0) return true;
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Whether one element is THIS plugin's own row in the Plugins list.
|
|
706
|
+
*
|
|
707
|
+
* The shipped id is checked first — `data-plugin-item="fonttune"` is data the host
|
|
708
|
+
* publishes, so it survives a language change; the translated label is the
|
|
709
|
+
* fallback. Only clickable shapes are accepted, so a heading that merely contains
|
|
710
|
+
* the name cannot swallow the click.
|
|
711
|
+
* @param {object} node - the candidate element.
|
|
712
|
+
* @returns {boolean} whether it is the row to open.
|
|
713
|
+
*/
|
|
714
|
+
function isPluginEntry(node) {
|
|
715
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
716
|
+
if (readNodeAttr(node, PLUGIN_ITEM_ATTR) === PLUGIN_ITEM_VALUE) return true;
|
|
717
|
+
var tag = typeof node.tagName === "string" ? node.tagName.toLowerCase() : "";
|
|
718
|
+
var role = readNodeAttr(node, "role").toLowerCase();
|
|
719
|
+
// A bare `li` is NOT clickable, so the text fallback must never accept one:
|
|
720
|
+
// a conversation message that merely QUOTES the plugin's name would become
|
|
721
|
+
// "the entry" and swallow the click (seen on a real page, where the report
|
|
722
|
+
// naming this plugin was the only match on screen).
|
|
723
|
+
if (tag !== "button" && tag !== "a" && role !== "button" && role !== "tab" && role !== "treeitem" && role !== "option" && role !== "link") {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
var text = readNodeText(node).trim().toLowerCase();
|
|
727
|
+
if (text === "") return false;
|
|
728
|
+
for (var index = 0; index < PLUGIN_TEXTS.length; index += 1) {
|
|
729
|
+
if (text.indexOf(PLUGIN_TEXTS[index].toLowerCase()) >= 0) return true;
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** The package name the host's own navigation service opens for this plugin. */
|
|
735
|
+
var PLUGIN_BUNDLE_NAME = "dsh-fonttune";
|
|
736
|
+
|
|
737
|
+
/** The main panel id the Plugins page lives in (what `layout.selectPanel` takes). */
|
|
738
|
+
var PLUGIN_PANEL_ID = "plugins";
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Whether a node sits inside this plugin's own floating panel.
|
|
742
|
+
*
|
|
743
|
+
* The panel can carry the plugin's own name, and a lookup that mistakes it for
|
|
744
|
+
* a host entry would click ourselves.
|
|
745
|
+
* @param {object} node - the candidate element.
|
|
746
|
+
* @returns {boolean} true when the node is ours, not the host's.
|
|
747
|
+
*/
|
|
748
|
+
function isOwnPanelNode(node) {
|
|
749
|
+
if (node === null || node === undefined) return true;
|
|
750
|
+
try {
|
|
751
|
+
return typeof node.closest === "function" && node.closest(".dfp-floatHost") !== null;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* THIS plugin's own card on the host's Plugins page, or null.
|
|
759
|
+
*
|
|
760
|
+
* The Plugins page publishes every entry as `data-plugin-item="<id>"` and this
|
|
761
|
+
* plugin's registration id is `fonttune`: the attribute is host-published data,
|
|
762
|
+
* so the lookup survives a language change and can never match a sentence that
|
|
763
|
+
* merely names the plugin.
|
|
764
|
+
* @param {object} doc - the document to search (a stub is fine).
|
|
765
|
+
* @returns {object|null} the card element, or null.
|
|
766
|
+
*/
|
|
767
|
+
function pluginEntryCard(doc) {
|
|
768
|
+
if (doc === null || doc === undefined || typeof doc.querySelector !== "function") return null;
|
|
769
|
+
var card = null;
|
|
770
|
+
try {
|
|
771
|
+
card = doc.querySelector("[" + PLUGIN_ITEM_ATTR + '="' + PLUGIN_ITEM_VALUE + '"]');
|
|
772
|
+
} catch (error) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
if (card === null || card === undefined) return null;
|
|
776
|
+
return isOwnPanelNode(card) ? null : card;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* The element that actually opens a plugin's page from its entry card.
|
|
781
|
+
*
|
|
782
|
+
* The card itself is a list item with no handler of its own; the host renders
|
|
783
|
+
* the title button inside it as the open gesture, and that button is the first
|
|
784
|
+
* one the card carries. A node that is already the clickable is returned as-is.
|
|
785
|
+
* @param {object} node - the card, or a clickable candidate.
|
|
786
|
+
* @returns {object|null} the element to click, or null.
|
|
787
|
+
*/
|
|
788
|
+
function pluginClickTarget(node) {
|
|
789
|
+
if (node === null || node === undefined) return null;
|
|
790
|
+
var tag = typeof node.tagName === "string" ? node.tagName.toLowerCase() : "";
|
|
791
|
+
var role = readNodeAttr(node, "role").toLowerCase();
|
|
792
|
+
if (tag === "button" || tag === "a" || role === "button" || role === "tab" || role === "link") return node;
|
|
793
|
+
var buttons = typeof node.querySelectorAll === "function" ? node.querySelectorAll("button") : [];
|
|
794
|
+
return buttons.length > 0 ? buttons[0] : null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Whether one element opens this plugin's own configuration page inside its row.
|
|
799
|
+
*
|
|
800
|
+
* Optional by design: a host that shows the configuration inline has no such tab,
|
|
801
|
+
* and the caller treats "not found" as success rather than as a failure.
|
|
802
|
+
* @param {object} node - the candidate element.
|
|
803
|
+
* @returns {boolean} whether it is the configuration tab.
|
|
804
|
+
*/
|
|
805
|
+
function isPluginConfigTab(node) {
|
|
806
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
807
|
+
var text = readNodeText(node).trim().toLowerCase();
|
|
808
|
+
if (text === "") return false;
|
|
809
|
+
for (var index = 0; index < PLUGIN_CONFIG_TEXTS.length; index += 1) {
|
|
810
|
+
if (text.indexOf(PLUGIN_CONFIG_TEXTS[index].toLowerCase()) >= 0) return true;
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Whether one element is the sidebar's settings launcher.
|
|
817
|
+
*
|
|
818
|
+
* Structural, not textual: DSH's shell renders it as a button whose
|
|
819
|
+
* `aria-haspopup` is `dialog`. Its label only has to CONFIRM the reading, so a
|
|
820
|
+
* host that renames the section list still opens the dialog, and a random
|
|
821
|
+
* `aria-haspopup="dialog"` button (a menu elsewhere on the page) is rejected by
|
|
822
|
+
* the label rather than by position.
|
|
823
|
+
* @param {object} node - the candidate element.
|
|
824
|
+
* @returns {boolean} whether it is the settings launcher.
|
|
825
|
+
*/
|
|
826
|
+
function isSettingsLauncher(node) {
|
|
827
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
828
|
+
if (readNodeAttr(node, "aria-haspopup") !== SETTINGS_LAUNCHER_HASPOPUP) return false;
|
|
829
|
+
var label = (readNodeAttr(node, "aria-label") || readNodeAttr(node, "title") || readNodeText(node))
|
|
830
|
+
.trim()
|
|
831
|
+
.toLowerCase();
|
|
832
|
+
if (label === "") return false;
|
|
833
|
+
for (var index = 0; index < SETTINGS_LAUNCHER_TEXTS.length; index += 1) {
|
|
834
|
+
if (label.indexOf(SETTINGS_LAUNCHER_TEXTS[index].toLowerCase()) >= 0) return true;
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Hand a synthetic event to a node, reporting whether the host took it.
|
|
841
|
+
*
|
|
842
|
+
* `cancelable` matters: a host may `preventDefault` the event to mark it as
|
|
843
|
+
* consumed, and that is the only acknowledgement available here.
|
|
844
|
+
* @param {object} node - the element to dispatch on.
|
|
845
|
+
* @param {string} type - the event type.
|
|
846
|
+
* @returns {boolean} whether the host ran without throwing.
|
|
847
|
+
*/
|
|
848
|
+
function dispatchSynthetic(node, type) {
|
|
849
|
+
if (node === null || node === undefined || typeof node.dispatchEvent !== "function") return false;
|
|
850
|
+
var win = typeof globalThis !== "undefined" ? globalThis : null;
|
|
851
|
+
var View = win !== null && win.MouseEvent !== undefined ? win.MouseEvent : null;
|
|
852
|
+
var event = null;
|
|
853
|
+
try {
|
|
854
|
+
event = View !== null
|
|
855
|
+
? new View(type, { bubbles: true, cancelable: true, view: win })
|
|
856
|
+
: { type: type, bubbles: true, cancelable: true, defaultPrevented: false, preventDefault: function () {} };
|
|
857
|
+
} catch (error) {
|
|
858
|
+
event = { type: type, bubbles: true, cancelable: true, defaultPrevented: false, preventDefault: function () {} };
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
node.dispatchEvent(event);
|
|
862
|
+
return true;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Resolve the shortest route to DSH's Plugins settings screen — or null.
|
|
870
|
+
*
|
|
871
|
+
* The panel's settings button must not guess at a fragile long selector, and it
|
|
872
|
+
* must not throw when the host is not there at all (the memory scope, a host
|
|
873
|
+
* line this package no longer supports, a page rendered before the shell
|
|
874
|
+
* mounted). So the lookup is a list of NARROWED plans, each with the evidence
|
|
875
|
+
* it rests on, and a `run` that performs the step:
|
|
876
|
+
*
|
|
877
|
+
* 1. `section-attr` — the Plugins cell, found by the entry's own id rendered as
|
|
878
|
+
* a `data-*` attribute. Stable by construction: it is data the host
|
|
879
|
+
* publishes, not text it translates.
|
|
880
|
+
* 2. `section-text` — the same cell found by its translated label among the
|
|
881
|
+
* cells the shell marked `aria-current="true"`. Same click, weaker key; it
|
|
882
|
+
* is what a host that renders no per-entry attribute needs.
|
|
883
|
+
* 3. `launcher` — no cell yet, so the sidebar's settings launcher is clicked
|
|
884
|
+
* first and the cell is looked for again on a later frame. The caller hands
|
|
885
|
+
* the retry in as `retry`, so this function stays pure about the DOM it can
|
|
886
|
+
* see right now.
|
|
887
|
+
*
|
|
888
|
+
* Every step is a plain `click()`, which is what a user does; nothing here pokes
|
|
889
|
+
* at a framework's internals, and nothing depends on element coordinates.
|
|
890
|
+
*
|
|
891
|
+
* @param {object} doc - the document to search (a stub is fine).
|
|
892
|
+
* @param {{retry?: (select: Function) => boolean}} [options] - retry runner.
|
|
893
|
+
* @returns {{kind: string, why: string, node: object, run: Function}|null} the plan, or null.
|
|
894
|
+
*/
|
|
895
|
+
function findSettingsEntry(doc, options) {
|
|
896
|
+
if (doc === null || doc === undefined || typeof doc !== "object") return null;
|
|
897
|
+
var settings = options !== null && typeof options === "object" ? options : {};
|
|
898
|
+
var all = function () {
|
|
899
|
+
var found = typeof doc.querySelectorAll === "function" ? doc.querySelectorAll("button") : null;
|
|
900
|
+
return found === null || found === undefined ? [] : found;
|
|
901
|
+
};
|
|
902
|
+
var first = function (predicate) {
|
|
903
|
+
var nodes = all();
|
|
904
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
905
|
+
if (predicate(nodes[index])) return nodes[index];
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
};
|
|
909
|
+
var cellWithAttr = function () {
|
|
910
|
+
return first(function (node) {
|
|
911
|
+
return readNodeAttr(node, SETTINGS_NAV_ITEM_ATTR) === SETTINGS_NAV_ITEM_VALUE;
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
var cellByText = function () {
|
|
915
|
+
return first(function (node) {
|
|
916
|
+
return readNodeAttr(node, SETTINGS_NAV_CURRENT_ATTR) !== "" && isSettingsNavItem(node);
|
|
917
|
+
});
|
|
918
|
+
};
|
|
919
|
+
var launcher = function () {
|
|
920
|
+
return first(isSettingsLauncher);
|
|
921
|
+
};
|
|
922
|
+
// The plugin row and its configuration tab are not necessarily `button`s, so the
|
|
923
|
+
// search is by shape instead of by tag alone.
|
|
924
|
+
var firstIn = function (selector, predicate) {
|
|
925
|
+
var nodes = typeof doc.querySelectorAll === "function" ? doc.querySelectorAll(selector) : [];
|
|
926
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
927
|
+
if (predicate(nodes[index])) return nodes[index];
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
};
|
|
931
|
+
var pluginEntry = function () {
|
|
932
|
+
var found = firstIn(PLUGIN_ITEM_CANDIDATES, function (node) {
|
|
933
|
+
// Our own floating panel may carry the plugin's own name; never click ourselves.
|
|
934
|
+
try {
|
|
935
|
+
if (typeof node.closest === "function" && node.closest(".dfp-floatHost") !== null) return false;
|
|
936
|
+
} catch (error) {
|
|
937
|
+
// A node without closest cannot be ours anyway.
|
|
938
|
+
}
|
|
939
|
+
return isPluginEntry(node);
|
|
940
|
+
});
|
|
941
|
+
if (found === null) return null;
|
|
942
|
+
// The entry card is a list item with no handler of its own: the click lands
|
|
943
|
+
// on the title button the host renders inside it.
|
|
944
|
+
return pluginClickTarget(found) || found;
|
|
945
|
+
};
|
|
946
|
+
var pluginConfigTab = function () {
|
|
947
|
+
return firstIn(PLUGIN_TAB_CANDIDATES, isPluginConfigTab);
|
|
948
|
+
};
|
|
949
|
+
// A plan that just clicks the cell it names.
|
|
950
|
+
var sectionPlan = function (kind, why, node) {
|
|
951
|
+
return {
|
|
952
|
+
kind: kind,
|
|
953
|
+
why: why,
|
|
954
|
+
node: node,
|
|
955
|
+
run: function () {
|
|
956
|
+
return dispatchSynthetic(node, "click");
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
/**
|
|
961
|
+
* Give a plan the tail every route shares: open the Plugins cell, then THIS
|
|
962
|
+
* plugin's own row inside it, then its configuration tab when the host has one.
|
|
963
|
+
*
|
|
964
|
+
* The row mounts one commit after the cell, so the waiting belongs to the
|
|
965
|
+
* caller's `retry` runner and this function stays pure about what it can see.
|
|
966
|
+
* A host with no separate tab is normal, not a failure: the row click is the
|
|
967
|
+
* answer in that case.
|
|
968
|
+
* @param {object} plan - a plan that opens the Plugins screen.
|
|
969
|
+
* @returns {object} the same plan with the tail attached.
|
|
970
|
+
*/
|
|
971
|
+
var withPluginEntry = function (plan) {
|
|
972
|
+
var opened = plan.run;
|
|
973
|
+
plan.run = function () {
|
|
974
|
+
var reached = opened();
|
|
975
|
+
if (typeof settings.retry !== "function") return reached;
|
|
976
|
+
var entry = settings.retry(pluginEntry);
|
|
977
|
+
if (entry === null) return reached;
|
|
978
|
+
return settings.retry(pluginConfigTab) || entry;
|
|
979
|
+
};
|
|
980
|
+
return plan;
|
|
981
|
+
};
|
|
982
|
+
// The plugin's own settings may be its own SIDEBAR entry — a page of its own,
|
|
983
|
+
// not a section of the settings dialog. When the shell publishes one, clicking
|
|
984
|
+
// it opens the configuration directly and there is no settings route to walk.
|
|
985
|
+
// The user's host lists the plugin in the sidebar, and this is that entry.
|
|
986
|
+
var sidebarEntry = pluginEntry();
|
|
987
|
+
if (sidebarEntry !== null) {
|
|
988
|
+
return {
|
|
989
|
+
kind: "sidebar-entry",
|
|
990
|
+
why: "the plugin ships its own entry; it opens the configuration directly",
|
|
991
|
+
node: sidebarEntry,
|
|
992
|
+
run: function () {
|
|
993
|
+
return dispatchSynthetic(sidebarEntry, "click");
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
var canonical = cellWithAttr();
|
|
998
|
+
if (canonical !== null) {
|
|
999
|
+
return withPluginEntry(
|
|
1000
|
+
sectionPlan("section-attr", "the Plugins entry ships its own id as a data attribute", canonical)
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
var byText = cellByText();
|
|
1004
|
+
if (byText !== null) {
|
|
1005
|
+
return withPluginEntry(
|
|
1006
|
+
sectionPlan("section-text", "the Plugins entry was matched by its translated label", byText)
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
var opener = launcher();
|
|
1010
|
+
if (opener === null) return null;
|
|
1011
|
+
return withPluginEntry({
|
|
1012
|
+
kind: "launcher",
|
|
1013
|
+
why: "only the sidebar's settings launcher is on screen; the entry is picked after it opens",
|
|
1014
|
+
node: opener,
|
|
1015
|
+
run: function () {
|
|
1016
|
+
var reached = dispatchSynthetic(opener, "click");
|
|
1017
|
+
if (typeof settings.retry !== "function") return reached;
|
|
1018
|
+
return settings.retry(cellWithAttr) || settings.retry(cellByText) || reached;
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Normalize the floating panel's stored position.
|
|
1025
|
+
* @param {unknown} value - candidate `"x,y"` text.
|
|
1026
|
+
* @returns {string} the normalized `"x,y"` text, or "" when unusable.
|
|
1027
|
+
*/
|
|
1028
|
+
function normalizePanelPos(value) {
|
|
1029
|
+
if (typeof value !== "string") return "";
|
|
1030
|
+
var parts = value.trim().split(",");
|
|
1031
|
+
if (parts.length !== 2) return "";
|
|
1032
|
+
var x = Math.round(Number(parts[0]));
|
|
1033
|
+
var y = Math.round(Number(parts[1]));
|
|
1034
|
+
if (!isFinite(x) || !isFinite(y)) return "";
|
|
1035
|
+
x = Math.min(PANEL_POS_MAX, Math.max(0, x));
|
|
1036
|
+
y = Math.min(PANEL_POS_MAX, Math.max(0, y));
|
|
1037
|
+
return x + "," + y;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Read the floating panel's stored position for placement.
|
|
1042
|
+
* @param {unknown} value - candidate `"x,y"` text.
|
|
1043
|
+
* @returns {{x: number, y: number}|null} the coordinates, or null when unset.
|
|
1044
|
+
*/
|
|
1045
|
+
function parsePanelPos(value) {
|
|
1046
|
+
var text = normalizePanelPos(value);
|
|
1047
|
+
if (text === "") return null;
|
|
1048
|
+
var parts = text.split(",");
|
|
1049
|
+
return { x: Number(parts[0]), y: Number(parts[1]) };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Normalize the floating panel's remembered corner.
|
|
1054
|
+
* @param {unknown} value - candidate corner name.
|
|
1055
|
+
* @returns {string} one of `PANEL_CORNERS`, or "" when it names none.
|
|
1056
|
+
*/
|
|
1057
|
+
function normalizePanelCorner(value) {
|
|
1058
|
+
if (typeof value !== "string") return "";
|
|
1059
|
+
var text = value.trim().toLowerCase();
|
|
1060
|
+
return PANEL_CORNERS.indexOf(text) >= 0 ? text : "";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Read the floating panel's remembered corner for placement.
|
|
1065
|
+
* @param {unknown} value - candidate corner name.
|
|
1066
|
+
* @returns {string|null} the corner, or null when the document names none (an
|
|
1067
|
+
* older document, or one nothing has settled yet).
|
|
1068
|
+
*/
|
|
1069
|
+
function parsePanelCorner(value) {
|
|
1070
|
+
var text = normalizePanelCorner(value);
|
|
1071
|
+
return text === "" ? null : text;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Normalize the floating panel's stored size.
|
|
1076
|
+
*
|
|
1077
|
+
* A height of 0 means "auto" (the card sizes to its content); any other
|
|
1078
|
+
* height clamps into the resizable window, like the width always does.
|
|
1079
|
+
* @param {unknown} value - candidate `"w,h"` text.
|
|
1080
|
+
* @returns {string} the normalized `"w,h"` text, or "" when unusable.
|
|
1081
|
+
*/
|
|
1082
|
+
function normalizePanelSize(value) {
|
|
1083
|
+
if (typeof value !== "string") return "";
|
|
1084
|
+
var parts = value.trim().split(",");
|
|
1085
|
+
if (parts.length !== 2) return "";
|
|
1086
|
+
var w = Math.round(Number(parts[0]));
|
|
1087
|
+
var h = Math.round(Number(parts[1]));
|
|
1088
|
+
if (!isFinite(w) || !isFinite(h)) return "";
|
|
1089
|
+
w = Math.min(PANEL_SIZE_MAX_W, Math.max(PANEL_SIZE_MIN_W, w));
|
|
1090
|
+
if (h !== 0) h = Math.min(PANEL_SIZE_MAX_H, Math.max(PANEL_SIZE_MIN_H, h));
|
|
1091
|
+
return w + "," + h;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Read the floating panel's stored size for layout.
|
|
1096
|
+
* @param {unknown} value - candidate `"w,h"` text.
|
|
1097
|
+
* @returns {{w: number, h: number}|null} the size, or null when unset.
|
|
1098
|
+
*/
|
|
1099
|
+
function parsePanelSize(value) {
|
|
1100
|
+
var text = normalizePanelSize(value);
|
|
1101
|
+
if (text === "") return null;
|
|
1102
|
+
var parts = text.split(",");
|
|
1103
|
+
return { w: Number(parts[0]), h: Number(parts[1]) };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* One rectangle as four finite edges, in viewport coordinates.
|
|
1108
|
+
*
|
|
1109
|
+
* A DOMRect, a plain box and anything in between is accepted; the width and
|
|
1110
|
+
* height are optional, because a DOMRect always carries `right`/`bottom` while a
|
|
1111
|
+
* hand-built box usually carries `width`/`height` instead.
|
|
1112
|
+
* @param {unknown} rect - the value to read.
|
|
1113
|
+
* @returns {{left: number, top: number, right: number, bottom: number}|null} the
|
|
1114
|
+
* box, or null when the value holds no usable rectangle.
|
|
1115
|
+
*/
|
|
1116
|
+
function floatBoxOf(rect) {
|
|
1117
|
+
if (rect === null || typeof rect !== "object") return null;
|
|
1118
|
+
var left = Number(rect.left);
|
|
1119
|
+
var top = Number(rect.top);
|
|
1120
|
+
var right = rect.right === undefined ? left + Number(rect.width) : Number(rect.right);
|
|
1121
|
+
var bottom = rect.bottom === undefined ? top + Number(rect.height) : Number(rect.bottom);
|
|
1122
|
+
if (!isFinite(left) || !isFinite(top) || !isFinite(right) || !isFinite(bottom)) return null;
|
|
1123
|
+
return { left: left, top: top, right: right, bottom: bottom };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* The rectangle the dot snaps to, drags within and reads its own quadrant from.
|
|
1128
|
+
*
|
|
1129
|
+
* The dot belongs INSIDE the conversation, not on the chrome around it: with a
|
|
1130
|
+
* sidebar on either side and a top bar, the window's four corners sit on bars the
|
|
1131
|
+
* user never wanted a control on. The caller therefore hands in its candidates in
|
|
1132
|
+
* priority order — the scroll container the conversation lives in first, the
|
|
1133
|
+
* prose element itself second — and the first one that is a usable region wins.
|
|
1134
|
+
*
|
|
1135
|
+
* A candidate is usable when, after being CLIPPED to the viewport, it is at least
|
|
1136
|
+
* `FLOAT_BOUNDS_MIN` on both axes: a container that hangs off the screen would
|
|
1137
|
+
* otherwise snap the dot to a corner nobody can reach, and one smaller than the
|
|
1138
|
+
* dot cannot hold it at all. Nothing usable (a settings page has no conversation
|
|
1139
|
+
* at all) falls back to the whole window, which is what the panel did before.
|
|
1140
|
+
*
|
|
1141
|
+
* The WINNING CANDIDATE is reported alongside the bounds, not only the numbers:
|
|
1142
|
+
* "which rectangle is the region" and "which element's size change means the
|
|
1143
|
+
* region changed" are one question, and the caller that watches for a region
|
|
1144
|
+
* change has to ask it the same way (see `regionElement` in the client half).
|
|
1145
|
+
* @param {Array<unknown>} rects - candidate rectangles, best first.
|
|
1146
|
+
* @param {{width: number, height: number}} viewport - the window size.
|
|
1147
|
+
* @returns {{index: number, bounds: {left: number, top: number, right: number,
|
|
1148
|
+
* bottom: number, width: number, height: number}}} the index of the rectangle
|
|
1149
|
+
* the region was taken from (-1 when the whole window stands in) and the
|
|
1150
|
+
* bounds, in viewport coordinates.
|
|
1151
|
+
*/
|
|
1152
|
+
function floatBoundsPick(rects, viewport) {
|
|
1153
|
+
var vw = viewport === null || viewport === undefined ? NaN : Number(viewport.width);
|
|
1154
|
+
var vh = viewport === null || viewport === undefined ? NaN : Number(viewport.height);
|
|
1155
|
+
if (!isFinite(vw) || vw <= 0) vw = FLOAT_VIEWPORT_W;
|
|
1156
|
+
if (!isFinite(vh) || vh <= 0) vh = FLOAT_VIEWPORT_H;
|
|
1157
|
+
var full = { left: 0, top: 0, right: vw, bottom: vh, width: vw, height: vh };
|
|
1158
|
+
var list = Array.isArray(rects) ? rects : [];
|
|
1159
|
+
for (var index = 0; index < list.length; index += 1) {
|
|
1160
|
+
var box = floatBoxOf(list[index]);
|
|
1161
|
+
if (box === null) continue;
|
|
1162
|
+
var left = Math.max(0, Math.min(vw, box.left));
|
|
1163
|
+
var top = Math.max(0, Math.min(vh, box.top));
|
|
1164
|
+
var right = Math.max(0, Math.min(vw, box.right));
|
|
1165
|
+
var bottom = Math.max(0, Math.min(vh, box.bottom));
|
|
1166
|
+
var width = right - left;
|
|
1167
|
+
var height = bottom - top;
|
|
1168
|
+
if (width < FLOAT_BOUNDS_MIN || height < FLOAT_BOUNDS_MIN) continue;
|
|
1169
|
+
return {
|
|
1170
|
+
index: index,
|
|
1171
|
+
bounds: { left: left, top: top, right: right, bottom: bottom, width: width, height: height },
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
return { index: -1, bounds: full };
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* The bounds alone, for every caller that only draws with them.
|
|
1179
|
+
* @param {Array<unknown>} rects - candidate rectangles, best first.
|
|
1180
|
+
* @param {{width: number, height: number}} viewport - the window size.
|
|
1181
|
+
* @returns {{left: number, top: number, right: number, bottom: number,
|
|
1182
|
+
* width: number, height: number}} the bounds, in viewport coordinates.
|
|
1183
|
+
*/
|
|
1184
|
+
function floatBoundsFrom(rects, viewport) {
|
|
1185
|
+
return floatBoundsPick(rects, viewport).bounds;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Whether a second reading of the winning rectangle is the same shape as the last.
|
|
1190
|
+
*
|
|
1191
|
+
* The fallback watcher asks this every tick and nothing else, so it is stated on
|
|
1192
|
+
* FOUR numbers — the two edges — and never on anything derived from them. A caller
|
|
1193
|
+
* that re-read the region, resolved styles or compared a `width`/`height` pair as
|
|
1194
|
+
* well would be doing work the question does not need, twice a second, on a page
|
|
1195
|
+
* that is usually not moving at all.
|
|
1196
|
+
*
|
|
1197
|
+
* A rectangle that could not be read (null, or absent) is never the same shape as
|
|
1198
|
+
* anything: an unreadable region is a change, not a confirmation.
|
|
1199
|
+
* @param {object|null} a - the rectangle read last time.
|
|
1200
|
+
* @param {object|null} b - the rectangle read now.
|
|
1201
|
+
* @returns {boolean} true when both readings name one and the same box.
|
|
1202
|
+
*/
|
|
1203
|
+
function sameWatchBounds(a, b) {
|
|
1204
|
+
if (a === null || a === undefined || b === null || b === undefined) return false;
|
|
1205
|
+
return a.left === b.left && a.top === b.top && a.right === b.right && a.bottom === b.bottom;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Whether every box an observer is holding is still in the document.
|
|
1210
|
+
*
|
|
1211
|
+
* An observer holds a NODE, and a node the host replaced (a re-render swapping the
|
|
1212
|
+
* conversation element, a session switch) is detached: it keeps its size forever
|
|
1213
|
+
* and never reports again. So a target list may look unchanged and still be a list
|
|
1214
|
+
* that has to be resolved anew — which is the whole reason the body is watched
|
|
1215
|
+
* alongside it (see `watchRegion` in the client half).
|
|
1216
|
+
* @param {object} doc - the document to ask (`document`, in the client half).
|
|
1217
|
+
* @param {Array<object>} targets - the boxes currently observed.
|
|
1218
|
+
* @returns {boolean} true when nothing in the list was taken out of the tree.
|
|
1219
|
+
*/
|
|
1220
|
+
function floatTargetsConnected(doc, targets) {
|
|
1221
|
+
if (doc === null || doc === undefined || typeof doc.contains !== "function") return true;
|
|
1222
|
+
if (Array.isArray(targets) === false) return true;
|
|
1223
|
+
try {
|
|
1224
|
+
for (var index = 0; index < targets.length; index += 1) {
|
|
1225
|
+
if (doc.contains(targets[index]) === false) return false;
|
|
1226
|
+
}
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Which quadrant a point sits in, measured inside the bounds it lives in.
|
|
1235
|
+
*
|
|
1236
|
+
* The bounds matter: the expand direction is decided by the edges the dot is
|
|
1237
|
+
* NEAREST, and NEAREST is a question about the conversation — the region's own
|
|
1238
|
+
* middle, not the window's. Ties fall left/top.
|
|
1239
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1240
|
+
* @param {number} x - CSS pixels from the left.
|
|
1241
|
+
* @param {number} y - CSS pixels from the top.
|
|
1242
|
+
* @returns {string} one of "tl", "tr", "bl", "br".
|
|
1243
|
+
*/
|
|
1244
|
+
function floatQuadrantIn(bounds, x, y) {
|
|
1245
|
+
var box = floatBoxOf(bounds);
|
|
1246
|
+
if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
|
|
1247
|
+
var left = x < (box.left + box.right) / 2;
|
|
1248
|
+
var top = y < (box.top + box.bottom) / 2;
|
|
1249
|
+
if (left && top) return "tl";
|
|
1250
|
+
if (!left && top) return "tr";
|
|
1251
|
+
if (left && !top) return "bl";
|
|
1252
|
+
return "br";
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Which viewport quadrant a point sits in (ties fall left/top).
|
|
1257
|
+
* @param {number} x - CSS pixels from the left.
|
|
1258
|
+
* @param {number} y - CSS pixels from the top.
|
|
1259
|
+
* @param {number} vw - viewport width.
|
|
1260
|
+
* @param {number} vh - viewport height.
|
|
1261
|
+
* @returns {string} one of "tl", "tr", "bl", "br".
|
|
1262
|
+
*/
|
|
1263
|
+
function floatQuadrant(x, y, vw, vh) {
|
|
1264
|
+
return floatQuadrantIn({ left: 0, top: 0, right: vw, bottom: vh }, x, y);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Where the panel grows from, given the dot's quadrant: always away from
|
|
1269
|
+
* the nearest edges, so the card lands inside the viewport.
|
|
1270
|
+
* @param {string} quadrant - one of "tl", "tr", "bl", "br".
|
|
1271
|
+
* @returns {string} one of "right-down", "left-down", "right-up", "left-up".
|
|
1272
|
+
*/
|
|
1273
|
+
function floatExpandDirection(quadrant) {
|
|
1274
|
+
if (quadrant === "tl") return "right-down";
|
|
1275
|
+
if (quadrant === "tr") return "left-down";
|
|
1276
|
+
if (quadrant === "bl") return "right-up";
|
|
1277
|
+
return "left-up";
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* The expand animation's origin corner, on the dot's side.
|
|
1282
|
+
* @param {string} quadrant - one of "tl", "tr", "bl", "br".
|
|
1283
|
+
* @returns {string} a CSS `transform-origin` value.
|
|
1284
|
+
*/
|
|
1285
|
+
function floatTransformOrigin(quadrant) {
|
|
1286
|
+
if (quadrant === "tl") return "0 0";
|
|
1287
|
+
if (quadrant === "tr") return "100% 0";
|
|
1288
|
+
if (quadrant === "bl") return "0 100%";
|
|
1289
|
+
return "100% 100%";
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Which corner of the card lands on the dot, and where the expansion starts.
|
|
1294
|
+
*
|
|
1295
|
+
* The card's nearest corner COINCIDES with the dot's own corner: the open card
|
|
1296
|
+
* is clipped, at the first frame, down to exactly the dot's rectangle, so the
|
|
1297
|
+
* panel reads as the dot unfolded rather than as a second object. The shared
|
|
1298
|
+
* corner is also the expand animation's origin, so the card grows toward the
|
|
1299
|
+
* opposite corner and shrinks back into the dot on the way out.
|
|
1300
|
+
* @param {string} quadrant - one of "tl", "tr", "bl", "br".
|
|
1301
|
+
* @returns {{h: string, v: string, origin: string}} the anchored edges (CSS
|
|
1302
|
+
* sides, always "0") and the `transform-origin` on that shared corner.
|
|
1303
|
+
*/
|
|
1304
|
+
function floatCardAnchor(quadrant) {
|
|
1305
|
+
var left = quadrant === "tl" || quadrant === "bl";
|
|
1306
|
+
var top = quadrant === "tl" || quadrant === "tr";
|
|
1307
|
+
return {
|
|
1308
|
+
h: left ? "left" : "right",
|
|
1309
|
+
v: top ? "top" : "bottom",
|
|
1310
|
+
origin: floatTransformOrigin(quadrant),
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* The clip-path the card's expand starts from — and its collapse ends on.
|
|
1316
|
+
*
|
|
1317
|
+
* The card is anchored on the dot's own corner, so the dot's rectangle inside
|
|
1318
|
+
* the card is `dot` by `dot` pixels flush with that corner. Clipping the card
|
|
1319
|
+
* down to it gives the "small rounded rectangle" the panel unfolds out of; the
|
|
1320
|
+
* animation then carries the same four lengths to zero, which is what makes the
|
|
1321
|
+
* panel LOOK like it grows instead of merely fading in. The radius is the dot's
|
|
1322
|
+
* own (half its diameter), so the first frame reads as the dot's circle.
|
|
1323
|
+
* @param {number} width - the card's border-box width, in CSS pixels.
|
|
1324
|
+
* @param {number} height - the card's border-box height, in CSS pixels.
|
|
1325
|
+
* @param {string} quadrant - one of "tl", "tr", "bl", "br".
|
|
1326
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1327
|
+
* @returns {string} a CSS `inset()` clip-path.
|
|
1328
|
+
*/
|
|
1329
|
+
function floatClipStart(width, height, quadrant, dot) {
|
|
1330
|
+
var size = dot === undefined ? FLOAT_DOT : dot;
|
|
1331
|
+
var anchor = floatCardAnchor(quadrant);
|
|
1332
|
+
var w = Math.max(0, Math.round(width));
|
|
1333
|
+
var h = Math.max(0, Math.round(height));
|
|
1334
|
+
var far = Math.max(0, w - size);
|
|
1335
|
+
var low = Math.max(0, h - size);
|
|
1336
|
+
var top = anchor.v === "top" ? 0 : low;
|
|
1337
|
+
var right = anchor.h === "left" ? far : 0;
|
|
1338
|
+
var bottom = anchor.v === "top" ? low : 0;
|
|
1339
|
+
var left = anchor.h === "left" ? 0 : far;
|
|
1340
|
+
return (
|
|
1341
|
+
"inset(" + top + "px " + right + "px " + bottom + "px " + left +
|
|
1342
|
+
"px round " + Math.round(size / 2) + "px)"
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* The clip-path the expanded card rests at: its own box, its own radius.
|
|
1348
|
+
*
|
|
1349
|
+
* The four lengths are zero, so nothing is cut away — but the value has to be a
|
|
1350
|
+
* real shape rather than `none`, because `none` does not interpolate and the
|
|
1351
|
+
* unfold would snap instead of growing. It is therefore only the shape the
|
|
1352
|
+
* animation lands ON: the card drops the clip for good once it has arrived
|
|
1353
|
+
* (`isRestClip` is how it knows), because clipping the card's own border box a
|
|
1354
|
+
* second time — `border-radius` already did it once — multiplies the
|
|
1355
|
+
* anti-aliasing along the rounded corners and cuts the card's own shadow away.
|
|
1356
|
+
* @returns {string} a CSS `inset()` clip-path.
|
|
1357
|
+
*/
|
|
1358
|
+
function floatClipRest() {
|
|
1359
|
+
var round = FLOAT_CARD_RADIUS + "px";
|
|
1360
|
+
return "inset(0px 0px 0px 0px round " + round + ")";
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Whether a computed `clip-path` has nothing left to cut away.
|
|
1365
|
+
*
|
|
1366
|
+
* `none` counts, and so does the resting `inset()` the card animates to: its
|
|
1367
|
+
* four lengths are zero, which is the element's own border box. Anything that
|
|
1368
|
+
* still hides part of the element — a clip in flight, a clip to the dot's
|
|
1369
|
+
* rectangle — does not.
|
|
1370
|
+
* @param {unknown} value - a computed `clip-path`.
|
|
1371
|
+
* @returns {boolean} true when the clip hides nothing.
|
|
1372
|
+
*/
|
|
1373
|
+
function isRestClip(value) {
|
|
1374
|
+
if (typeof value !== "string") return false;
|
|
1375
|
+
var text = value.trim();
|
|
1376
|
+
if (text === "" || text === "none" || text === "auto") return true;
|
|
1377
|
+
var shape = /^inset\(([^)]*)\)$/.exec(text);
|
|
1378
|
+
if (shape === null) return false;
|
|
1379
|
+
var halves = shape[1].split("round");
|
|
1380
|
+
var found = halves[0].match(/-?\d+(?:\.\d+)?/g);
|
|
1381
|
+
if (found === null) return false;
|
|
1382
|
+
var values = found.map(Number);
|
|
1383
|
+
if (values.length > 4) return false;
|
|
1384
|
+
// CSS repeats the shorter lists: 1 -> all four, 2 -> vertical/horizontal,
|
|
1385
|
+
// 3 -> top/horizontal/bottom.
|
|
1386
|
+
var four;
|
|
1387
|
+
if (values.length === 1) four = [values[0], values[0], values[0], values[0]];
|
|
1388
|
+
else if (values.length === 2) four = [values[0], values[1], values[0], values[1]];
|
|
1389
|
+
else if (values.length === 3) four = [values[0], values[1], values[2], values[1]];
|
|
1390
|
+
else four = values;
|
|
1391
|
+
for (var index = 0; index < four.length; index += 1) {
|
|
1392
|
+
if (Math.abs(four[index]) > 0.5) return false;
|
|
1393
|
+
}
|
|
1394
|
+
if (halves.length < 2) return true;
|
|
1395
|
+
var round = halves[1].match(/-?\d+(?:\.\d+)?/g);
|
|
1396
|
+
if (round === null) return false;
|
|
1397
|
+
return Math.abs(Number(round[0]) - FLOAT_CARD_RADIUS) <= 0.5;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* The four snapped corners of a region, as the one box they are laid out in.
|
|
1402
|
+
*
|
|
1403
|
+
* The margin is inward from the region's own edges and the dot's diameter is
|
|
1404
|
+
* held clear of the far ones, so a corner of this box is where a dot of that
|
|
1405
|
+
* size REST on that corner of the region. Everything about corners — which one
|
|
1406
|
+
* is nearest, and where a named one is — is read from these four numbers, so the
|
|
1407
|
+
* two questions can never disagree.
|
|
1408
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1409
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1410
|
+
* @param {number} [margin] - the edge margin (defaults to FLOAT_MARGIN).
|
|
1411
|
+
* @returns {{left: number, top: number, right: number, bottom: number}} the
|
|
1412
|
+
* four edges a snapped dot's top-left may take.
|
|
1413
|
+
*/
|
|
1414
|
+
function floatCornersOf(bounds, dot, margin) {
|
|
1415
|
+
var size = dot === undefined ? FLOAT_DOT : dot;
|
|
1416
|
+
var gap = margin === undefined ? FLOAT_MARGIN : margin;
|
|
1417
|
+
var box = floatBoxOf(bounds);
|
|
1418
|
+
if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
|
|
1419
|
+
var left = box.left + Math.max(0, gap);
|
|
1420
|
+
var top = box.top + Math.max(0, gap);
|
|
1421
|
+
return {
|
|
1422
|
+
left: left,
|
|
1423
|
+
top: top,
|
|
1424
|
+
right: Math.max(left, box.right - gap - size),
|
|
1425
|
+
bottom: Math.max(top, box.bottom - gap - size),
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Which of a region's four corners a point is nearest.
|
|
1431
|
+
*
|
|
1432
|
+
* Measured between the SNAPPED corners, which is the box `floatCornerPoint`
|
|
1433
|
+
* places a named corner in: "the nearest corner" and "that corner's position"
|
|
1434
|
+
* are then one question with one answer. Ties fall left/top.
|
|
1435
|
+
*
|
|
1436
|
+
* This is the ONLY way a corner is ever chosen: everywhere else the chosen one
|
|
1437
|
+
* is read back from the document (see `PANEL_CORNER_FIELD`), because a region
|
|
1438
|
+
* that changed shape must move the dot to its own corner rather than let the
|
|
1439
|
+
* old region's coordinates pick a different one.
|
|
1440
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1441
|
+
* @param {number} x - CSS pixels from the left.
|
|
1442
|
+
* @param {number} y - CSS pixels from the top.
|
|
1443
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1444
|
+
* @param {number} [margin] - the edge margin (defaults to FLOAT_MARGIN).
|
|
1445
|
+
* @returns {string} one of "tl", "tr", "bl", "br".
|
|
1446
|
+
*/
|
|
1447
|
+
function floatNearestCorner(bounds, x, y, dot, margin) {
|
|
1448
|
+
var edges = floatCornersOf(bounds, dot, margin);
|
|
1449
|
+
var left = x < (edges.left + edges.right) / 2;
|
|
1450
|
+
var top = y < (edges.top + edges.bottom) / 2;
|
|
1451
|
+
if (left && top) return "tl";
|
|
1452
|
+
if (!left && top) return "tr";
|
|
1453
|
+
if (left && !top) return "bl";
|
|
1454
|
+
return "br";
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* The coordinates of one NAMED corner of a region, margin and all.
|
|
1459
|
+
*
|
|
1460
|
+
* This is where the dot's identity becomes a position, and the one place a
|
|
1461
|
+
* region change is answered from: whatever happened to the region, a dot that
|
|
1462
|
+
* lives in `br` is put back on the new `br`. A name that is not one of the four
|
|
1463
|
+
* falls back to `br`, the corner the panel is parked in before anything at all
|
|
1464
|
+
* is stored.
|
|
1465
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1466
|
+
* @param {string} corner - one of "tl", "tr", "bl", "br".
|
|
1467
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1468
|
+
* @param {number} [margin] - the edge margin (defaults to FLOAT_MARGIN).
|
|
1469
|
+
* @returns {{x: number, y: number}} the snapped top-left of the dot.
|
|
1470
|
+
*/
|
|
1471
|
+
function floatCornerPoint(bounds, corner, dot, margin) {
|
|
1472
|
+
var edges = floatCornersOf(bounds, dot, margin);
|
|
1473
|
+
var left = corner === "tl" || corner === "bl";
|
|
1474
|
+
var top = corner === "tl" || corner === "tr";
|
|
1475
|
+
return {
|
|
1476
|
+
x: Math.round(left ? edges.left : edges.right),
|
|
1477
|
+
y: Math.round(top ? edges.top : edges.bottom),
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Snap a point to the nearest corner of a region, keeping the margin clear.
|
|
1483
|
+
*
|
|
1484
|
+
* The candidate corners are the BOUNDS' own, so a dot left on the chrome while
|
|
1485
|
+
* a sidebar grew is pulled onto the conversation's nearest corner — the same
|
|
1486
|
+
* corner the next expand grows away from.
|
|
1487
|
+
*
|
|
1488
|
+
* Asked when the USER picks a corner (a drag released, a document that names
|
|
1489
|
+
* none): everywhere else the corner is remembered rather than re-derived, or a
|
|
1490
|
+
* region change would silently re-pick it (`floatNearestCorner`).
|
|
1491
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1492
|
+
* @param {number} x - CSS pixels from the left.
|
|
1493
|
+
* @param {number} y - CSS pixels from the top.
|
|
1494
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1495
|
+
* @param {number} [margin] - the edge margin (defaults to FLOAT_MARGIN).
|
|
1496
|
+
* @returns {{x: number, y: number}} the snapped top-left of the dot.
|
|
1497
|
+
*/
|
|
1498
|
+
function floatSnapTo(bounds, x, y, dot, margin) {
|
|
1499
|
+
return floatCornerPoint(bounds, floatNearestCorner(bounds, x, y, dot, margin), dot, margin);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Snap a point to the nearest viewport corner, keeping the margin clear.
|
|
1504
|
+
* @param {number} x - CSS pixels from the left.
|
|
1505
|
+
* @param {number} y - CSS pixels from the top.
|
|
1506
|
+
* @param {number} vw - viewport width.
|
|
1507
|
+
* @param {number} vh - viewport height.
|
|
1508
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1509
|
+
* @param {number} [margin] - the edge margin (defaults to FLOAT_MARGIN).
|
|
1510
|
+
* @returns {{x: number, y: number}} the snapped top-left of the dot.
|
|
1511
|
+
*/
|
|
1512
|
+
function floatSnapCorner(x, y, vw, vh, dot, margin) {
|
|
1513
|
+
return floatSnapTo({ left: 0, top: 0, right: vw, bottom: vh }, x, y, dot, margin);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Whether a pointer travel counts as a drag (past the click threshold).
|
|
1518
|
+
* @param {number} dx - horizontal travel in CSS pixels.
|
|
1519
|
+
* @param {number} dy - vertical travel in CSS pixels.
|
|
1520
|
+
* @param {number} [threshold] - the limit (defaults to FLOAT_DRAG_THRESHOLD).
|
|
1521
|
+
* @returns {boolean} true when the travel is a drag, not a click.
|
|
1522
|
+
*/
|
|
1523
|
+
function floatDragExceeded(dx, dy, threshold) {
|
|
1524
|
+
var limit = threshold === undefined ? FLOAT_DRAG_THRESHOLD : threshold;
|
|
1525
|
+
return Math.sqrt(dx * dx + dy * dy) > limit;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
454
1528
|
/* ------------------------------------------------------------------ *
|
|
455
1529
|
* configuration normalization
|
|
456
1530
|
* ------------------------------------------------------------------ */
|
|
@@ -488,6 +1562,17 @@ function normalizeField(field, value, relative) {
|
|
|
488
1562
|
case UI_FOLLOWS_FIELD:
|
|
489
1563
|
// Absent means the default: the interface follows the conversation.
|
|
490
1564
|
return value === undefined || value === null || value === "" ? true : toBool(value);
|
|
1565
|
+
case PANEL_ENABLED_FIELD:
|
|
1566
|
+
// Absent means the default: the floating dot is shown.
|
|
1567
|
+
return value === undefined || value === null || value === "" ? true : toBool(value);
|
|
1568
|
+
case PANEL_POS_FIELD:
|
|
1569
|
+
return normalizePanelPos(value);
|
|
1570
|
+
case PANEL_CORNER_FIELD:
|
|
1571
|
+
return normalizePanelCorner(value);
|
|
1572
|
+
case PANEL_SIZE_FIELD:
|
|
1573
|
+
return normalizePanelSize(value);
|
|
1574
|
+
case SMOOTHING_FIELD:
|
|
1575
|
+
return clampSmoothing(value);
|
|
491
1576
|
default:
|
|
492
1577
|
return undefined;
|
|
493
1578
|
}
|
|
@@ -607,6 +1692,36 @@ function clampLigatures(value) {
|
|
|
607
1692
|
return mode >= LIGATURES_OFF ? LIGATURES_OFF : LIGATURES_ON;
|
|
608
1693
|
}
|
|
609
1694
|
|
|
1695
|
+
/**
|
|
1696
|
+
* Clamp one text-rendering preference to the three known values.
|
|
1697
|
+
* @param {unknown} value - candidate mode.
|
|
1698
|
+
* @returns {string} "auto", "sharp" or "smooth".
|
|
1699
|
+
*/
|
|
1700
|
+
function clampSmoothing(value) {
|
|
1701
|
+
return typeof value === "string" && SMOOTHING_VALUES.indexOf(value) >= 0 ? value : SMOOTHING_AUTO;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* The stroke-rendering rule one preference asks for, or "".
|
|
1706
|
+
*
|
|
1707
|
+
* `sharp` keeps the system's crisp subpixel rendering explicit and `smooth`
|
|
1708
|
+
* asks for the soft grayscale kind: the same face reads firmer under one and
|
|
1709
|
+
* softer under the other. `auto` — and anything unusable — installs no rule at
|
|
1710
|
+
* all, so installing this plugin still changes nothing by default.
|
|
1711
|
+
* @param {unknown} value - the stored preference.
|
|
1712
|
+
* @returns {string} one rule, or "".
|
|
1713
|
+
*/
|
|
1714
|
+
function smoothingRule(value) {
|
|
1715
|
+
var mode = clampSmoothing(value);
|
|
1716
|
+
if (mode === SMOOTHING_SHARP) {
|
|
1717
|
+
return "body{-webkit-font-smoothing:subpixel-antialiased !important;-moz-osx-font-smoothing:auto !important}";
|
|
1718
|
+
}
|
|
1719
|
+
if (mode === SMOOTHING_SMOOTH) {
|
|
1720
|
+
return "body{-webkit-font-smoothing:antialiased !important;-moz-osx-font-smoothing:grayscale !important}";
|
|
1721
|
+
}
|
|
1722
|
+
return "";
|
|
1723
|
+
}
|
|
1724
|
+
|
|
610
1725
|
/**
|
|
611
1726
|
* Validate and normalize one sparse axis-value map (dark overrides, presets).
|
|
612
1727
|
* Only known axis fields survive, each clamped.
|
|
@@ -708,6 +1823,67 @@ function migratePresetWeights(presets) {
|
|
|
708
1823
|
var PENDING_SET = "set";
|
|
709
1824
|
var PENDING_UNSET = "unset";
|
|
710
1825
|
|
|
1826
|
+
/**
|
|
1827
|
+
* The preset snapshot op a single axis write implies, as one op — or null.
|
|
1828
|
+
*
|
|
1829
|
+
* The auto-save contract (an edit with a preset selected IS that preset from
|
|
1830
|
+
* now on) belongs to every writer, not just the card: the floating panel
|
|
1831
|
+
* confirms through this instead of reimplementing it. A write
|
|
1832
|
+
* that would only move `savedAt` builds nothing, for the same reason the
|
|
1833
|
+
* card's own path skips it — a whole-document rewrite for a timestamp is a
|
|
1834
|
+
* write nobody asked for.
|
|
1835
|
+
* @param {object[]} presetList - the stored preset entries.
|
|
1836
|
+
* @param {string} activeName - the selected preset's name.
|
|
1837
|
+
* @param {Record<string, unknown>} changes - field to value (`undefined`
|
|
1838
|
+
* unsets); only value-axis fields survive.
|
|
1839
|
+
* @returns {{op: string, path: string[], value: string}|null} the op, if due.
|
|
1840
|
+
*/
|
|
1841
|
+
function mirrorPresetSnapshot(presetList, activeName, changes) {
|
|
1842
|
+
if (!Array.isArray(presetList) || typeof activeName !== "string" || activeName === "") return null;
|
|
1843
|
+
var entry = null;
|
|
1844
|
+
for (var index = 0; index < presetList.length; index += 1) {
|
|
1845
|
+
if (presetList[index] !== null && typeof presetList[index] === "object" && presetList[index].name === activeName) {
|
|
1846
|
+
entry = presetList[index];
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (entry === null) return null;
|
|
1851
|
+
var base = entry.values !== null && typeof entry.values === "object" && !Array.isArray(entry.values)
|
|
1852
|
+
? entry.values
|
|
1853
|
+
: {};
|
|
1854
|
+
var values = {};
|
|
1855
|
+
for (var key in base) {
|
|
1856
|
+
if (Object.prototype.hasOwnProperty.call(base, key)) values[key] = base[key];
|
|
1857
|
+
}
|
|
1858
|
+
var changed = false;
|
|
1859
|
+
for (var field in changes) {
|
|
1860
|
+
if (!Object.prototype.hasOwnProperty.call(changes, field)) continue;
|
|
1861
|
+
if (VALUE_FIELDS.indexOf(field) < 0) continue;
|
|
1862
|
+
var next = changes[field];
|
|
1863
|
+
if (next === undefined) {
|
|
1864
|
+
if (Object.prototype.hasOwnProperty.call(values, field)) {
|
|
1865
|
+
delete values[field];
|
|
1866
|
+
changed = true;
|
|
1867
|
+
}
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (values[field] !== next) {
|
|
1871
|
+
values[field] = next;
|
|
1872
|
+
changed = true;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
if (!changed) return null;
|
|
1876
|
+
var list = [];
|
|
1877
|
+
for (var at = 0; at < presetList.length; at += 1) {
|
|
1878
|
+
if (presetList[at] !== null && typeof presetList[at] === "object" && presetList[at].name === activeName) {
|
|
1879
|
+
list.push({ name: activeName, values: values, savedAt: Date.now() });
|
|
1880
|
+
} else {
|
|
1881
|
+
list.push(presetList[at]);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return { op: PENDING_SET, path: [PRESETS_FIELD], value: JSON.stringify(list) };
|
|
1885
|
+
}
|
|
1886
|
+
|
|
711
1887
|
/**
|
|
712
1888
|
* Merge the values the card painted locally over the document's own values.
|
|
713
1889
|
*
|
|
@@ -2620,12 +3796,21 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
|
2620
3796
|
function buildFontCss(config, baseTokens, hints) {
|
|
2621
3797
|
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
2622
3798
|
var sets = resolveAxes(config);
|
|
2623
|
-
|
|
3799
|
+
// The rendering preference is NOT an axis: it must paint even when every
|
|
3800
|
+
// axis is dormant — "the strokes are drawn the other way" is exactly its own
|
|
3801
|
+
// use case with everything else untouched.
|
|
3802
|
+
var smoothing = smoothingRule(config !== null && typeof config === "object" ? config[SMOOTHING_FIELD] : "");
|
|
3803
|
+
if (isDormant(sets.light) && isDormant(sets.dark)) return smoothing;
|
|
2624
3804
|
var light = buildAxisCss(sets.light, tokens, false, hints);
|
|
2625
|
-
if (sameValueSet(sets.light, sets.dark))
|
|
3805
|
+
if (sameValueSet(sets.light, sets.dark)) {
|
|
3806
|
+
return smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing;
|
|
3807
|
+
}
|
|
2626
3808
|
var dark = buildAxisCss(sets.dark, tokens, true, hints);
|
|
2627
|
-
|
|
2628
|
-
|
|
3809
|
+
var parts = [];
|
|
3810
|
+
if (light !== "") parts.push(light);
|
|
3811
|
+
if (dark !== "") parts.push(dark);
|
|
3812
|
+
if (smoothing !== "") parts.push(smoothing);
|
|
3813
|
+
return parts.join("\n");
|
|
2629
3814
|
}
|
|
2630
3815
|
|
|
2631
3816
|
/**
|
|
@@ -2994,6 +4179,72 @@ var shared = {
|
|
|
2994
4179
|
PRESETS_FIELD: PRESETS_FIELD,
|
|
2995
4180
|
ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
|
|
2996
4181
|
UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
|
|
4182
|
+
PANEL_ENABLED_FIELD: PANEL_ENABLED_FIELD,
|
|
4183
|
+
PANEL_POS_FIELD: PANEL_POS_FIELD,
|
|
4184
|
+
PANEL_CORNER_FIELD: PANEL_CORNER_FIELD,
|
|
4185
|
+
PANEL_SIZE_FIELD: PANEL_SIZE_FIELD,
|
|
4186
|
+
PANEL_SIZE_DEFAULT_W: PANEL_SIZE_DEFAULT_W,
|
|
4187
|
+
PANEL_SIZE_DEFAULT_H: PANEL_SIZE_DEFAULT_H,
|
|
4188
|
+
PANEL_SIZE_MIN_W: PANEL_SIZE_MIN_W,
|
|
4189
|
+
PANEL_SIZE_MAX_W: PANEL_SIZE_MAX_W,
|
|
4190
|
+
PANEL_SIZE_MIN_H: PANEL_SIZE_MIN_H,
|
|
4191
|
+
PANEL_SIZE_MAX_H: PANEL_SIZE_MAX_H,
|
|
4192
|
+
FLOAT_DOT: FLOAT_DOT,
|
|
4193
|
+
FLOAT_MARGIN: FLOAT_MARGIN,
|
|
4194
|
+
FLOAT_DRAG_THRESHOLD: FLOAT_DRAG_THRESHOLD,
|
|
4195
|
+
FLOAT_VIEWPORT_W: FLOAT_VIEWPORT_W,
|
|
4196
|
+
FLOAT_VIEWPORT_H: FLOAT_VIEWPORT_H,
|
|
4197
|
+
FLOAT_BOUNDS_MIN: FLOAT_BOUNDS_MIN,
|
|
4198
|
+
FLOAT_CLOSE_GAP: FLOAT_CLOSE_GAP,
|
|
4199
|
+
FLOAT_CLOSE: FLOAT_CLOSE,
|
|
4200
|
+
FLOAT_CLOSE_INSET: FLOAT_CLOSE_INSET,
|
|
4201
|
+
FLOAT_SETTINGS_GAP: FLOAT_SETTINGS_GAP,
|
|
4202
|
+
FLOAT_CARD_PAD: FLOAT_CARD_PAD,
|
|
4203
|
+
FLOAT_CARD_RADIUS: FLOAT_CARD_RADIUS,
|
|
4204
|
+
floatBoxOf: floatBoxOf,
|
|
4205
|
+
floatBoundsPick: floatBoundsPick,
|
|
4206
|
+
floatBoundsFrom: floatBoundsFrom,
|
|
4207
|
+
sameWatchBounds: sameWatchBounds,
|
|
4208
|
+
floatTargetsConnected: floatTargetsConnected,
|
|
4209
|
+
floatQuadrantIn: floatQuadrantIn,
|
|
4210
|
+
floatQuadrant: floatQuadrant,
|
|
4211
|
+
floatExpandDirection: floatExpandDirection,
|
|
4212
|
+
floatTransformOrigin: floatTransformOrigin,
|
|
4213
|
+
floatCardAnchor: floatCardAnchor,
|
|
4214
|
+
floatClipStart: floatClipStart,
|
|
4215
|
+
floatClipRest: floatClipRest,
|
|
4216
|
+
isRestClip: isRestClip,
|
|
4217
|
+
floatSnapTo: floatSnapTo,
|
|
4218
|
+
floatSnapCorner: floatSnapCorner,
|
|
4219
|
+
floatNearestCorner: floatNearestCorner,
|
|
4220
|
+
floatCornerPoint: floatCornerPoint,
|
|
4221
|
+
floatDragExceeded: floatDragExceeded,
|
|
4222
|
+
normalizePanelSize: normalizePanelSize,
|
|
4223
|
+
parsePanelSize: parsePanelSize,
|
|
4224
|
+
PANEL_POS_MAX: PANEL_POS_MAX,
|
|
4225
|
+
normalizePanelPos: normalizePanelPos,
|
|
4226
|
+
parsePanelPos: parsePanelPos,
|
|
4227
|
+
PANEL_CORNERS: PANEL_CORNERS,
|
|
4228
|
+
normalizePanelCorner: normalizePanelCorner,
|
|
4229
|
+
parsePanelCorner: parsePanelCorner,
|
|
4230
|
+
SETTINGS_NAV_ITEM_ATTR: SETTINGS_NAV_ITEM_ATTR,
|
|
4231
|
+
SETTINGS_NAV_ITEM_VALUE: SETTINGS_NAV_ITEM_VALUE,
|
|
4232
|
+
SETTINGS_NAV_CURRENT_ATTR: SETTINGS_NAV_CURRENT_ATTR,
|
|
4233
|
+
SETTINGS_NAV_TEXTS: SETTINGS_NAV_TEXTS,
|
|
4234
|
+
SETTINGS_LAUNCHER_TEXTS: SETTINGS_LAUNCHER_TEXTS,
|
|
4235
|
+
isSettingsNavItem: isSettingsNavItem,
|
|
4236
|
+
isSettingsLauncher: isSettingsLauncher,
|
|
4237
|
+
findSettingsEntry: findSettingsEntry,
|
|
4238
|
+
PLUGIN_ITEM_ATTR: PLUGIN_ITEM_ATTR,
|
|
4239
|
+
PLUGIN_ITEM_VALUE: PLUGIN_ITEM_VALUE,
|
|
4240
|
+
PLUGIN_BUNDLE_NAME: PLUGIN_BUNDLE_NAME,
|
|
4241
|
+
PLUGIN_PANEL_ID: PLUGIN_PANEL_ID,
|
|
4242
|
+
pluginEntryCard: pluginEntryCard,
|
|
4243
|
+
pluginClickTarget: pluginClickTarget,
|
|
4244
|
+
// The client's own click step needs this: the bundle keeps this module's scope
|
|
4245
|
+
// separate from the client's, so a bare name here is a ReferenceError there.
|
|
4246
|
+
dispatchSynthetic: dispatchSynthetic,
|
|
4247
|
+
mirrorPresetSnapshot: mirrorPresetSnapshot,
|
|
2997
4248
|
VALUE_FIELDS: VALUE_FIELDS,
|
|
2998
4249
|
RETIRED_FIELDS: RETIRED_FIELDS,
|
|
2999
4250
|
DURABLE_FIELDS: DURABLE_FIELDS,
|
|
@@ -3007,6 +4258,12 @@ var shared = {
|
|
|
3007
4258
|
CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
|
|
3008
4259
|
CODE_LINE_HEIGHT_MAX: CODE_LINE_HEIGHT_MAX,
|
|
3009
4260
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
4261
|
+
SMOOTHING_FIELD: SMOOTHING_FIELD,
|
|
4262
|
+
SMOOTHING_AUTO: SMOOTHING_AUTO,
|
|
4263
|
+
SMOOTHING_SHARP: SMOOTHING_SHARP,
|
|
4264
|
+
SMOOTHING_SMOOTH: SMOOTHING_SMOOTH,
|
|
4265
|
+
SMOOTHING_VALUES: SMOOTHING_VALUES,
|
|
4266
|
+
clampSmoothing: clampSmoothing,
|
|
3010
4267
|
LIGATURES_ON: LIGATURES_ON,
|
|
3011
4268
|
LIGATURES_OFF: LIGATURES_OFF,
|
|
3012
4269
|
STYLE_TAG: STYLE_TAG,
|