@streamscloud/kit 0.89.0 → 0.89.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/core/utils/html-helper.d.ts +10 -0
- package/dist/core/utils/html-helper.js +37 -0
- package/dist/core/utils/string-helper.d.ts +10 -0
- package/dist/core/utils/string-helper.js +14 -0
- package/dist/ui/input/cmp.input.svelte +13 -1
- package/dist/ui/input/cmp.input.svelte.d.ts +3 -0
- package/dist/ui/textarea/cmp.textarea.svelte +11 -15
- package/dist/ui/textarea/cmp.textarea.svelte.d.ts +4 -0
- package/package.json +1 -1
|
@@ -11,6 +11,16 @@ export declare class HtmlHelper {
|
|
|
11
11
|
* swallow the space in "hello world" as it is typed.
|
|
12
12
|
*/
|
|
13
13
|
static trimElementValue(element: HTMLInputElement | HTMLTextAreaElement): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites a field's current value in place through `clean`, keeping the caret after the same
|
|
16
|
+
* character, and returns whether it changed. Dispatches nothing — the caller emits.
|
|
17
|
+
*/
|
|
18
|
+
static cleanElementValue(element: HTMLInputElement | HTMLTextAreaElement, clean: (text: string) => string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* `paste` handler for a text field: when `clean` changes the pasted text, inserts the cleaned text instead as one
|
|
21
|
+
* native edit, so Cmd+Z undoes the paste like any other.
|
|
22
|
+
*/
|
|
23
|
+
static pasteCleanedText(event: ClipboardEvent, clean: (text: string) => string): void;
|
|
14
24
|
static pasteIntoInput(value: string, element: HTMLInputElement | HTMLTextAreaElement): void;
|
|
15
25
|
/**
|
|
16
26
|
* Sanitizes SVG markup and serializes it as XML, valid both inline in HTML and as a standalone `image/svg+xml` file.
|
|
@@ -93,6 +93,43 @@ export class HtmlHelper {
|
|
|
93
93
|
element.value = trimmed;
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Rewrites a field's current value in place through `clean`, keeping the caret after the same
|
|
98
|
+
* character, and returns whether it changed. Dispatches nothing — the caller emits.
|
|
99
|
+
*/
|
|
100
|
+
static cleanElementValue(element, clean) {
|
|
101
|
+
const cleaned = clean(element.value);
|
|
102
|
+
if (cleaned === element.value) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const caret = element.selectionEnd === null ? null : clean(element.value.slice(0, element.selectionEnd)).length;
|
|
106
|
+
element.value = cleaned;
|
|
107
|
+
if (caret !== null && document.activeElement === element) {
|
|
108
|
+
element.setSelectionRange(caret, caret);
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `paste` handler for a text field: when `clean` changes the pasted text, inserts the cleaned text instead as one
|
|
114
|
+
* native edit, so Cmd+Z undoes the paste like any other.
|
|
115
|
+
*/
|
|
116
|
+
static pasteCleanedText(event, clean) {
|
|
117
|
+
const text = event.clipboardData?.getData('text/plain');
|
|
118
|
+
if (!text) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const cleaned = clean(text);
|
|
122
|
+
if (cleaned === text) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const field = event.currentTarget;
|
|
127
|
+
// Without it the browser merges the insert into the typing before it, and Cmd+Z undoes both at once.
|
|
128
|
+
if (field.selectionStart !== null && field.selectionEnd !== null) {
|
|
129
|
+
field.setSelectionRange(field.selectionStart, field.selectionEnd, field.selectionDirection ?? undefined);
|
|
130
|
+
}
|
|
131
|
+
document.execCommand('insertText', false, cleaned);
|
|
132
|
+
}
|
|
96
133
|
static pasteIntoInput(value, element) {
|
|
97
134
|
if (element.selectionStart || element.selectionStart === 0) {
|
|
98
135
|
const startPos = element.selectionStart;
|
|
@@ -7,6 +7,16 @@ export declare class StringHelper {
|
|
|
7
7
|
static toKebabCase(str: string): string;
|
|
8
8
|
static toSnakeCase(str: string): string;
|
|
9
9
|
static truncate(str: string, length: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Text for a single-line field: every line break (CR, LF, CRLF, VT, FF, NEL, U+2028, U+2029) and tab
|
|
12
|
+
* becomes a space; every other control character (C0, DEL, C1) is dropped.
|
|
13
|
+
*/
|
|
14
|
+
static toSingleLineText(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Text for a multi-line field: every line break (CRLF, lone CR, VT, FF, NEL, U+2028, U+2029) becomes `\n`;
|
|
17
|
+
* `\n` and tab stay; every other control character (C0, DEL, C1) is dropped.
|
|
18
|
+
*/
|
|
19
|
+
static toMultilineText(text: string): string;
|
|
10
20
|
static capitalizeFirstLetter(word: string): string;
|
|
11
21
|
static toFileSizeString(sizeBytes: number): string;
|
|
12
22
|
}
|
|
@@ -50,6 +50,20 @@ export class StringHelper {
|
|
|
50
50
|
str = str.slice(0, length) + '...';
|
|
51
51
|
return str;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Text for a single-line field: every line break (CR, LF, CRLF, VT, FF, NEL, U+2028, U+2029) and tab
|
|
55
|
+
* becomes a space; every other control character (C0, DEL, C1) is dropped.
|
|
56
|
+
*/
|
|
57
|
+
static toSingleLineText(text) {
|
|
58
|
+
return text.replace(/\r\n|[\n\r\t\v\f\u0085\p{Zl}\p{Zp}]/gu, ' ').replace(/\p{Cc}/gu, '');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Text for a multi-line field: every line break (CRLF, lone CR, VT, FF, NEL, U+2028, U+2029) becomes `\n`;
|
|
62
|
+
* `\n` and tab stay; every other control character (C0, DEL, C1) is dropped.
|
|
63
|
+
*/
|
|
64
|
+
static toMultilineText(text) {
|
|
65
|
+
return text.replace(/\r\n?|[\v\f\u0085\p{Zl}\p{Zp}]/gu, '\n').replace(/[^\P{Cc}\n\t]/gu, '');
|
|
66
|
+
}
|
|
53
67
|
static capitalizeFirstLetter(word) {
|
|
54
68
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
55
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">import { HtmlHelper } from '../../core/utils';
|
|
1
|
+
<script lang="ts">import { HtmlHelper, StringHelper } from '../../core/utils';
|
|
2
2
|
import { InputClearButton } from '../_internal/input-clear-button';
|
|
3
3
|
import { InputEmojiPicker } from '../_internal/input-emoji-picker';
|
|
4
4
|
import { InputPasswordToggle } from '../_internal/input-password-toggle';
|
|
@@ -25,6 +25,9 @@ const handleInput = () => {
|
|
|
25
25
|
if (!inputEl) {
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
+
if (type !== 'password') {
|
|
29
|
+
HtmlHelper.cleanElementValue(inputEl, StringHelper.toSingleLineText);
|
|
30
|
+
}
|
|
28
31
|
on?.input?.(inputEl.value);
|
|
29
32
|
};
|
|
30
33
|
const applyTrim = () => {
|
|
@@ -71,6 +74,11 @@ const handleBlur = () => {
|
|
|
71
74
|
}
|
|
72
75
|
on?.blur?.();
|
|
73
76
|
};
|
|
77
|
+
const handlePaste = (event) => {
|
|
78
|
+
if (type !== 'password') {
|
|
79
|
+
HtmlHelper.pasteCleanedText(event, StringHelper.toSingleLineText);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
74
82
|
const handleKeydown = (event) => {
|
|
75
83
|
// Input only — the native `change` Enter fires next announces the commit; emitting it here doubles it.
|
|
76
84
|
if (event.key === 'Enter' && !event.isComposing) {
|
|
@@ -133,6 +141,7 @@ const focusInput = () => inputEl?.focus();
|
|
|
133
141
|
aria-required={ariaRequired ? 'true' : undefined}
|
|
134
142
|
oninput={handleInput}
|
|
135
143
|
onchange={handleChange}
|
|
144
|
+
onpaste={handlePaste}
|
|
136
145
|
onblur={handleBlur}
|
|
137
146
|
onfocus={() => on?.focus?.()}
|
|
138
147
|
onkeydown={handleKeydown} />
|
|
@@ -173,6 +182,9 @@ Leading / trailing whitespace is stripped at every commit point — `change`, bl
|
|
|
173
182
|
value the consumer receives always matches what the field shows. Typing is untouched. Pass
|
|
174
183
|
`trim={false}` to opt out; `type="password"` never trims.
|
|
175
184
|
|
|
185
|
+
Entered text is cleaned as it arrives — typed, pasted, dropped or autofilled: line breaks and tabs
|
|
186
|
+
become spaces, other control characters are dropped. `type="password"` is never cleaned.
|
|
187
|
+
|
|
176
188
|
Two snippet slots widen the chrome: `prefix` for a full-height segmented leading addon (see
|
|
177
189
|
`WebsiteInput`), and `suffix` for a free-form trailing addon rendered before any trailing icons
|
|
178
190
|
or the clear button (see `HandleInput`'s availability pill).
|
|
@@ -10,6 +10,9 @@ import type { InputProps } from './types';
|
|
|
10
10
|
* value the consumer receives always matches what the field shows. Typing is untouched. Pass
|
|
11
11
|
* `trim={false}` to opt out; `type="password"` never trims.
|
|
12
12
|
*
|
|
13
|
+
* Entered text is cleaned as it arrives — typed, pasted, dropped or autofilled: line breaks and tabs
|
|
14
|
+
* become spaces, other control characters are dropped. `type="password"` is never cleaned.
|
|
15
|
+
*
|
|
13
16
|
* Two snippet slots widen the chrome: `prefix` for a full-height segmented leading addon (see
|
|
14
17
|
* `WebsiteInput`), and `suffix` for a free-form trailing addon rendered before any trailing icons
|
|
15
18
|
* or the clear button (see `HandleInput`'s availability pill).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<script lang="ts">import { HtmlHelper } from '../../core/utils';
|
|
1
|
+
<script lang="ts">import { HtmlHelper, StringHelper } from '../../core/utils';
|
|
2
2
|
import { InputEmojiPicker } from '../_internal/input-emoji-picker';
|
|
3
3
|
const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, singleLine = false, submitOnEnter = false, trim = true, placeholder = '', name = '', id = null, autofocus = false, maxLength = null, rows = 3, autoresize = true, maxRows = 12, autocomplete, spellcheck, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, on } = $props();
|
|
4
4
|
let textareaEl = $state.raw(undefined);
|
|
5
|
-
const
|
|
6
|
-
const valueParsed = $derived(singleLine && value ?
|
|
5
|
+
const cleanText = $derived(singleLine ? StringHelper.toSingleLineText : StringHelper.toMultilineText);
|
|
6
|
+
const valueParsed = $derived(singleLine && value ? cleanText(value) : (value ?? ''));
|
|
7
7
|
const effectiveRows = $derived(singleLine ? 1 : rows);
|
|
8
8
|
const trimOnCommit = $derived(trim && !disabled && !readonly && !inert);
|
|
9
9
|
/** Imperative API exposed via `bind:this` on `<Textarea>`. */
|
|
@@ -38,6 +38,7 @@ const handleInput = () => {
|
|
|
38
38
|
if (!textareaEl) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
HtmlHelper.cleanElementValue(textareaEl, cleanText);
|
|
41
42
|
resize();
|
|
42
43
|
on?.input?.(textareaEl.value);
|
|
43
44
|
};
|
|
@@ -62,21 +63,11 @@ const handleChange = () => {
|
|
|
62
63
|
on?.change?.(textareaEl.value);
|
|
63
64
|
};
|
|
64
65
|
const handleBeforeInput = (e) => {
|
|
65
|
-
if (
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph') {
|
|
69
|
-
e.preventDefault();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (e.data && /[\r\n]/.test(e.data)) {
|
|
66
|
+
if (singleLine && (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph')) {
|
|
73
67
|
e.preventDefault();
|
|
74
|
-
const start = textareaEl.selectionStart;
|
|
75
|
-
const end = textareaEl.selectionEnd;
|
|
76
|
-
textareaEl.setRangeText(sanitizeLineBreaks(e.data), start, end, 'end');
|
|
77
|
-
textareaEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
78
68
|
}
|
|
79
69
|
};
|
|
70
|
+
const handlePaste = (e) => HtmlHelper.pasteCleanedText(e, cleanText);
|
|
80
71
|
const handleKeydown = (e) => {
|
|
81
72
|
if (submitOnEnter && e.key === 'Enter' && !e.isComposing && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
|
|
82
73
|
e.preventDefault();
|
|
@@ -134,6 +125,7 @@ const focusTextarea = () => textareaEl?.focus();
|
|
|
134
125
|
oninput={handleInput}
|
|
135
126
|
onchange={handleChange}
|
|
136
127
|
onbeforeinput={handleBeforeInput}
|
|
128
|
+
onpaste={handlePaste}
|
|
137
129
|
onkeydown={handleKeydown}
|
|
138
130
|
onblur={handleBlur}
|
|
139
131
|
onfocus={() => on?.focus?.()}></textarea>
|
|
@@ -157,6 +149,10 @@ point — `change`, blur, and before `on.submit` — so the value the consumer r
|
|
|
157
149
|
what the field shows. Blank lines inside the text are left alone. Typing is untouched. Pass
|
|
158
150
|
`trim={false}` to opt out.
|
|
159
151
|
|
|
152
|
+
Entered text is cleaned as it arrives — typed, pasted or dropped: every line break (including Word's
|
|
153
|
+
soft break) becomes `\n`, other control characters except tab are dropped. In `singleLine` mode line breaks and tabs
|
|
154
|
+
become spaces instead.
|
|
155
|
+
|
|
160
156
|
### CSS Custom Properties
|
|
161
157
|
| Property | Description | Default |
|
|
162
158
|
|---|---|---|
|
|
@@ -67,6 +67,10 @@ type Props = {
|
|
|
67
67
|
* what the field shows. Blank lines inside the text are left alone. Typing is untouched. Pass
|
|
68
68
|
* `trim={false}` to opt out.
|
|
69
69
|
*
|
|
70
|
+
* Entered text is cleaned as it arrives — typed, pasted or dropped: every line break (including Word's
|
|
71
|
+
* soft break) becomes `\n`, other control characters except tab are dropped. In `singleLine` mode line breaks and tabs
|
|
72
|
+
* become spaces instead.
|
|
73
|
+
*
|
|
70
74
|
* ### CSS Custom Properties
|
|
71
75
|
* | Property | Description | Default |
|
|
72
76
|
* |---|---|---|
|