@signal9/era-ui 3.14.0 → 3.14.1
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.
|
@@ -57,6 +57,11 @@ class BubbleMenuView {
|
|
|
57
57
|
#host;
|
|
58
58
|
#toolbar;
|
|
59
59
|
#editor;
|
|
60
|
+
/**
|
|
61
|
+
* The DOCUMENT range the toolbar is currently parked over, or null while it is
|
|
62
|
+
* hidden. This is what the toolbar is anchored to — see update().
|
|
63
|
+
*/
|
|
64
|
+
#anchor = null;
|
|
60
65
|
#view = $state({
|
|
61
66
|
items,
|
|
62
67
|
active: {},
|
|
@@ -80,16 +85,42 @@ class BubbleMenuView {
|
|
|
80
85
|
!state.doc.textBetween(selection.from, selection.to).length ||
|
|
81
86
|
this.#editor.isActive('codeBlock')) {
|
|
82
87
|
this.#host.style.display = 'none';
|
|
88
|
+
this.#anchor = null;
|
|
83
89
|
return;
|
|
84
90
|
}
|
|
85
91
|
this.#view.active = Object.fromEntries(buttons.map((b) => [b.mark, this.#editor.isActive(b.mark)]));
|
|
86
|
-
|
|
87
|
-
// applies the `active` change above FIRST — measuring a Svelte component
|
|
88
|
-
// before its pending render lands reads the previous frame's box, which is
|
|
89
|
-
// the same trap the slash menu documents.
|
|
92
|
+
const wasHidden = this.#host.style.display === 'none';
|
|
90
93
|
this.#host.style.display = 'block';
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
/*
|
|
95
|
+
* The toolbar is anchored to the SELECTION RANGE, not to its pixel box.
|
|
96
|
+
*
|
|
97
|
+
* Those are not the same thing, and the difference is the whole bug:
|
|
98
|
+
* applying a mark re-renders the selected text — code sets it monospace, a
|
|
99
|
+
* link adds an underline — so its rect moves while the selection itself
|
|
100
|
+
* does not change by a single character. Re-centring on the new rect slides
|
|
101
|
+
* the toolbar out from under the pointer that just clicked it, and the next
|
|
102
|
+
* click lands on nothing. The user has to chase the menu to keep using it.
|
|
103
|
+
*
|
|
104
|
+
* So: reposition only when the range actually changes (or when the toolbar
|
|
105
|
+
* is appearing fresh). While `from`/`to` hold steady the toolbar is pinned,
|
|
106
|
+
* and a whole run of formatting happens under a motionless pointer.
|
|
107
|
+
*
|
|
108
|
+
* The trade is deliberate: if a format reflows the text far enough, the
|
|
109
|
+
* toolbar can end up slightly off its selection until the selection next
|
|
110
|
+
* changes. Being a few pixels stale beats moving while it is being used —
|
|
111
|
+
* a toolbar is a target first and an indicator second.
|
|
112
|
+
*/
|
|
113
|
+
const { from, to } = selection;
|
|
114
|
+
const movedRange = !this.#anchor || this.#anchor.from !== from || this.#anchor.to !== to;
|
|
115
|
+
if (wasHidden || movedRange) {
|
|
116
|
+
// flushSync applies the `active` change above BEFORE measuring — reading
|
|
117
|
+
// a Svelte component's box before its pending render lands gives the
|
|
118
|
+
// previous frame's, the same trap the slash menu documents. Only needed
|
|
119
|
+
// on the frames that actually measure.
|
|
120
|
+
flushSync();
|
|
121
|
+
placeOverSelection(this.#host, selectionRect(view));
|
|
122
|
+
this.#anchor = { from, to };
|
|
123
|
+
}
|
|
93
124
|
}
|
|
94
125
|
destroy() {
|
|
95
126
|
unmount(this.#toolbar);
|