@dumiflex/wp-kit 0.3.0 → 0.5.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.md +92 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +146 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +293 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +98 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/loading-mark-motion.ts +249 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/useGrowableField.ts
CHANGED
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The behaviour every capped, user-resizable text field in this project needs.
|
|
3
|
-
*
|
|
4
|
-
* Three components had grown their own copy of it — `RichTextInput`'s two
|
|
5
|
-
* hosts, the fixed-values `ValueRow`, the combine template — and the copies had
|
|
6
|
-
* already diverged: only one clears the resize grip, only one follows the grip
|
|
7
|
-
* off-screen, and the auto-grow in `ValueRow` silently fought the drag handle.
|
|
8
|
-
* One implementation, three fixes, no third copy.
|
|
9
|
-
*
|
|
10
|
-
* What it provides:
|
|
11
|
-
*
|
|
12
|
-
* 1. **Auto-grow that yields to the user.** The field sizes itself to its
|
|
13
|
-
* content until the user drags the handle; from then on their height is
|
|
14
|
-
* authoritative. Previously `height = scrollHeight` ran on every input and
|
|
15
|
-
* external value change, so a manually collapsed box snapped straight back
|
|
16
|
-
* open — which reads as "the drag sticks, then starts working".
|
|
17
|
-
*
|
|
18
|
-
* 2. **A "more below" flag.** A capped box looks identical whether it holds
|
|
19
|
-
* its whole value or a third of it.
|
|
20
|
-
*
|
|
21
|
-
* 3. **Grip-follow.** Dragging a field taller than the space beneath it moves
|
|
22
|
-
* the box, not the pointer, so the handle being held slides off-screen and
|
|
23
|
-
* the drag goes blind. Scrolling by exactly the overshoot pins the bottom
|
|
24
|
-
* edge to the viewport, keeping the grip under the cursor.
|
|
25
|
-
*
|
|
26
|
-
* DOM-only and framework-light: takes an element getter, returns plain
|
|
27
|
-
* functions plus one ref. Callers own their own `onMounted` / `watch`.
|
|
28
|
-
*/
|
|
29
|
-
import { onBeforeUnmount, ref, type Ref } from "vue";
|
|
30
|
-
|
|
31
|
-
export interface GrowableField {
|
|
32
|
-
/** True while content extends past the visible bottom edge. */
|
|
33
|
-
hasMoreBelow: Ref<boolean>;
|
|
34
|
-
/** Re-measure the overflow flag. Cheap; call on scroll and input. */
|
|
35
|
-
updateOverflowHint: () => void;
|
|
36
|
-
/** Same, deferred a frame — use at mount, before layout has settled. */
|
|
37
|
-
scheduleOverflowHint: () => void;
|
|
38
|
-
/** Resize to fit content, UNLESS the user has taken manual control. */
|
|
39
|
-
autosize: () => void;
|
|
40
|
-
/** Begin observing: wires the resize observer for the hint + grip-follow. */
|
|
41
|
-
attach: () => void;
|
|
42
|
-
/** True once the user has dragged the handle. */
|
|
43
|
-
userResized: Ref<boolean>;
|
|
44
|
-
/** Bind to a grip element's `pointerdown`. Replaces `resize: vertical`,
|
|
45
|
-
* which cannot be clamped from outside — see `startResize`. */
|
|
46
|
-
startResize: (ev: PointerEvent) => void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Nearest ancestor that actually scrolls, or null when only the page does.
|
|
50
|
-
* Walks computed `overflow-y` rather than trusting a class name — these
|
|
51
|
-
* fields live in modals, pages and canvas widgets, each of which puts its
|
|
52
|
-
* scroller somewhere different. */
|
|
53
|
-
function scrollableAncestor(el: HTMLElement): HTMLElement | null {
|
|
54
|
-
let node = el.parentElement;
|
|
55
|
-
while (node && node !== document.body) {
|
|
56
|
-
const oy = getComputedStyle(node).overflowY;
|
|
57
|
-
if (oy === "auto" || oy === "scroll") return node;
|
|
58
|
-
node = node.parentElement;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Backstop floor for a field whose CSS declares no `min-height`. Enough to
|
|
64
|
-
* keep one line and its padding visible, so a drag can always be undone. */
|
|
65
|
-
const ABSOLUTE_MIN = 28;
|
|
66
|
-
|
|
67
|
-
export function useGrowableField(
|
|
68
|
-
getEl: () => HTMLElement | null,
|
|
69
|
-
opts: { minHeight?: number } = {},
|
|
70
|
-
): GrowableField {
|
|
71
|
-
const hasMoreBelow = ref(false);
|
|
72
|
-
const userResized = ref(false);
|
|
73
|
-
|
|
74
|
-
function updateOverflowHint(): void {
|
|
75
|
-
const el = getEl();
|
|
76
|
-
if (!el) return;
|
|
77
|
-
// 2px slack absorbs sub-pixel rounding at the exact bottom.
|
|
78
|
-
hasMoreBelow.value = el.scrollHeight - el.scrollTop - el.clientHeight > 2;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function scheduleOverflowHint(): void {
|
|
82
|
-
if (typeof requestAnimationFrame === "function") requestAnimationFrame(updateOverflowHint);
|
|
83
|
-
else updateOverflowHint();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function autosize(): void {
|
|
87
|
-
const el = getEl();
|
|
88
|
-
// The user's drag wins from the moment it happens. Re-running the fit
|
|
89
|
-
// after that is what made a collapsed box spring open again.
|
|
90
|
-
if (!el || userResized.value) return;
|
|
91
|
-
autoDriven = true;
|
|
92
|
-
el.style.height = "auto";
|
|
93
|
-
const next = Math.max(opts.minHeight ?? 0, el.scrollHeight);
|
|
94
|
-
el.style.height = `${next}px`;
|
|
95
|
-
lastHeight = next;
|
|
96
|
-
// Release on the next frame: the observer fires asynchronously, and
|
|
97
|
-
// clearing synchronously would let our own write look like a user drag.
|
|
98
|
-
if (typeof requestAnimationFrame === "function") {
|
|
99
|
-
requestAnimationFrame(() => { autoDriven = false; });
|
|
100
|
-
} else {
|
|
101
|
-
autoDriven = false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Keep the grip under the cursor while the box grows past the fold.
|
|
106
|
-
* Only ever scrolls DOWN, and only while the bottom is genuinely
|
|
107
|
-
* off-screen — shrinking must not yank the view around.
|
|
108
|
-
*
|
|
109
|
-
* Deliberately not `scrollIntoView`, which this project bans for scrolling
|
|
110
|
-
* every ancestor rather than the one the user is working in. */
|
|
111
|
-
function followGrip(): void {
|
|
112
|
-
const el = getEl();
|
|
113
|
-
if (!el) return;
|
|
114
|
-
const margin = 8;
|
|
115
|
-
const rect = el.getBoundingClientRect();
|
|
116
|
-
const container = scrollableAncestor(el);
|
|
117
|
-
if (container) {
|
|
118
|
-
const cRect = container.getBoundingClientRect();
|
|
119
|
-
const overshoot = rect.bottom - (cRect.bottom - margin);
|
|
120
|
-
if (overshoot > 0) container.scrollTop += overshoot;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const overshoot = rect.bottom - (window.innerHeight - margin);
|
|
124
|
-
if (overshoot > 0) window.scrollBy(0, overshoot);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let obs: ResizeObserver | null = null;
|
|
128
|
-
let lastHeight = 0;
|
|
129
|
-
/** Set while `autosize` is writing, so its own change is not mistaken for a
|
|
130
|
-
* drag. Without it the first auto-fit would lock the field immediately. */
|
|
131
|
-
let autoDriven = false;
|
|
132
|
-
/** True between pointerdown on the field and the matching pointerup —
|
|
133
|
-
* i.e. while the user may be holding the resize handle. */
|
|
134
|
-
let dragging = false;
|
|
135
|
-
let pending = false;
|
|
136
|
-
/** Set once the height cap has been dropped for a manual drag. */
|
|
137
|
-
let capLifted = false;
|
|
138
|
-
/** Pointer Y at the previous move — the drag is incremental, so only the
|
|
139
|
-
* step between moves matters, never a captured origin. */
|
|
140
|
-
let lastPointerY = 0;
|
|
141
|
-
/** Floor read from CSS at drag start; CSS stays the source of truth. */
|
|
142
|
-
let minPx = 0;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Everything the observer wants to do, moved OUT of the observer.
|
|
146
|
-
*
|
|
147
|
-
* A ResizeObserver callback that reads layout and writes reactive state runs
|
|
148
|
-
* before paint, so doing it synchronously on every frame of a resize drag
|
|
149
|
-
* dirties layout inside the very callback the browser is using to report
|
|
150
|
-
* layout. Chrome throttles that (the "ResizeObserver loop" case) and stops
|
|
151
|
-
* delivering for a frame, which the user feels as the drag seizing up until
|
|
152
|
-
* they release and re-grab. Deferring to rAF leaves the callback trivial.
|
|
153
|
-
*/
|
|
154
|
-
function flush(): void {
|
|
155
|
-
pending = false;
|
|
156
|
-
const el = getEl();
|
|
157
|
-
if (!el) return;
|
|
158
|
-
const h = el.getBoundingClientRect().height;
|
|
159
|
-
updateOverflowHint();
|
|
160
|
-
// A height change we did not author is the user on the drag handle.
|
|
161
|
-
if (!autoDriven && Math.abs(h - lastHeight) > 1) userResized.value = true;
|
|
162
|
-
// Follows LIVE, including mid-drag. Scrolling by the overshoot moves the
|
|
163
|
-
// element up by exactly that much, landing its bottom edge at the viewport
|
|
164
|
-
// edge — which is where the pointer already is, so the grip stays under the
|
|
165
|
-
// cursor. Deferring this to pointerup (an earlier attempt at the stall,
|
|
166
|
-
// which turned out to be the height cap) just made the field vanish off
|
|
167
|
-
// the bottom for the whole drag.
|
|
168
|
-
//
|
|
169
|
-
// Gated on `dragging`, which is what the name always implied. It exists to
|
|
170
|
-
// keep the GRIP under the CURSOR, and there is no grip being held unless a
|
|
171
|
-
// drag is in progress. Ungated it also ran on content-driven growth — so
|
|
172
|
-
// typing a long enough template scrolled the page out from under you — and
|
|
173
|
-
// it made this flush mutate scroll position, which moves the element the
|
|
174
|
-
// observer is measuring. A resize handler that can change its own input is
|
|
175
|
-
// the shape a runaway needs.
|
|
176
|
-
if (dragging && h > lastHeight) followGrip();
|
|
177
|
-
lastHeight = h;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function schedule(): void {
|
|
181
|
-
if (pending) return;
|
|
182
|
-
pending = true;
|
|
183
|
-
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
184
|
-
else flush();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Our own resize drag, replacing the native `resize: vertical`.
|
|
189
|
-
*
|
|
190
|
-
* The native resizer computes `startHeight + (pointerY - startY)` from an
|
|
191
|
-
* origin it captured at pointerdown, and it does NOT clamp to the element's
|
|
192
|
-
* own min/max. Drag past a limit and that sum keeps running while the
|
|
193
|
-
* rendered box stays pinned, so dragging back moves nothing until the
|
|
194
|
-
* invisible total walks all the way home. Measured: collapsing past a 34px
|
|
195
|
-
* floor then expanding cost 13 of 23 frames of completely dead travel. That
|
|
196
|
-
* is the "gets stuck, then starts working" report, and it bites exactly when
|
|
197
|
-
* the box reaches one line — which is also when the overflow fade returns,
|
|
198
|
-
* which is why the fade kept looking guilty.
|
|
199
|
-
*
|
|
200
|
-
* Nothing outside can fix it: rewriting `style.height` mid-drag is ignored,
|
|
201
|
-
* because the browser recomputes from its own captured origin every move.
|
|
202
|
-
* Clamping the RESULT does not help either — the accumulated input is the
|
|
203
|
-
* problem, so a clamped absolute drag measured the same 13 / 23.
|
|
204
|
-
*
|
|
205
|
-
* Applying each move's DELTA to the current height instead keeps no history
|
|
206
|
-
* to accumulate: the first pixel back off a limit moves the box. Same
|
|
207
|
-
* harness, same over-drag: 0 / 23.
|
|
208
|
-
*/
|
|
209
|
-
function startResize(ev: PointerEvent): void {
|
|
210
|
-
const el = getEl();
|
|
211
|
-
if (!el) return;
|
|
212
|
-
ev.preventDefault();
|
|
213
|
-
dragging = true;
|
|
214
|
-
userResized.value = true;
|
|
215
|
-
lastPointerY = ev.clientY;
|
|
216
|
-
|
|
217
|
-
const cs = getComputedStyle(el);
|
|
218
|
-
// CSS stays the source of truth for the floor; the ceiling is deliberately
|
|
219
|
-
// released, because a deliberate drag is the user overriding a cap that
|
|
220
|
-
// exists only to bound AUTO-grow.
|
|
221
|
-
// `min-height: auto` (the default) parses to NaN, so a field that never
|
|
222
|
-
// declared one would get a floor of 0 and could be dragged away to a
|
|
223
|
-
// sliver. CSS stays authoritative when it says anything; ABSOLUTE_MIN is
|
|
224
|
-
// only the backstop for when it does not.
|
|
225
|
-
const declared = Number.parseFloat(cs.minHeight);
|
|
226
|
-
minPx = Number.isFinite(declared) && declared > 0 ? declared : ABSOLUTE_MIN;
|
|
227
|
-
if (!capLifted) {
|
|
228
|
-
el.style.height = `${el.getBoundingClientRect().height}px`;
|
|
229
|
-
el.style.maxHeight = "none";
|
|
230
|
-
capLifted = true;
|
|
231
|
-
}
|
|
232
|
-
/* No `setPointerCapture`.
|
|
233
|
-
*
|
|
234
|
-
* It reads like the right call and it is what stranded the drag. Capture
|
|
235
|
-
* retargets every subsequent pointer event to the grip — including
|
|
236
|
-
* `pointerup`. The editor re-renders while it resizes, and a captured
|
|
237
|
-
* element that gets replaced is detached: the `pointerup` is delivered to a
|
|
238
|
-
* node no longer in the tree, so it never bubbles to `window`, `dragging`
|
|
239
|
-
* stays true, and the box follows the cursor with no button held and no way
|
|
240
|
-
* to stop it. That is the reported symptom exactly.
|
|
241
|
-
*
|
|
242
|
-
* Capture bought nothing here anyway. The listeners below are on `window`,
|
|
243
|
-
* which sees the events regardless of what is under the cursor.
|
|
244
|
-
*
|
|
245
|
-
* CAPTURE PHASE on all three, so a `stopPropagation()` between the target
|
|
246
|
-
* and the window — ComfyUI's node layer does this to stop the canvas
|
|
247
|
-
* panning under a widget — cannot hide the end of the drag from us.
|
|
248
|
-
*/
|
|
249
|
-
window.addEventListener("pointermove", onResizeMove, true);
|
|
250
|
-
window.addEventListener("pointerup", onPointerUp, true);
|
|
251
|
-
window.addEventListener("pointercancel", onPointerUp, true);
|
|
252
|
-
// A fresh press means the previous drag is over however it ended.
|
|
253
|
-
window.addEventListener("pointerdown", onPointerUp, true);
|
|
254
|
-
window.addEventListener("blur", onPointerUp);
|
|
255
|
-
window.addEventListener("keydown", onDragEscape, true);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Escape ends a drag. Cheap, and it is the key a stuck user presses. */
|
|
259
|
-
function onDragEscape(ev: KeyboardEvent): void {
|
|
260
|
-
if (ev.key === "Escape") onPointerUp();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function onResizeMove(ev: PointerEvent): void {
|
|
264
|
-
const el = getEl();
|
|
265
|
-
if (!el || !dragging) return;
|
|
266
|
-
// Self-heal. Every pointermove reports which buttons are down, so a move
|
|
267
|
-
// with none held means the pointerup was lost — whatever swallowed it —
|
|
268
|
-
// and the drag should already be over. Without this the field follows the
|
|
269
|
-
// cursor forever and no key or click can stop it, because nothing else in
|
|
270
|
-
// here ever clears `dragging`. Belt and braces on top of the listener
|
|
271
|
-
// changes above: those address the causes we found, this ends the state
|
|
272
|
-
// regardless of a cause we did not.
|
|
273
|
-
if (ev.buttons === 0) {
|
|
274
|
-
onPointerUp();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const dy = ev.clientY - lastPointerY;
|
|
278
|
-
lastPointerY = ev.clientY;
|
|
279
|
-
const cur = el.getBoundingClientRect().height;
|
|
280
|
-
el.style.height = `${Math.max(minPx, cur + dy)}px`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function onPointerUp(): void {
|
|
284
|
-
if (!dragging) return;
|
|
285
|
-
dragging = false;
|
|
286
|
-
// Re-arm for the next drag. `capLifted` guards a one-time "pin the current
|
|
287
|
-
// height, release the max-height cap" step, and leaving it set meant the
|
|
288
|
-
// release survived for the lifetime of the field while the explicit height
|
|
289
|
-
// that bounded it did not — `applyAtoms` replaces the host element on a
|
|
290
|
-
// rebuild, which drops the inline height but not this flag. The next drag
|
|
291
|
-
// then found an uncapped, unpinned box. Each drag now re-establishes both
|
|
292
|
-
// against the element that actually exists.
|
|
293
|
-
capLifted = false;
|
|
294
|
-
window.removeEventListener("pointermove", onResizeMove);
|
|
295
|
-
window.removeEventListener("pointerup", onPointerUp);
|
|
296
|
-
window.removeEventListener("pointercancel", onPointerUp);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Idempotent: callers re-attach when the element they own is REPLACED
|
|
300
|
-
* rather than mutated (RichTextInput swaps its contenteditable host to
|
|
301
|
-
* repair a browser-mangled DOM). Without the disconnect the old observer
|
|
302
|
-
* would keep watching a detached node and leak, and the hint would go
|
|
303
|
-
* permanently stale on the live one. */
|
|
304
|
-
function attach(): void {
|
|
305
|
-
obs?.disconnect();
|
|
306
|
-
obs = null;
|
|
307
|
-
const el = getEl();
|
|
308
|
-
if (!el) return;
|
|
309
|
-
lastHeight = el.getBoundingClientRect().height;
|
|
310
|
-
|
|
311
|
-
/* Re-measure once the webfonts land.
|
|
312
|
-
*
|
|
313
|
-
* These fields are typed in `--wp-font-mono`, a bundled webfont declared
|
|
314
|
-
* `font-display: swap` — so the first paint uses the system fallback
|
|
315
|
-
* (Consolas / Monaco) and the real face swaps in later. The two stacks
|
|
316
|
-
* have different metrics, so the `scrollHeight` measured at mount is taken
|
|
317
|
-
* against type that is about to be replaced.
|
|
318
|
-
*
|
|
319
|
-
* That is the "sometimes the bottom fade is wrong" report: on a cold load
|
|
320
|
-
* the hint is computed against the fallback and never recomputed, and on a
|
|
321
|
-
* warm cache the font is already there so it looks fine. Any later scroll,
|
|
322
|
-
* input or resize also fixes it by accident, which is what made it look
|
|
323
|
-
* intermittent rather than deterministic.
|
|
324
|
-
*
|
|
325
|
-
* `document.fonts.ready` resolves once, after all pending faces settle.
|
|
326
|
-
* Cheap, and a no-op where the font was already cached.
|
|
327
|
-
*/
|
|
328
|
-
if (typeof document !== "undefined" && document.fonts?.ready) {
|
|
329
|
-
void document.fonts.ready.then(() => {
|
|
330
|
-
// The field may have unmounted during the load.
|
|
331
|
-
if (!getEl()) return;
|
|
332
|
-
updateOverflowHint();
|
|
333
|
-
autosize();
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (typeof ResizeObserver === "undefined") return;
|
|
338
|
-
obs = new ResizeObserver(schedule);
|
|
339
|
-
obs.observe(el);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
onBeforeUnmount(() => {
|
|
343
|
-
obs?.disconnect();
|
|
344
|
-
obs = null;
|
|
345
|
-
onPointerUp();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
hasMoreBelow,
|
|
350
|
-
updateOverflowHint,
|
|
351
|
-
scheduleOverflowHint,
|
|
352
|
-
autosize,
|
|
353
|
-
attach,
|
|
354
|
-
userResized,
|
|
355
|
-
startResize,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The behaviour every capped, user-resizable text field in this project needs.
|
|
3
|
+
*
|
|
4
|
+
* Three components had grown their own copy of it — `RichTextInput`'s two
|
|
5
|
+
* hosts, the fixed-values `ValueRow`, the combine template — and the copies had
|
|
6
|
+
* already diverged: only one clears the resize grip, only one follows the grip
|
|
7
|
+
* off-screen, and the auto-grow in `ValueRow` silently fought the drag handle.
|
|
8
|
+
* One implementation, three fixes, no third copy.
|
|
9
|
+
*
|
|
10
|
+
* What it provides:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Auto-grow that yields to the user.** The field sizes itself to its
|
|
13
|
+
* content until the user drags the handle; from then on their height is
|
|
14
|
+
* authoritative. Previously `height = scrollHeight` ran on every input and
|
|
15
|
+
* external value change, so a manually collapsed box snapped straight back
|
|
16
|
+
* open — which reads as "the drag sticks, then starts working".
|
|
17
|
+
*
|
|
18
|
+
* 2. **A "more below" flag.** A capped box looks identical whether it holds
|
|
19
|
+
* its whole value or a third of it.
|
|
20
|
+
*
|
|
21
|
+
* 3. **Grip-follow.** Dragging a field taller than the space beneath it moves
|
|
22
|
+
* the box, not the pointer, so the handle being held slides off-screen and
|
|
23
|
+
* the drag goes blind. Scrolling by exactly the overshoot pins the bottom
|
|
24
|
+
* edge to the viewport, keeping the grip under the cursor.
|
|
25
|
+
*
|
|
26
|
+
* DOM-only and framework-light: takes an element getter, returns plain
|
|
27
|
+
* functions plus one ref. Callers own their own `onMounted` / `watch`.
|
|
28
|
+
*/
|
|
29
|
+
import { onBeforeUnmount, ref, type Ref } from "vue";
|
|
30
|
+
|
|
31
|
+
export interface GrowableField {
|
|
32
|
+
/** True while content extends past the visible bottom edge. */
|
|
33
|
+
hasMoreBelow: Ref<boolean>;
|
|
34
|
+
/** Re-measure the overflow flag. Cheap; call on scroll and input. */
|
|
35
|
+
updateOverflowHint: () => void;
|
|
36
|
+
/** Same, deferred a frame — use at mount, before layout has settled. */
|
|
37
|
+
scheduleOverflowHint: () => void;
|
|
38
|
+
/** Resize to fit content, UNLESS the user has taken manual control. */
|
|
39
|
+
autosize: () => void;
|
|
40
|
+
/** Begin observing: wires the resize observer for the hint + grip-follow. */
|
|
41
|
+
attach: () => void;
|
|
42
|
+
/** True once the user has dragged the handle. */
|
|
43
|
+
userResized: Ref<boolean>;
|
|
44
|
+
/** Bind to a grip element's `pointerdown`. Replaces `resize: vertical`,
|
|
45
|
+
* which cannot be clamped from outside — see `startResize`. */
|
|
46
|
+
startResize: (ev: PointerEvent) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Nearest ancestor that actually scrolls, or null when only the page does.
|
|
50
|
+
* Walks computed `overflow-y` rather than trusting a class name — these
|
|
51
|
+
* fields live in modals, pages and canvas widgets, each of which puts its
|
|
52
|
+
* scroller somewhere different. */
|
|
53
|
+
function scrollableAncestor(el: HTMLElement): HTMLElement | null {
|
|
54
|
+
let node = el.parentElement;
|
|
55
|
+
while (node && node !== document.body) {
|
|
56
|
+
const oy = getComputedStyle(node).overflowY;
|
|
57
|
+
if (oy === "auto" || oy === "scroll") return node;
|
|
58
|
+
node = node.parentElement;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Backstop floor for a field whose CSS declares no `min-height`. Enough to
|
|
64
|
+
* keep one line and its padding visible, so a drag can always be undone. */
|
|
65
|
+
const ABSOLUTE_MIN = 28;
|
|
66
|
+
|
|
67
|
+
export function useGrowableField(
|
|
68
|
+
getEl: () => HTMLElement | null,
|
|
69
|
+
opts: { minHeight?: number } = {},
|
|
70
|
+
): GrowableField {
|
|
71
|
+
const hasMoreBelow = ref(false);
|
|
72
|
+
const userResized = ref(false);
|
|
73
|
+
|
|
74
|
+
function updateOverflowHint(): void {
|
|
75
|
+
const el = getEl();
|
|
76
|
+
if (!el) return;
|
|
77
|
+
// 2px slack absorbs sub-pixel rounding at the exact bottom.
|
|
78
|
+
hasMoreBelow.value = el.scrollHeight - el.scrollTop - el.clientHeight > 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scheduleOverflowHint(): void {
|
|
82
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(updateOverflowHint);
|
|
83
|
+
else updateOverflowHint();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function autosize(): void {
|
|
87
|
+
const el = getEl();
|
|
88
|
+
// The user's drag wins from the moment it happens. Re-running the fit
|
|
89
|
+
// after that is what made a collapsed box spring open again.
|
|
90
|
+
if (!el || userResized.value) return;
|
|
91
|
+
autoDriven = true;
|
|
92
|
+
el.style.height = "auto";
|
|
93
|
+
const next = Math.max(opts.minHeight ?? 0, el.scrollHeight);
|
|
94
|
+
el.style.height = `${next}px`;
|
|
95
|
+
lastHeight = next;
|
|
96
|
+
// Release on the next frame: the observer fires asynchronously, and
|
|
97
|
+
// clearing synchronously would let our own write look like a user drag.
|
|
98
|
+
if (typeof requestAnimationFrame === "function") {
|
|
99
|
+
requestAnimationFrame(() => { autoDriven = false; });
|
|
100
|
+
} else {
|
|
101
|
+
autoDriven = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Keep the grip under the cursor while the box grows past the fold.
|
|
106
|
+
* Only ever scrolls DOWN, and only while the bottom is genuinely
|
|
107
|
+
* off-screen — shrinking must not yank the view around.
|
|
108
|
+
*
|
|
109
|
+
* Deliberately not `scrollIntoView`, which this project bans for scrolling
|
|
110
|
+
* every ancestor rather than the one the user is working in. */
|
|
111
|
+
function followGrip(): void {
|
|
112
|
+
const el = getEl();
|
|
113
|
+
if (!el) return;
|
|
114
|
+
const margin = 8;
|
|
115
|
+
const rect = el.getBoundingClientRect();
|
|
116
|
+
const container = scrollableAncestor(el);
|
|
117
|
+
if (container) {
|
|
118
|
+
const cRect = container.getBoundingClientRect();
|
|
119
|
+
const overshoot = rect.bottom - (cRect.bottom - margin);
|
|
120
|
+
if (overshoot > 0) container.scrollTop += overshoot;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const overshoot = rect.bottom - (window.innerHeight - margin);
|
|
124
|
+
if (overshoot > 0) window.scrollBy(0, overshoot);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let obs: ResizeObserver | null = null;
|
|
128
|
+
let lastHeight = 0;
|
|
129
|
+
/** Set while `autosize` is writing, so its own change is not mistaken for a
|
|
130
|
+
* drag. Without it the first auto-fit would lock the field immediately. */
|
|
131
|
+
let autoDriven = false;
|
|
132
|
+
/** True between pointerdown on the field and the matching pointerup —
|
|
133
|
+
* i.e. while the user may be holding the resize handle. */
|
|
134
|
+
let dragging = false;
|
|
135
|
+
let pending = false;
|
|
136
|
+
/** Set once the height cap has been dropped for a manual drag. */
|
|
137
|
+
let capLifted = false;
|
|
138
|
+
/** Pointer Y at the previous move — the drag is incremental, so only the
|
|
139
|
+
* step between moves matters, never a captured origin. */
|
|
140
|
+
let lastPointerY = 0;
|
|
141
|
+
/** Floor read from CSS at drag start; CSS stays the source of truth. */
|
|
142
|
+
let minPx = 0;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Everything the observer wants to do, moved OUT of the observer.
|
|
146
|
+
*
|
|
147
|
+
* A ResizeObserver callback that reads layout and writes reactive state runs
|
|
148
|
+
* before paint, so doing it synchronously on every frame of a resize drag
|
|
149
|
+
* dirties layout inside the very callback the browser is using to report
|
|
150
|
+
* layout. Chrome throttles that (the "ResizeObserver loop" case) and stops
|
|
151
|
+
* delivering for a frame, which the user feels as the drag seizing up until
|
|
152
|
+
* they release and re-grab. Deferring to rAF leaves the callback trivial.
|
|
153
|
+
*/
|
|
154
|
+
function flush(): void {
|
|
155
|
+
pending = false;
|
|
156
|
+
const el = getEl();
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const h = el.getBoundingClientRect().height;
|
|
159
|
+
updateOverflowHint();
|
|
160
|
+
// A height change we did not author is the user on the drag handle.
|
|
161
|
+
if (!autoDriven && Math.abs(h - lastHeight) > 1) userResized.value = true;
|
|
162
|
+
// Follows LIVE, including mid-drag. Scrolling by the overshoot moves the
|
|
163
|
+
// element up by exactly that much, landing its bottom edge at the viewport
|
|
164
|
+
// edge — which is where the pointer already is, so the grip stays under the
|
|
165
|
+
// cursor. Deferring this to pointerup (an earlier attempt at the stall,
|
|
166
|
+
// which turned out to be the height cap) just made the field vanish off
|
|
167
|
+
// the bottom for the whole drag.
|
|
168
|
+
//
|
|
169
|
+
// Gated on `dragging`, which is what the name always implied. It exists to
|
|
170
|
+
// keep the GRIP under the CURSOR, and there is no grip being held unless a
|
|
171
|
+
// drag is in progress. Ungated it also ran on content-driven growth — so
|
|
172
|
+
// typing a long enough template scrolled the page out from under you — and
|
|
173
|
+
// it made this flush mutate scroll position, which moves the element the
|
|
174
|
+
// observer is measuring. A resize handler that can change its own input is
|
|
175
|
+
// the shape a runaway needs.
|
|
176
|
+
if (dragging && h > lastHeight) followGrip();
|
|
177
|
+
lastHeight = h;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function schedule(): void {
|
|
181
|
+
if (pending) return;
|
|
182
|
+
pending = true;
|
|
183
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
184
|
+
else flush();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Our own resize drag, replacing the native `resize: vertical`.
|
|
189
|
+
*
|
|
190
|
+
* The native resizer computes `startHeight + (pointerY - startY)` from an
|
|
191
|
+
* origin it captured at pointerdown, and it does NOT clamp to the element's
|
|
192
|
+
* own min/max. Drag past a limit and that sum keeps running while the
|
|
193
|
+
* rendered box stays pinned, so dragging back moves nothing until the
|
|
194
|
+
* invisible total walks all the way home. Measured: collapsing past a 34px
|
|
195
|
+
* floor then expanding cost 13 of 23 frames of completely dead travel. That
|
|
196
|
+
* is the "gets stuck, then starts working" report, and it bites exactly when
|
|
197
|
+
* the box reaches one line — which is also when the overflow fade returns,
|
|
198
|
+
* which is why the fade kept looking guilty.
|
|
199
|
+
*
|
|
200
|
+
* Nothing outside can fix it: rewriting `style.height` mid-drag is ignored,
|
|
201
|
+
* because the browser recomputes from its own captured origin every move.
|
|
202
|
+
* Clamping the RESULT does not help either — the accumulated input is the
|
|
203
|
+
* problem, so a clamped absolute drag measured the same 13 / 23.
|
|
204
|
+
*
|
|
205
|
+
* Applying each move's DELTA to the current height instead keeps no history
|
|
206
|
+
* to accumulate: the first pixel back off a limit moves the box. Same
|
|
207
|
+
* harness, same over-drag: 0 / 23.
|
|
208
|
+
*/
|
|
209
|
+
function startResize(ev: PointerEvent): void {
|
|
210
|
+
const el = getEl();
|
|
211
|
+
if (!el) return;
|
|
212
|
+
ev.preventDefault();
|
|
213
|
+
dragging = true;
|
|
214
|
+
userResized.value = true;
|
|
215
|
+
lastPointerY = ev.clientY;
|
|
216
|
+
|
|
217
|
+
const cs = getComputedStyle(el);
|
|
218
|
+
// CSS stays the source of truth for the floor; the ceiling is deliberately
|
|
219
|
+
// released, because a deliberate drag is the user overriding a cap that
|
|
220
|
+
// exists only to bound AUTO-grow.
|
|
221
|
+
// `min-height: auto` (the default) parses to NaN, so a field that never
|
|
222
|
+
// declared one would get a floor of 0 and could be dragged away to a
|
|
223
|
+
// sliver. CSS stays authoritative when it says anything; ABSOLUTE_MIN is
|
|
224
|
+
// only the backstop for when it does not.
|
|
225
|
+
const declared = Number.parseFloat(cs.minHeight);
|
|
226
|
+
minPx = Number.isFinite(declared) && declared > 0 ? declared : ABSOLUTE_MIN;
|
|
227
|
+
if (!capLifted) {
|
|
228
|
+
el.style.height = `${el.getBoundingClientRect().height}px`;
|
|
229
|
+
el.style.maxHeight = "none";
|
|
230
|
+
capLifted = true;
|
|
231
|
+
}
|
|
232
|
+
/* No `setPointerCapture`.
|
|
233
|
+
*
|
|
234
|
+
* It reads like the right call and it is what stranded the drag. Capture
|
|
235
|
+
* retargets every subsequent pointer event to the grip — including
|
|
236
|
+
* `pointerup`. The editor re-renders while it resizes, and a captured
|
|
237
|
+
* element that gets replaced is detached: the `pointerup` is delivered to a
|
|
238
|
+
* node no longer in the tree, so it never bubbles to `window`, `dragging`
|
|
239
|
+
* stays true, and the box follows the cursor with no button held and no way
|
|
240
|
+
* to stop it. That is the reported symptom exactly.
|
|
241
|
+
*
|
|
242
|
+
* Capture bought nothing here anyway. The listeners below are on `window`,
|
|
243
|
+
* which sees the events regardless of what is under the cursor.
|
|
244
|
+
*
|
|
245
|
+
* CAPTURE PHASE on all three, so a `stopPropagation()` between the target
|
|
246
|
+
* and the window — ComfyUI's node layer does this to stop the canvas
|
|
247
|
+
* panning under a widget — cannot hide the end of the drag from us.
|
|
248
|
+
*/
|
|
249
|
+
window.addEventListener("pointermove", onResizeMove, true);
|
|
250
|
+
window.addEventListener("pointerup", onPointerUp, true);
|
|
251
|
+
window.addEventListener("pointercancel", onPointerUp, true);
|
|
252
|
+
// A fresh press means the previous drag is over however it ended.
|
|
253
|
+
window.addEventListener("pointerdown", onPointerUp, true);
|
|
254
|
+
window.addEventListener("blur", onPointerUp);
|
|
255
|
+
window.addEventListener("keydown", onDragEscape, true);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Escape ends a drag. Cheap, and it is the key a stuck user presses. */
|
|
259
|
+
function onDragEscape(ev: KeyboardEvent): void {
|
|
260
|
+
if (ev.key === "Escape") onPointerUp();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onResizeMove(ev: PointerEvent): void {
|
|
264
|
+
const el = getEl();
|
|
265
|
+
if (!el || !dragging) return;
|
|
266
|
+
// Self-heal. Every pointermove reports which buttons are down, so a move
|
|
267
|
+
// with none held means the pointerup was lost — whatever swallowed it —
|
|
268
|
+
// and the drag should already be over. Without this the field follows the
|
|
269
|
+
// cursor forever and no key or click can stop it, because nothing else in
|
|
270
|
+
// here ever clears `dragging`. Belt and braces on top of the listener
|
|
271
|
+
// changes above: those address the causes we found, this ends the state
|
|
272
|
+
// regardless of a cause we did not.
|
|
273
|
+
if (ev.buttons === 0) {
|
|
274
|
+
onPointerUp();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const dy = ev.clientY - lastPointerY;
|
|
278
|
+
lastPointerY = ev.clientY;
|
|
279
|
+
const cur = el.getBoundingClientRect().height;
|
|
280
|
+
el.style.height = `${Math.max(minPx, cur + dy)}px`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function onPointerUp(): void {
|
|
284
|
+
if (!dragging) return;
|
|
285
|
+
dragging = false;
|
|
286
|
+
// Re-arm for the next drag. `capLifted` guards a one-time "pin the current
|
|
287
|
+
// height, release the max-height cap" step, and leaving it set meant the
|
|
288
|
+
// release survived for the lifetime of the field while the explicit height
|
|
289
|
+
// that bounded it did not — `applyAtoms` replaces the host element on a
|
|
290
|
+
// rebuild, which drops the inline height but not this flag. The next drag
|
|
291
|
+
// then found an uncapped, unpinned box. Each drag now re-establishes both
|
|
292
|
+
// against the element that actually exists.
|
|
293
|
+
capLifted = false;
|
|
294
|
+
window.removeEventListener("pointermove", onResizeMove);
|
|
295
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
296
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Idempotent: callers re-attach when the element they own is REPLACED
|
|
300
|
+
* rather than mutated (RichTextInput swaps its contenteditable host to
|
|
301
|
+
* repair a browser-mangled DOM). Without the disconnect the old observer
|
|
302
|
+
* would keep watching a detached node and leak, and the hint would go
|
|
303
|
+
* permanently stale on the live one. */
|
|
304
|
+
function attach(): void {
|
|
305
|
+
obs?.disconnect();
|
|
306
|
+
obs = null;
|
|
307
|
+
const el = getEl();
|
|
308
|
+
if (!el) return;
|
|
309
|
+
lastHeight = el.getBoundingClientRect().height;
|
|
310
|
+
|
|
311
|
+
/* Re-measure once the webfonts land.
|
|
312
|
+
*
|
|
313
|
+
* These fields are typed in `--wp-font-mono`, a bundled webfont declared
|
|
314
|
+
* `font-display: swap` — so the first paint uses the system fallback
|
|
315
|
+
* (Consolas / Monaco) and the real face swaps in later. The two stacks
|
|
316
|
+
* have different metrics, so the `scrollHeight` measured at mount is taken
|
|
317
|
+
* against type that is about to be replaced.
|
|
318
|
+
*
|
|
319
|
+
* That is the "sometimes the bottom fade is wrong" report: on a cold load
|
|
320
|
+
* the hint is computed against the fallback and never recomputed, and on a
|
|
321
|
+
* warm cache the font is already there so it looks fine. Any later scroll,
|
|
322
|
+
* input or resize also fixes it by accident, which is what made it look
|
|
323
|
+
* intermittent rather than deterministic.
|
|
324
|
+
*
|
|
325
|
+
* `document.fonts.ready` resolves once, after all pending faces settle.
|
|
326
|
+
* Cheap, and a no-op where the font was already cached.
|
|
327
|
+
*/
|
|
328
|
+
if (typeof document !== "undefined" && document.fonts?.ready) {
|
|
329
|
+
void document.fonts.ready.then(() => {
|
|
330
|
+
// The field may have unmounted during the load.
|
|
331
|
+
if (!getEl()) return;
|
|
332
|
+
updateOverflowHint();
|
|
333
|
+
autosize();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
338
|
+
obs = new ResizeObserver(schedule);
|
|
339
|
+
obs.observe(el);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
onBeforeUnmount(() => {
|
|
343
|
+
obs?.disconnect();
|
|
344
|
+
obs = null;
|
|
345
|
+
onPointerUp();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
hasMoreBelow,
|
|
350
|
+
updateOverflowHint,
|
|
351
|
+
scheduleOverflowHint,
|
|
352
|
+
autosize,
|
|
353
|
+
attach,
|
|
354
|
+
userResized,
|
|
355
|
+
startResize,
|
|
356
|
+
};
|
|
357
|
+
}
|