-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
textarea.ts
213 lines (186 loc) · 7.38 KB
/
textarea.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import { SetValueReasons } from "./baseControl";
import WUPTextControl from "./text";
import WUPTextareaInput from "./textarea.input";
WUPTextareaInput.$use();
const tagName = "wup-textarea";
declare global {
namespace WUP.Textarea {
interface EventMap extends WUP.Text.EventMap {}
interface ValidityMap extends WUP.Text.ValidityMap {}
interface Options<T = string, VM = ValidityMap>
extends Omit<WUP.Text.Options<T, VM>, "mask" | "maskholder" | "prefix" | "postfix"> {}
interface JSXProps<C = WUPTextareaControl>
extends Omit<WUP.Text.JSXProps<C>, "mask" | "maskholder" | "prefix" | "postfix"> {}
}
interface HTMLElementTagNameMap {
[tagName]: WUPTextareaControl; // add element to document.createElement
}
}
declare module "react" {
namespace JSX {
interface IntrinsicElements {
/** Form-control with multiline text-input
* @see {@link WUPTextareaControl} */
[tagName]: WUP.Base.ReactHTML<WUPTextareaControl> & WUP.Textarea.JSXProps; // add element to tsx/jsx intellisense (react)
}
}
}
// @ts-ignore - because Preact & React can't work together
declare module "preact/jsx-runtime" {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLAttributes<RefType> {}
interface IntrinsicElements {
/** Form-control with multiline text-input
* @see {@link WUPTextareaControl} */
[tagName]: HTMLAttributes<WUPTextareaControl> & WUP.Textarea.JSXProps; // add element to tsx/jsx intellisense (preact)
}
}
}
/** Form-control with multiline text-input
* @see demo {@link https://yegorich555.github.io/web-ui-pack/control/textarea}
* @example
const el = document.createElement("wup-textarea");
el.$options.name = "textarea";
el.$options.validations = {
required: true,
max: 250
};
const form = document.body.appendChild(document.createElement("wup-form"));
form.appendChild(el);
// or HTML
<wup-form>
<wup-textarea w-name="textarea" w-validations="myValidations"/>
</wup-form>;
* @tutorial innerHTML @example
* <label>
* <span> // extra span requires to use with icons via label:before, label:after without adjustments
* <span contenteditable="true" />
* <strong>{$options.label}</strong>
* </span>
* <button clear/>
* </label>
* @tutorial Troubleshooting
* * known issue: NVDA doesn't read multiline text https://github.com/nvaccess/nvda/issues/13369
* to resolve it set WUPTextareaControl.$defaults.selectOnFocus = true */
export default class WUPTextareaControl<
ValueType = string,
TOptions extends WUP.Textarea.Options = WUP.Textarea.Options,
EventMap extends WUP.Textarea.EventMap = WUP.Textarea.EventMap
> extends WUPTextControl<ValueType, TOptions, EventMap> {
/** Returns this.constructor // watch-fix: https://github.com/Microsoft/TypeScript/issues/3841#issuecomment-337560146 */
// #ctr = this.constructor as typeof WUPTextareaControl;
static get $style(): string {
return `${super.$style}
:host strong { top: 1.6em; }
:host [contenteditable=true] {
min-height: 4em;
max-height: 4em;
}`;
}
/** Default options - applied to every element. Change it to configure default behavior */
static $defaults: WUP.Textarea.Options = {
...WUPTextControl.$defaults,
validationRules: {
...WUPTextControl.$defaults.validationRules,
// WARN: validations min/max must depends only on visible chars
min: (v, setV, c, r) => WUPTextControl.$defaults.validationRules.min!.call!(c, v?.replace(/\n/g, ""), setV, c, r),
max: (v, setV, c, r) => WUPTextControl.$defaults.validationRules.max!.call!(c, v?.replace(/\n/g, ""), setV, c, r),
},
};
$refInput = document.createElement("wup-areainput") as HTMLInputElement;
protected override renderControl(): void {
super.renderControl();
const { id } = this.$refInput;
this.$refInput.removeAttribute("id");
this.$refInput.setAttribute("aria-labelledby", id);
this.$refTitle.id = id;
}
protected override gotChanges(propsChanged: Array<keyof WUP.Textarea.Options> | null): void {
super.gotChanges(propsChanged);
const o = this._opts as WUP.Text.Options;
delete o.mask;
delete o.maskholder;
delete o.prefix;
delete o.postfix;
}
protected override renderPrefix(): void {
// not supported
}
protected override renderPostfix(): void {
// not supported
}
// protected override gotBeforeInput(e: WUP.Text.GotInputEvent): void {
// super.gotBeforeInput(e);
// let data: string | null = null;
// switch (e.inputType) {
// case "insertLineBreak":
// case "insertParagraph":
// data = "\n";
// this.insertText(data);
// break;
// default:
// console.warn(e.inputType, e.data);
// break;
// }
// if (data) {
// e.preventDefault();
// this.$refInput.dispatchEvent(new InputEvent("input", { inputType: e.inputType, data }));
// }
// }
// protected override gotInput(e: WUP.Text.GotInputEvent): void {
// super.gotInput(e);
// console.warn({ v: this.$refInput.value });
// }
// protected insertText(text: string): void {
// let range = window.getSelection()!.getRangeAt(0);
// range.deleteContents(); // delete all prev selected part
// const fr = document.createDocumentFragment();
// const content = document.createTextNode(text);
// fr.appendChild(content);
// // issue: when insertText: and endswith "\nAbc " need to remove last empty space
// // issue: when delete an empty space: chrome adds <br>
// if (text === "\n" && this.$refInput.selectionEnd === this.$refInput.textContent!.length) {
// fr.appendChild(document.createTextNode(" ")); // WARN: otherwise Chrome doesn't go to next line
// }
// range.insertNode(fr);
// // create a new range
// range = document.createRange();
// range.setStartAfter(content);
// range.collapse(true);
// // make the cursor there
// const sel = window.getSelection()!;
// sel.removeAllRanges();
// sel.addRange(range);
// // autoscroll to cursor
// const tempAnchorEl = document.createElement("br");
// range.insertNode(tempAnchorEl);
// tempAnchorEl.scrollIntoView({ block: "nearest" });
// tempAnchorEl.remove(); // remove after scrolling is done
// }
protected override gotKeyDown(e: KeyboardEvent & { submitPrevented?: boolean }): void {
super.gotKeyDown(e);
if (e.key === "Enter") {
e.submitPrevented = true;
}
}
protected override gotBeforeInput(e: WUP.Text.GotInputEvent): void {
if (e.inputType.startsWith("format")) {
// todo need to process this for customHistory in the future
e.preventDefault(); // prevent Bold,Italic etc. styles until textrich is developed
} else {
super.gotBeforeInput(e);
}
// delete (this.$refInput as unknown as WUPTextareaInput)._cached;
}
protected override gotFocusLost(): void {
super.gotFocusLost();
this.setInputValue(this.$value as string, SetValueReasons.userInput); // update because newLine is replaced
}
}
// prettify defaults before create
let rr: Array<keyof WUP.Text.Options> | undefined = ["mask", "maskholder", "prefix", "postfix"];
rr.forEach((k) => delete WUPTextareaControl.$defaults[k as keyof WUP.Textarea.Options]);
rr = undefined;
customElements.define(tagName, WUPTextareaControl);
// NiceToHave display of btnClear affects on internal width - is it ok ???