@dumiflex/wp-kit 0.0.0-stage → 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.md +77 -0
- package/LICENSE +674 -0
- package/README.md +72 -2
- package/package.json +76 -4
- package/src/Avatar.vue +83 -0
- package/src/Badge.vue +97 -0
- package/src/Banner.vue +118 -0
- package/src/BookmarkButton.vue +27 -0
- package/src/BundleMeta.vue +51 -0
- package/src/Button.vue +103 -0
- package/src/Card.vue +68 -0
- package/src/Checkbox.vue +109 -0
- package/src/Chip.vue +163 -0
- package/src/ChoiceCards.vue +133 -0
- package/src/CloseButton.vue +69 -0
- package/src/CodeBlock.vue +119 -0
- package/src/ColorField.vue +188 -0
- package/src/ColorPicker.vue +201 -0
- package/src/Combobox.vue +331 -0
- package/src/ConfirmDialog.vue +165 -0
- package/src/ConfirmModal.vue +147 -0
- package/src/ConstraintMatrixPreview.vue +708 -0
- package/src/ContextMenu.vue +386 -0
- package/src/CopyButton.vue +156 -0
- package/src/DataTable.vue +292 -0
- package/src/DatePicker.vue +234 -0
- package/src/DescriptionList.vue +73 -0
- package/src/DiffView.vue +68 -0
- package/src/Disclosure.vue +166 -0
- package/src/DragHandle.vue +49 -0
- package/src/Drawer.vue +131 -0
- package/src/DropZone.vue +196 -0
- package/src/DropdownMenu.vue +72 -0
- package/src/EmptyState.vue +111 -0
- package/src/Field.vue +28 -0
- package/src/HsvPicker.vue +208 -0
- package/src/Icon.vue +39 -0
- package/src/IconPicker.vue +209 -0
- package/src/ImageGrid.vue +125 -0
- package/src/InlineEdit.vue +183 -0
- package/src/Input.vue +99 -0
- package/src/Kbd.vue +56 -0
- package/src/Lightbox.vue +173 -0
- package/src/ListFilter.vue +85 -0
- package/src/ListRow.vue +204 -0
- package/src/LoadingMark.vue +175 -0
- package/src/Logo.vue +60 -0
- package/src/Modal.vue +78 -0
- package/src/ModalShell.vue +109 -0
- package/src/Pagination.vue +106 -0
- package/src/Popover.vue +180 -0
- package/src/PostCard.vue +210 -0
- package/src/ProgressBar.vue +90 -0
- package/src/RadioGroup.vue +177 -0
- package/src/RangeSlider.vue +183 -0
- package/src/RelativeDate.vue +35 -0
- package/src/ResizeHandle.vue +143 -0
- package/src/SearchInput.vue +192 -0
- package/src/Select.vue +644 -0
- package/src/Separator.vue +54 -0
- package/src/Skeleton.vue +82 -0
- package/src/Slider.vue +198 -0
- package/src/SortableList.vue +260 -0
- package/src/SplitPane.vue +68 -0
- package/src/StarButton.vue +40 -0
- package/src/Stepper.vue +141 -0
- package/src/SupportLinks.vue +93 -0
- package/src/TagInput.vue +297 -0
- package/src/Textarea.vue +57 -0
- package/src/Timeline.vue +92 -0
- package/src/ToastCard.vue +119 -0
- package/src/ToastHost.vue +40 -0
- package/src/Toggle.vue +36 -0
- package/src/TokenizedValue.vue +310 -0
- package/src/Toolbar.vue +110 -0
- package/src/VirtualList.vue +87 -0
- package/src/VisuallyHidden.vue +29 -0
- package/src/WpButton.vue +75 -0
- package/src/WpCheck.vue +104 -0
- package/src/WpIconButton.vue +107 -0
- package/src/WpImageZoom.vue +544 -0
- package/src/WpNumber.vue +178 -0
- package/src/WpSegmented.vue +175 -0
- package/src/WpSelect.vue +93 -0
- package/src/WpSwitch.vue +163 -0
- package/src/WpTabs.vue +188 -0
- package/src/_modal-motion.css +33 -0
- package/src/_skeleton.css +50 -0
- package/src/axis-color.ts +42 -0
- package/src/button.css +169 -0
- package/src/calendar.ts +59 -0
- package/src/clipboard.ts +50 -0
- package/src/colour-presets.ts +12 -0
- package/src/diff.ts +51 -0
- package/src/drop-zone.ts +41 -0
- package/src/field.css +50 -0
- package/src/flip.ts +237 -0
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -0
- package/src/hsv.ts +100 -0
- package/src/icon-catalog.ts +52 -0
- package/src/image-zoom.ts +93 -0
- package/src/kind-colour.ts +28 -0
- package/src/logo-paths.ts +21 -0
- package/src/matrix-axis.ts +91 -0
- package/src/pagination.ts +27 -0
- package/src/platform-keys.ts +29 -0
- package/src/rich-tokenize.ts +357 -0
- package/src/select-types.ts +32 -0
- package/src/slider-math.ts +45 -0
- package/src/sortable.ts +22 -0
- package/src/theme/fonts/OFL-Fraunces.txt +93 -0
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -0
- package/src/theme/fonts/OFL-Manrope.txt +93 -0
- package/src/theme/fonts/fraunces-latin-wght-normal.woff2 +0 -0
- package/src/theme/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/src/theme/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/src/theme/looks.css +428 -0
- package/src/theme/palettes.css +254 -0
- package/src/theme/tokens.css +2896 -0
- package/src/tokens.ts +56 -0
- package/src/tooltip.css +43 -0
- package/src/tooltip.ts +146 -0
- package/src/useDialogFocus.ts +73 -0
- package/src/useGrowableField.ts +357 -0
- package/src/useModalKeyShield.ts +116 -0
- package/src/useToast.ts +48 -0
- package/src/wp-logo.svg +14 -0
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The CSS custom properties a kit piece may read.
|
|
2
|
+
//
|
|
3
|
+
// Most pieces also run on the ComfyUI canvas, whose colour sheet is the
|
|
4
|
+
// extension's own (components/shared/theme.css), not src/theme/. Those pieces
|
|
5
|
+
// read only SHARED_TOKENS, which every host defines, so a piece wears
|
|
6
|
+
// whichever side's colours it sits on. The pieces in PAGE_PIECES only run on
|
|
7
|
+
// pages that load src/theme/tokens.css (the manager, the community site, the
|
|
8
|
+
// kit site), so they may read any name that sheet defines.
|
|
9
|
+
// tokens.test.ts holds both rules.
|
|
10
|
+
//
|
|
11
|
+
// Add a shared name only once every host defines it (docs/tokens.md).
|
|
12
|
+
|
|
13
|
+
export const SHARED_TOKENS = [
|
|
14
|
+
// Surfaces and text
|
|
15
|
+
"--wp-bg", "--wp-bg-1", "--wp-bg-2", "--wp-bg-3", "--wp-bg-4",
|
|
16
|
+
"--wp-text", "--wp-text-muted", "--wp-text-dim",
|
|
17
|
+
"--wp-border", "--wp-border-strong", "--wp-border-soft", "--wp-border-focus",
|
|
18
|
+
"--wp-overlay-bg", "--wp-row-hover", "--wp-input-shade", "--wp-scrollbar-thumb",
|
|
19
|
+
// Accent and states
|
|
20
|
+
"--wp-accent", "--wp-accent-glow", "--wp-accent-text", "--wp-accent-text-strong",
|
|
21
|
+
"--wp-accent-300", "--wp-accent-400", "--wp-accent-500", "--wp-accent-600", "--wp-accent-700", "--wp-accent-800",
|
|
22
|
+
"--wp-focus-ring", "--wp-on-accent", "--wp-on-media", "--wp-scrim",
|
|
23
|
+
"--wp-success", "--wp-warn", "--wp-danger", "--wp-info", "--wp-danger-text",
|
|
24
|
+
"--wp-error-border", "--wp-error-text",
|
|
25
|
+
"--wp-red", "--wp-red-bg", "--wp-green", "--wp-green-bg", "--wp-amber", "--wp-amber-bg",
|
|
26
|
+
"--wp-teal", "--wp-teal-bg", "--wp-violet", "--wp-violet-bg", "--wp-rose", "--wp-rose-bg",
|
|
27
|
+
// Kinds and variables
|
|
28
|
+
"--wp-kind-wildcard", "--wp-kind-fixed", "--wp-kind-combine", "--wp-kind-derivation", "--wp-kind-constraint",
|
|
29
|
+
"--wp-var-1", "--wp-var-2", "--wp-var-3", "--wp-var-4", "--wp-var-5", "--wp-var-6", "--wp-var-7", "--wp-var-8",
|
|
30
|
+
// Shape, depth and type
|
|
31
|
+
"--wp-radius-sm", "--wp-radius", "--wp-radius-md", "--wp-radius-lg", "--wp-radius-xl", "--wp-radius-cap",
|
|
32
|
+
"--wp-shadow-sm", "--wp-shadow", "--wp-shadow-md", "--wp-shadow-lg", "--wp-shadow-xl",
|
|
33
|
+
"--wp-font-sans", "--wp-font-mono",
|
|
34
|
+
"--wp-text-xs", "--wp-text-sm", "--wp-text-base", "--wp-text-md", "--wp-text-lg", "--wp-text-xl",
|
|
35
|
+
"--wp-space-0", "--wp-space-1", "--wp-space-2", "--wp-space-3", "--wp-space-4",
|
|
36
|
+
"--wp-space-5", "--wp-space-6", "--wp-space-7", "--wp-space-8",
|
|
37
|
+
"--wp-control-h-sm", "--wp-control-h-md", "--wp-control-h-lg",
|
|
38
|
+
// Motion
|
|
39
|
+
"--wp-motion-quick", "--wp-motion-hover", "--wp-motion-shimmer", "--wp-motion-fade", "--wp-motion-swap", "--wp-motion-flip",
|
|
40
|
+
"--wp-motion-curve-collapse", "--wp-motion-curve-flip", "--wp-motion-curve-linear",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
/** Read with a fallback (`var(--wp-bg-deep, var(--wp-bg))`) or kept for older
|
|
44
|
+
* hosts; a host may leave these out. */
|
|
45
|
+
export const OPTIONAL_TOKENS = ["--wp-bg-deep", "--wp-bg2"] as const;
|
|
46
|
+
|
|
47
|
+
/** Set by the piece itself, on its own root, not by the host. */
|
|
48
|
+
export const LOCAL_TOKENS = ["--wp-hsv-hue", "--wp-lm-t", "--wp-lm-x", "--wp-lm-r", "--wp-lm-s", "--wp-lm-o", "--wp-select-line-h", "--wp-list-row-depth", "--wp-list-row-icon", "--wp-chip-c", "--wp-field-h"] as const;
|
|
49
|
+
|
|
50
|
+
/** Pieces that need a page with src/theme/ loaded (not the canvas). */
|
|
51
|
+
export const PAGE_PIECES = [
|
|
52
|
+
"axis-color.ts", "kind-colour.ts", "matrix-axis.ts", "BundleMeta.vue", "ConstraintMatrixPreview.vue", "PostCard.vue", "TokenizedValue.vue",
|
|
53
|
+
"Button.vue", "Card.vue", "Checkbox.vue", "Chip.vue", "ChoiceCards.vue", "ColorPicker.vue", "ConfirmModal.vue",
|
|
54
|
+
"DataTable.vue", "Drawer.vue", "EmptyState.vue", "Field.vue", "Icon.vue", "IconPicker.vue", "Input.vue",
|
|
55
|
+
"ListFilter.vue", "Modal.vue", "RelativeDate.vue", "Select.vue", "Textarea.vue", "ToastHost.vue", "Toggle.vue",
|
|
56
|
+
] as const;
|
package/src/tooltip.css
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* The one shared tooltip element (tooltip.ts). Colours come from tokens both
|
|
2
|
+
* the canvas and the manager define, so it matches whichever side it is on. */
|
|
3
|
+
@layer wp-extension {
|
|
4
|
+
.wp-tooltip {
|
|
5
|
+
position: fixed;
|
|
6
|
+
z-index: 10001;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
max-width: 280px;
|
|
11
|
+
padding: 6px 10px;
|
|
12
|
+
border: 1px solid var(--wp-border-strong);
|
|
13
|
+
border-radius: var(--wp-radius);
|
|
14
|
+
background: var(--wp-bg-3);
|
|
15
|
+
color: var(--wp-text);
|
|
16
|
+
box-shadow: var(--wp-shadow-md);
|
|
17
|
+
font: 500 12px/1.4 var(--wp-font-sans);
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
white-space: normal;
|
|
20
|
+
overflow-wrap: break-word;
|
|
21
|
+
opacity: 0;
|
|
22
|
+
visibility: hidden;
|
|
23
|
+
transition: opacity 90ms ease-out, visibility 0s linear 90ms;
|
|
24
|
+
}
|
|
25
|
+
.wp-tooltip[data-open] {
|
|
26
|
+
opacity: 1;
|
|
27
|
+
visibility: visible;
|
|
28
|
+
transition: opacity 90ms ease-out;
|
|
29
|
+
}
|
|
30
|
+
.wp-tooltip__kbd {
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
padding: 1px 6px;
|
|
33
|
+
border: 1px solid var(--wp-border-strong);
|
|
34
|
+
border-radius: var(--wp-radius-sm);
|
|
35
|
+
background: var(--wp-bg-1);
|
|
36
|
+
color: var(--wp-text-muted);
|
|
37
|
+
font: 500 11px/16px var(--wp-font-mono);
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
}
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
.wp-tooltip, .wp-tooltip[data-open] { transition: none; }
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/tooltip.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `v-tooltip`: a short hint shown on hover (after a short wait) and on
|
|
3
|
+
* keyboard focus, hidden on leave, blur or Escape. Use it instead of
|
|
4
|
+
* `title=`, which is slow, unstyled, never shows from the keyboard and is
|
|
5
|
+
* cut off on the canvas.
|
|
6
|
+
*
|
|
7
|
+
* <Button v-tooltip="'Copy the prompt'" icon="pi-copy" aria-label="Copy" />
|
|
8
|
+
* <span v-tooltip="{ text: 'Runs first', placement: 'right' }">…</span>
|
|
9
|
+
* <Button v-tooltip="{ text: 'Copy to clipboard', kbd: 'Ctrl C' }" … />
|
|
10
|
+
*
|
|
11
|
+
* One tooltip element is shared by the whole page, so a thousand hints
|
|
12
|
+
* cost one node. State per element lives in a WeakMap (no expando keys).
|
|
13
|
+
*/
|
|
14
|
+
import type { Directive } from "vue";
|
|
15
|
+
import { placeFloating, viewportSize, type Placement } from "./floating";
|
|
16
|
+
import "./tooltip.css";
|
|
17
|
+
|
|
18
|
+
export type TooltipValue =
|
|
19
|
+
| string
|
|
20
|
+
| null
|
|
21
|
+
| undefined
|
|
22
|
+
| {
|
|
23
|
+
text: string | null | undefined;
|
|
24
|
+
placement?: Placement | undefined;
|
|
25
|
+
/** A shortcut shown after the text, e.g. "Ctrl C". */
|
|
26
|
+
kbd?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface State {
|
|
30
|
+
text: string;
|
|
31
|
+
kbd: string;
|
|
32
|
+
placement: Placement;
|
|
33
|
+
timer: number | undefined;
|
|
34
|
+
off: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SHOW_DELAY = 450;
|
|
38
|
+
/** Moving from one hint to the next within this window shows it at once. */
|
|
39
|
+
const WARM_MS = 600;
|
|
40
|
+
|
|
41
|
+
const states = new WeakMap<HTMLElement, State>();
|
|
42
|
+
let tipEl: HTMLDivElement | null = null;
|
|
43
|
+
let owner: HTMLElement | null = null;
|
|
44
|
+
let lastHidden = 0;
|
|
45
|
+
let uid = 0;
|
|
46
|
+
|
|
47
|
+
function read(v: TooltipValue): { text: string; kbd: string; placement: Placement } {
|
|
48
|
+
if (v && typeof v === "object") return { text: v.text ?? "", kbd: v.kbd ?? "", placement: v.placement ?? "top" };
|
|
49
|
+
return { text: v ?? "", kbd: "", placement: "top" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tip(): HTMLDivElement {
|
|
53
|
+
if (tipEl && tipEl.isConnected) return tipEl;
|
|
54
|
+
tipEl = document.createElement("div");
|
|
55
|
+
tipEl.className = "wp-tooltip";
|
|
56
|
+
tipEl.id = `wp-tooltip-${++uid}`;
|
|
57
|
+
tipEl.setAttribute("role", "tooltip");
|
|
58
|
+
document.body.appendChild(tipEl);
|
|
59
|
+
return tipEl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function show(el: HTMLElement): void {
|
|
63
|
+
const s = states.get(el);
|
|
64
|
+
if (!s || !s.text || !el.isConnected) return;
|
|
65
|
+
const t = tip();
|
|
66
|
+
t.textContent = s.text;
|
|
67
|
+
if (s.kbd) {
|
|
68
|
+
const k = document.createElement("kbd");
|
|
69
|
+
k.className = "wp-tooltip__kbd";
|
|
70
|
+
k.textContent = s.kbd;
|
|
71
|
+
t.append(" ", k);
|
|
72
|
+
}
|
|
73
|
+
t.dataset.open = "true";
|
|
74
|
+
owner = el;
|
|
75
|
+
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
|
|
76
|
+
if (!ids.includes(t.id)) el.setAttribute("aria-describedby", [...ids, t.id].join(" "));
|
|
77
|
+
const at = placeFloating(el.getBoundingClientRect(), { width: t.offsetWidth, height: t.offsetHeight }, viewportSize(), {
|
|
78
|
+
placement: s.placement,
|
|
79
|
+
});
|
|
80
|
+
t.style.left = `${at.x}px`;
|
|
81
|
+
t.style.top = `${at.y}px`;
|
|
82
|
+
t.dataset.side = at.side;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hide(el: HTMLElement): void {
|
|
86
|
+
const s = states.get(el);
|
|
87
|
+
if (s?.timer !== undefined) { window.clearTimeout(s.timer); s.timer = undefined; }
|
|
88
|
+
if (owner !== el) return;
|
|
89
|
+
owner = null;
|
|
90
|
+
lastHidden = Date.now();
|
|
91
|
+
if (tipEl) {
|
|
92
|
+
delete tipEl.dataset.open;
|
|
93
|
+
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter((i) => i && i !== tipEl?.id);
|
|
94
|
+
if (ids.length) el.setAttribute("aria-describedby", ids.join(" "));
|
|
95
|
+
else el.removeAttribute("aria-describedby");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function schedule(el: HTMLElement): void {
|
|
100
|
+
const s = states.get(el);
|
|
101
|
+
if (!s) return;
|
|
102
|
+
if (s.timer !== undefined) window.clearTimeout(s.timer);
|
|
103
|
+
const warm = owner !== null || Date.now() - lastHidden < WARM_MS;
|
|
104
|
+
if (warm) { show(el); return; }
|
|
105
|
+
s.timer = window.setTimeout(() => { s.timer = undefined; show(el); }, SHOW_DELAY);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const vTooltip: Directive<HTMLElement, TooltipValue> = {
|
|
109
|
+
mounted(el, binding) {
|
|
110
|
+
const enter = (): void => schedule(el);
|
|
111
|
+
const leave = (): void => hide(el);
|
|
112
|
+
const focus = (): void => { if (el.matches(":focus-visible")) show(el); };
|
|
113
|
+
const key = (ev: KeyboardEvent): void => { if (ev.key === "Escape" && owner === el) hide(el); };
|
|
114
|
+
el.addEventListener("pointerenter", enter);
|
|
115
|
+
el.addEventListener("pointerleave", leave);
|
|
116
|
+
el.addEventListener("pointerdown", leave);
|
|
117
|
+
el.addEventListener("focusin", focus);
|
|
118
|
+
el.addEventListener("focusout", leave);
|
|
119
|
+
el.addEventListener("keydown", key);
|
|
120
|
+
states.set(el, {
|
|
121
|
+
...read(binding.value),
|
|
122
|
+
timer: undefined,
|
|
123
|
+
off: () => {
|
|
124
|
+
el.removeEventListener("pointerenter", enter);
|
|
125
|
+
el.removeEventListener("pointerleave", leave);
|
|
126
|
+
el.removeEventListener("pointerdown", leave);
|
|
127
|
+
el.removeEventListener("focusin", focus);
|
|
128
|
+
el.removeEventListener("focusout", leave);
|
|
129
|
+
el.removeEventListener("keydown", key);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
updated(el, binding) {
|
|
134
|
+
const s = states.get(el);
|
|
135
|
+
if (!s) return;
|
|
136
|
+
Object.assign(s, read(binding.value));
|
|
137
|
+
if (owner === el) { if (s.text) show(el); else hide(el); }
|
|
138
|
+
},
|
|
139
|
+
beforeUnmount(el) {
|
|
140
|
+
hide(el);
|
|
141
|
+
states.get(el)?.off();
|
|
142
|
+
states.delete(el);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default vTooltip;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus handling every dialog-like panel needs (Modal, Drawer): while open,
|
|
3
|
+
* Escape calls `onEscape` and Tab stays inside the panel; on open, focus
|
|
4
|
+
* moves in (`initial`, else the first control, else the panel itself); on
|
|
5
|
+
* close, it goes back to where it was.
|
|
6
|
+
*/
|
|
7
|
+
import { nextTick, onBeforeUnmount, watch, type Ref } from "vue";
|
|
8
|
+
|
|
9
|
+
export const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), '
|
|
10
|
+
+ 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
11
|
+
|
|
12
|
+
export function focusablesIn(root: HTMLElement | null): HTMLElement[] {
|
|
13
|
+
return Array.from(root?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])
|
|
14
|
+
.filter((el) => !el.closest("[hidden], [inert]"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DialogFocusOptions {
|
|
18
|
+
onEscape: () => void;
|
|
19
|
+
/** Where focus goes on open; defaults to the first control. */
|
|
20
|
+
initial?: (root: HTMLElement, controls: HTMLElement[]) => HTMLElement | null | undefined;
|
|
21
|
+
/** Keep Tab inside the panel (default). Off for a panel beside the page. */
|
|
22
|
+
trap?: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useDialogFocus(panel: Ref<HTMLElement | null>, isOpen: () => boolean, opts: DialogFocusOptions): void {
|
|
26
|
+
let returnTo: HTMLElement | null = null;
|
|
27
|
+
|
|
28
|
+
function onKeydown(e: KeyboardEvent): void {
|
|
29
|
+
if (e.key === "Escape") { opts.onEscape(); return; }
|
|
30
|
+
const root = panel.value;
|
|
31
|
+
if (e.key !== "Tab" || !root || opts.trap?.() === false) return;
|
|
32
|
+
const els = focusablesIn(root);
|
|
33
|
+
if (!els.length) { e.preventDefault(); root.focus(); return; }
|
|
34
|
+
const first = els[0];
|
|
35
|
+
const last = els[els.length - 1];
|
|
36
|
+
const at = document.activeElement;
|
|
37
|
+
const inside = at instanceof Node && root.contains(at);
|
|
38
|
+
if (e.shiftKey && (at === first || !inside)) { e.preventDefault(); last?.focus(); }
|
|
39
|
+
else if (!e.shiftKey && (at === last || !inside)) { e.preventDefault(); first?.focus(); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function opened(): Promise<void> {
|
|
43
|
+
returnTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
44
|
+
await nextTick();
|
|
45
|
+
const root = panel.value;
|
|
46
|
+
if (!root || root.contains(document.activeElement)) return;
|
|
47
|
+
const els = focusablesIn(root);
|
|
48
|
+
((opts.initial ? opts.initial(root, els) : els[0]) ?? root).focus();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function closed(): void {
|
|
52
|
+
const el = returnTo;
|
|
53
|
+
returnTo = null;
|
|
54
|
+
if (el?.isConnected) el.focus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
watch(isOpen, (v, was) => {
|
|
58
|
+
if (typeof window === "undefined") return;
|
|
59
|
+
if (v) {
|
|
60
|
+
window.addEventListener("keydown", onKeydown);
|
|
61
|
+
void opened();
|
|
62
|
+
} else {
|
|
63
|
+
window.removeEventListener("keydown", onKeydown);
|
|
64
|
+
if (was) closed();
|
|
65
|
+
}
|
|
66
|
+
}, { immediate: true });
|
|
67
|
+
|
|
68
|
+
onBeforeUnmount(() => {
|
|
69
|
+
if (typeof window === "undefined") return;
|
|
70
|
+
window.removeEventListener("keydown", onKeydown);
|
|
71
|
+
if (isOpen()) closed();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +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
|
+
}
|