ropav 0.11.0 → 0.11.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.
- package/dist/components/autocomplete/autocomplete-filter.d.ts +1 -1
- package/dist/components/textarea/textarea-root.js +20 -0
- package/dist/components/textarea/textarea.types.d.ts +21 -0
- package/dist/composables/use-textarea-autosize.d.ts +55 -0
- package/dist/composables/use-textarea-autosize.js +189 -0
- package/dist/ropav.min.css +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AutocompleteFilterProps, AutocompleteFilterSlotProps } from "./autocomplete.types.js";
|
|
2
|
-
declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
|
|
2
|
+
declare const __VLS_export: <T = any>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
|
|
3
3
|
props: import('vue').PublicProps & __VLS_PrettifyLocal<AutocompleteFilterProps<T> & {
|
|
4
4
|
onInputChange?: ((value: string) => any) | undefined;
|
|
5
5
|
"onUpdate:inputValue"?: ((value: string) => any) | undefined;
|
|
@@ -3,6 +3,7 @@ import { useInteractionStates } from "../../composables/use-interaction-states.j
|
|
|
3
3
|
import { setFormValue } from "../../utils/form-value.js";
|
|
4
4
|
import { useTextFieldControlContext } from "../../composables/use-text-field.js";
|
|
5
5
|
import { useTextFieldContext } from "../textfield/textfield.context.js";
|
|
6
|
+
import { useTextareaAutosize } from "../../composables/use-textarea-autosize.js";
|
|
6
7
|
import { computed, createTemplateRefSetter, defineVaporComponent, on, renderEffect, setDynamicProps, shallowRef, template, unref, watch } from "vue";
|
|
7
8
|
import { textAreaVariants } from "@ropav/styles";
|
|
8
9
|
//#region src/components/textarea/textarea-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
@@ -18,6 +19,13 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
|
|
|
18
19
|
type: Boolean,
|
|
19
20
|
default: void 0
|
|
20
21
|
},
|
|
22
|
+
autosize: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: void 0
|
|
25
|
+
},
|
|
26
|
+
minRows: { default: void 0 },
|
|
27
|
+
maxRows: { default: void 0 },
|
|
28
|
+
resize: { default: void 0 },
|
|
21
29
|
placeholder: {}
|
|
22
30
|
},
|
|
23
31
|
emits: ["change", "update:value"],
|
|
@@ -33,9 +41,11 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
|
|
|
33
41
|
};
|
|
34
42
|
const resolvedVariant = computed(() => props.variant ?? textField?.variant.value);
|
|
35
43
|
const resolvedSize = computed(() => props.size ?? textField?.size.value);
|
|
44
|
+
const resolvedResize = computed(() => props.autosize ? "none" : props.resize);
|
|
36
45
|
const styles = computed(() => textAreaVariants({
|
|
37
46
|
class: props.class,
|
|
38
47
|
fullWidth: props.fullWidth,
|
|
48
|
+
resize: resolvedResize.value,
|
|
39
49
|
size: resolvedSize.value,
|
|
40
50
|
variant: resolvedVariant.value
|
|
41
51
|
}));
|
|
@@ -43,15 +53,24 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
|
|
|
43
53
|
const merged = { ...control?.attrs.value };
|
|
44
54
|
if (props.placeholder !== void 0) merged["placeholder"] = props.placeholder;
|
|
45
55
|
if (props.value !== void 0) merged["value"] = props.value;
|
|
56
|
+
if (props.autosize && props.minRows !== void 0) merged["rows"] = props.minRows;
|
|
46
57
|
return merged;
|
|
47
58
|
});
|
|
48
59
|
const interaction = useInteractionStates({ isDisabled: () => control?.isDisabled.value });
|
|
49
60
|
const inputCount = shallowRef(0);
|
|
61
|
+
const { sync: syncAutosize } = useTextareaAutosize({
|
|
62
|
+
content: () => control?.attrs.value["value"],
|
|
63
|
+
element,
|
|
64
|
+
enabled: () => props.autosize,
|
|
65
|
+
maxRows: () => props.maxRows,
|
|
66
|
+
minRows: () => props.minRows
|
|
67
|
+
});
|
|
50
68
|
const onInput = (event) => {
|
|
51
69
|
control?.handlers.onInput(event);
|
|
52
70
|
const next = event.currentTarget.value;
|
|
53
71
|
emit("change", next);
|
|
54
72
|
emit("update:value", next);
|
|
73
|
+
syncAutosize({ fromInput: true });
|
|
55
74
|
if (props.value !== void 0) inputCount.value++;
|
|
56
75
|
};
|
|
57
76
|
/**
|
|
@@ -66,6 +85,7 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
|
|
|
66
85
|
], ([el, pinned]) => {
|
|
67
86
|
if (pinned === void 0) return;
|
|
68
87
|
setFormValue(el, pinned);
|
|
88
|
+
syncAutosize();
|
|
69
89
|
}, {
|
|
70
90
|
flush: "post",
|
|
71
91
|
immediate: true
|
|
@@ -16,6 +16,27 @@ export interface TextAreaRootProps {
|
|
|
16
16
|
size?: TextAreaVariants["size"];
|
|
17
17
|
/** Whether the control stretches to fill its container. */
|
|
18
18
|
fullWidth?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the control grows with its content. `minRows` and `maxRows` only apply when this
|
|
21
|
+
* is set. Native resize is forced off while it is, so the drag handle cannot fight the
|
|
22
|
+
* measured height. @default false
|
|
23
|
+
*/
|
|
24
|
+
autosize?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Minimum visible rows while `autosize` is set. Ignored otherwise. Written through to the
|
|
27
|
+
* native `rows` attribute so the first paint is already that tall.
|
|
28
|
+
*/
|
|
29
|
+
minRows?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum visible rows while `autosize` is set. Ignored otherwise. Absent, the control grows
|
|
32
|
+
* without a cap.
|
|
33
|
+
*/
|
|
34
|
+
maxRows?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the pointer can drag the edge. `none` by default; `vertical` or `both` opt in.
|
|
37
|
+
* Ignored while `autosize` is set. @default "none"
|
|
38
|
+
*/
|
|
39
|
+
resize?: TextAreaVariants["resize"];
|
|
19
40
|
/**
|
|
20
41
|
* Placeholder shown while the control is empty. Declared so it can also be set here rather
|
|
21
42
|
* than only on the field; every other native attribute arrives by attribute fallthrough.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from "vue";
|
|
2
|
+
export interface UseTextareaAutosizeOptions {
|
|
3
|
+
element: MaybeRefOrGetter<HTMLTextAreaElement | null | undefined>;
|
|
4
|
+
/** When false, inline height and overflow are cleared and the native `rows` height is used. */
|
|
5
|
+
enabled: MaybeRefOrGetter<boolean | undefined>;
|
|
6
|
+
/** Floor, in rows. Ignored when `enabled` is false. */
|
|
7
|
+
minRows: MaybeRefOrGetter<number | undefined>;
|
|
8
|
+
/** Ceiling, in rows. Absent, the control grows without a cap. Ignored when `enabled` is false. */
|
|
9
|
+
maxRows: MaybeRefOrGetter<number | undefined>;
|
|
10
|
+
/**
|
|
11
|
+
* Text held somewhere the element cannot be read from. Reading `element.value` is not
|
|
12
|
+
* reactive, so a caller that does not already remeasure when its own text moves passes
|
|
13
|
+
* the value it holds here.
|
|
14
|
+
*/
|
|
15
|
+
content?: MaybeRefOrGetter<unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface UseTextareaAutosizeSyncOptions {
|
|
18
|
+
/**
|
|
19
|
+
* From `input` only. A caret at the end then keeps the last line's padding in view.
|
|
20
|
+
* Layout remeasures omit this so a scroll the user made is not stolen.
|
|
21
|
+
*/
|
|
22
|
+
fromInput?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface UseTextareaAutosizeReturn {
|
|
25
|
+
/** Measure now. Needed from `input`, where the DOM already holds the next text. */
|
|
26
|
+
sync: (options?: UseTextareaAutosizeSyncOptions) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Keep a textarea as tall as its content, optionally between `minRows` and `maxRows`.
|
|
30
|
+
*
|
|
31
|
+
* The caller has to invoke {@link UseTextareaAutosizeReturn.sync} from `input`: reading
|
|
32
|
+
* `element.value` inside a getter is not a reactive dependency, and waiting for a post-flush
|
|
33
|
+
* watch would size against the previous stroke. `minRows`, `maxRows` and {@link
|
|
34
|
+
* UseTextareaAutosizeOptions.content} go through a post-flush watch instead.
|
|
35
|
+
*
|
|
36
|
+
* Observers attach only while autosize is on. Native `resize` writes inline `width` and
|
|
37
|
+
* `height` as the pointer drags; an observer that stayed attached with autosize off would
|
|
38
|
+
* `clear` that height on every width change and fight the handle.
|
|
39
|
+
*
|
|
40
|
+
* Writing `style.height` is itself a size change, so the observer has to discriminate width
|
|
41
|
+
* or it would loop. That makes the seed load-bearing: it is read from the same content box
|
|
42
|
+
* `contentRect` reports, because a seed off by the border makes the first delivery look like
|
|
43
|
+
* a width change, and skipping that first delivery instead loses a real one — `sync` after
|
|
44
|
+
* `observe` can make a scrollbar appear, and the callback reporting the narrower width is
|
|
45
|
+
* the only notice of it.
|
|
46
|
+
*
|
|
47
|
+
* An inline height also hides height-only metric changes from the observer — a webfont that
|
|
48
|
+
* lands after first paint, or `.rp-textarea`'s `@media (width >= 40rem)` type/padding switch
|
|
49
|
+
* on a fixed-width control. Those go through `document.fonts` and `window` `resize`. Used
|
|
50
|
+
* metrics that do not move the box (`--rp-leading`, Firefox text-only zoom) never notify at
|
|
51
|
+
* all, which is why every field settles with `overflow-y: auto` — a stale height then
|
|
52
|
+
* scrolls rather than clips. A leftover clip on a field no cap clamped is the observer
|
|
53
|
+
* backstop for same-width box changes such as padding, which do notify.
|
|
54
|
+
*/
|
|
55
|
+
export declare const useTextareaAutosize: (options: UseTextareaAutosizeOptions) => UseTextareaAutosizeReturn;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { onScopeDispose, toValue, watch } from "vue";
|
|
2
|
+
//#region src/composables/use-textarea-autosize.ts
|
|
3
|
+
var px = (value) => {
|
|
4
|
+
const parsed = Number.parseFloat(value);
|
|
5
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A used line-height, not the keyword.
|
|
9
|
+
*
|
|
10
|
+
* `normal` is a computed value the platform leaves unresolved, and a row count in `normal`s is
|
|
11
|
+
* not a length. Font-size times 1.2 is the usual stand-in, and is what a missing font-size falls
|
|
12
|
+
* back through as well.
|
|
13
|
+
*/
|
|
14
|
+
var lineHeightOf = (style) => {
|
|
15
|
+
const raw = style.lineHeight;
|
|
16
|
+
const parsed = Number.parseFloat(raw);
|
|
17
|
+
if (raw !== "normal" && Number.isFinite(parsed)) return parsed;
|
|
18
|
+
const fontSize = Number.parseFloat(style.fontSize);
|
|
19
|
+
return Number.isFinite(fontSize) ? fontSize * 1.2 : 16;
|
|
20
|
+
};
|
|
21
|
+
var positive = (value) => value !== void 0 && value > 0 ? value : void 0;
|
|
22
|
+
var clear = (element) => {
|
|
23
|
+
element.style.height = "";
|
|
24
|
+
element.style.overflowY = "";
|
|
25
|
+
element.style.scrollPaddingBottom = "";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Content-box width, the same box ResizeObserver reports as `contentRect`.
|
|
29
|
+
*
|
|
30
|
+
* `clientWidth` is the padding box minus the scrollbar, so subtracting inline
|
|
31
|
+
* padding lands on the content box without mixing in the border the way
|
|
32
|
+
* `getBoundingClientRect` does.
|
|
33
|
+
*/
|
|
34
|
+
var contentWidthOf = (element) => {
|
|
35
|
+
const style = getComputedStyle(element);
|
|
36
|
+
return element.clientWidth - px(style.paddingLeft) - px(style.paddingRight);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Size a textarea to its content, clamped to a row range.
|
|
40
|
+
*
|
|
41
|
+
* Measured on the live element rather than a hidden clone of it: a clone is a second node in
|
|
42
|
+
* the document to keep in step, and the live element already has the font, the padding and
|
|
43
|
+
* the wrapping width. Height is set to `auto` for the read so a previous inline height cannot
|
|
44
|
+
* pin `scrollHeight` to itself, then written back as a pixel height.
|
|
45
|
+
*
|
|
46
|
+
* `scrollHeight` includes padding and not the border. The reset is `border-box`, so the height
|
|
47
|
+
* that is written has to put the border back or the last line is clipped by that much.
|
|
48
|
+
*
|
|
49
|
+
* The bottom-pin is input-only. Typing at the end has to bring `padding-bottom` into view;
|
|
50
|
+
* a width, font or row-range remeasure must not yank a scroll the user just made.
|
|
51
|
+
*
|
|
52
|
+
* @returns Whether `maxRows` clamped the height, which is the one case where a field left
|
|
53
|
+
* scrolling its own content is doing what it was asked to.
|
|
54
|
+
*/
|
|
55
|
+
var measure = (element, minRows, maxRows, fromInput) => {
|
|
56
|
+
const style = getComputedStyle(element);
|
|
57
|
+
const paddingY = px(style.paddingTop) + px(style.paddingBottom);
|
|
58
|
+
const borderY = px(style.borderTopWidth) + px(style.borderBottomWidth);
|
|
59
|
+
const lineHeight = lineHeightOf(style);
|
|
60
|
+
const borderBox = style.boxSizing === "border-box";
|
|
61
|
+
const extras = borderBox ? paddingY + borderY : 0;
|
|
62
|
+
const min = positive(minRows);
|
|
63
|
+
const max = positive(maxRows);
|
|
64
|
+
const paddingBottom = px(style.paddingBottom);
|
|
65
|
+
const scrollTop = element.scrollTop;
|
|
66
|
+
const atEnd = element.selectionStart === element.value.length && element.selectionEnd === element.value.length;
|
|
67
|
+
if (min !== void 0) element.rows = min;
|
|
68
|
+
element.style.height = "auto";
|
|
69
|
+
element.style.overflowY = "hidden";
|
|
70
|
+
let height = element.scrollHeight;
|
|
71
|
+
if (borderBox) height += borderY;
|
|
72
|
+
else height -= paddingY;
|
|
73
|
+
if (min !== void 0) height = Math.max(height, min * lineHeight + extras);
|
|
74
|
+
let capped = false;
|
|
75
|
+
if (max !== void 0) {
|
|
76
|
+
const cap = max * lineHeight + extras;
|
|
77
|
+
if (height > cap) {
|
|
78
|
+
height = cap;
|
|
79
|
+
capped = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
element.style.height = `${height}px`;
|
|
83
|
+
element.style.overflowY = "auto";
|
|
84
|
+
element.style.scrollPaddingBottom = `${paddingBottom}px`;
|
|
85
|
+
if (fromInput && capped && atEnd) element.scrollTop = element.scrollHeight;
|
|
86
|
+
else element.scrollTop = scrollTop;
|
|
87
|
+
return capped;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Keep a textarea as tall as its content, optionally between `minRows` and `maxRows`.
|
|
91
|
+
*
|
|
92
|
+
* The caller has to invoke {@link UseTextareaAutosizeReturn.sync} from `input`: reading
|
|
93
|
+
* `element.value` inside a getter is not a reactive dependency, and waiting for a post-flush
|
|
94
|
+
* watch would size against the previous stroke. `minRows`, `maxRows` and {@link
|
|
95
|
+
* UseTextareaAutosizeOptions.content} go through a post-flush watch instead.
|
|
96
|
+
*
|
|
97
|
+
* Observers attach only while autosize is on. Native `resize` writes inline `width` and
|
|
98
|
+
* `height` as the pointer drags; an observer that stayed attached with autosize off would
|
|
99
|
+
* `clear` that height on every width change and fight the handle.
|
|
100
|
+
*
|
|
101
|
+
* Writing `style.height` is itself a size change, so the observer has to discriminate width
|
|
102
|
+
* or it would loop. That makes the seed load-bearing: it is read from the same content box
|
|
103
|
+
* `contentRect` reports, because a seed off by the border makes the first delivery look like
|
|
104
|
+
* a width change, and skipping that first delivery instead loses a real one — `sync` after
|
|
105
|
+
* `observe` can make a scrollbar appear, and the callback reporting the narrower width is
|
|
106
|
+
* the only notice of it.
|
|
107
|
+
*
|
|
108
|
+
* An inline height also hides height-only metric changes from the observer — a webfont that
|
|
109
|
+
* lands after first paint, or `.rp-textarea`'s `@media (width >= 40rem)` type/padding switch
|
|
110
|
+
* on a fixed-width control. Those go through `document.fonts` and `window` `resize`. Used
|
|
111
|
+
* metrics that do not move the box (`--rp-leading`, Firefox text-only zoom) never notify at
|
|
112
|
+
* all, which is why every field settles with `overflow-y: auto` — a stale height then
|
|
113
|
+
* scrolls rather than clips. A leftover clip on a field no cap clamped is the observer
|
|
114
|
+
* backstop for same-width box changes such as padding, which do notify.
|
|
115
|
+
*/
|
|
116
|
+
var useTextareaAutosize = (options) => {
|
|
117
|
+
let observer;
|
|
118
|
+
let lastWidth = NaN;
|
|
119
|
+
/** So turning autosize off does not wipe a height the native resize handle wrote. */
|
|
120
|
+
let applied = false;
|
|
121
|
+
/** Whether `maxRows` clamped the last measure, which is when a clip is meant to be there. */
|
|
122
|
+
let capped = false;
|
|
123
|
+
const sync = (syncOptions) => {
|
|
124
|
+
const element = toValue(options.element) ?? null;
|
|
125
|
+
const enabled = Boolean(toValue(options.enabled));
|
|
126
|
+
if (!element) return;
|
|
127
|
+
if (!enabled) {
|
|
128
|
+
if (applied) {
|
|
129
|
+
clear(element);
|
|
130
|
+
applied = false;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
capped = measure(element, toValue(options.minRows), toValue(options.maxRows), Boolean(syncOptions?.fromInput));
|
|
135
|
+
applied = true;
|
|
136
|
+
};
|
|
137
|
+
const onMetrics = () => {
|
|
138
|
+
sync();
|
|
139
|
+
};
|
|
140
|
+
const metrics = (bind) => {
|
|
141
|
+
if (typeof document === "undefined") return;
|
|
142
|
+
window[bind]("resize", onMetrics);
|
|
143
|
+
document.fonts?.[bind]("loadingdone", onMetrics);
|
|
144
|
+
};
|
|
145
|
+
const detach = () => {
|
|
146
|
+
observer?.disconnect();
|
|
147
|
+
observer = void 0;
|
|
148
|
+
metrics("removeEventListener");
|
|
149
|
+
};
|
|
150
|
+
const observe = (element) => {
|
|
151
|
+
detach();
|
|
152
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
153
|
+
lastWidth = contentWidthOf(element);
|
|
154
|
+
observer = new ResizeObserver((entries) => {
|
|
155
|
+
const width = entries[0]?.contentRect.width;
|
|
156
|
+
if (width === void 0) return;
|
|
157
|
+
const widthChanged = width !== lastWidth;
|
|
158
|
+
lastWidth = width;
|
|
159
|
+
if (widthChanged) {
|
|
160
|
+
sync();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!capped && element.scrollHeight > element.clientHeight) sync();
|
|
164
|
+
});
|
|
165
|
+
observer.observe(element);
|
|
166
|
+
}
|
|
167
|
+
metrics("addEventListener");
|
|
168
|
+
};
|
|
169
|
+
watch([() => toValue(options.element) ?? null, () => Boolean(toValue(options.enabled))], ([element, enabled]) => {
|
|
170
|
+
detach();
|
|
171
|
+
if (!element) return;
|
|
172
|
+
if (enabled) observe(element);
|
|
173
|
+
sync();
|
|
174
|
+
}, {
|
|
175
|
+
flush: "post",
|
|
176
|
+
immediate: true
|
|
177
|
+
});
|
|
178
|
+
watch([
|
|
179
|
+
() => toValue(options.minRows),
|
|
180
|
+
() => toValue(options.maxRows),
|
|
181
|
+
() => toValue(options.content)
|
|
182
|
+
], () => {
|
|
183
|
+
sync();
|
|
184
|
+
}, { flush: "post" });
|
|
185
|
+
onScopeDispose(detach, true);
|
|
186
|
+
return { sync };
|
|
187
|
+
};
|
|
188
|
+
//#endregion
|
|
189
|
+
export { useTextareaAutosize };
|