uni-textarea-field 1.0.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.
@@ -0,0 +1,538 @@
1
+ import { _wcl } from './common-lib.js';
2
+ import { _wccss } from './common-css.js';
3
+ import {
4
+ colorPalette as _uniColorPalette
5
+ } from 'https://unpkg.com/uni-input-field/mjs/uni-css.js';
6
+
7
+ const defaults = {
8
+ subject: '',
9
+ message: '',
10
+ stat: '', // valid, invalid
11
+ appearance: 'filled', // filled, outlined
12
+ size: 'medium' // large, medium, small
13
+ };
14
+
15
+ const booleanAttrs = [];
16
+ const objectAttrs = [];
17
+ const custumEvents = {};
18
+
19
+ const template = document.createElement('template');
20
+ template.innerHTML = `
21
+ <style>
22
+ ${_wccss}
23
+ ${_uniColorPalette}
24
+
25
+ :host{position:relative;display:block;}
26
+
27
+ /* force hide counter */
28
+ :host([data-hide-counter]) {
29
+ .main .main__info__counter {
30
+ --counter-display: none;
31
+ }
32
+ }
33
+
34
+ :host([hidden]) {
35
+ display: none;
36
+ }
37
+
38
+ /* state */
39
+ :host([stat=valid]) {
40
+ .main {
41
+ --border-color: var(--border-color-valid);
42
+ --message-color: var(--message-color-valid);
43
+ }
44
+ }
45
+
46
+ :host([stat=invalid]) {
47
+ .main {
48
+ --text-color: var(--text-color-invalid);
49
+ --border-color: var(--border-color-invalid);
50
+ --message-color: var(--message-color-invalid);
51
+ }
52
+ }
53
+
54
+ /* appearance */
55
+ :host([appearance=outlined]) {
56
+ .main {
57
+ --border-color: var(--border-color-outline);
58
+ --background-color: transparent;
59
+ }
60
+ }
61
+
62
+ /* size */
63
+ :host([size=large]) {
64
+ .main {
65
+ --border-radius: var(--large-border-radius);
66
+ --padding: var(--large-padding);
67
+ }
68
+ }
69
+
70
+ :host([size=medium]) {
71
+ .main {
72
+ --border-radius: var(--medium-border-radius);
73
+ --padding: var(--medium-padding);
74
+ }
75
+ }
76
+
77
+ :host([size=small]) {
78
+ .main {
79
+ --border-radius: var(--small-border-radius);
80
+ --padding: var(--small-padding);
81
+ }
82
+ }
83
+
84
+ :host {
85
+ &:has([slot="textarea"][required]) {
86
+ .main__subject__span::after {
87
+ content: '*';
88
+ color: var(--ct_icon_moderate_strong);
89
+ margin-inline-start: 4px;
90
+ }
91
+ }
92
+
93
+ &:has([slot="textarea"][maxlength]) {
94
+ .main {
95
+ --counter-display: block;
96
+ }
97
+ }
98
+
99
+ &:has([slot="textarea"][readonly]) {
100
+ .main {
101
+ --background-color: transparent;
102
+ --border-color: var(--border-color-readonly);
103
+ }
104
+ }
105
+
106
+ &:has([slot="textarea"][disabled],[slot="textarea"][inert]) {
107
+ .main {
108
+ --text-color: var(--text-color-disabled);
109
+ }
110
+
111
+ slot[name="textarea"] {
112
+ interactivity: inert;
113
+ }
114
+ }
115
+
116
+ @container style(--interactivity: inert) {
117
+ .main {
118
+ --text-color: var(--text-color-disabled);
119
+ }
120
+
121
+ slot[name="textarea"] {
122
+ interactivity: inert;
123
+ }
124
+ }
125
+ }
126
+
127
+ .main {
128
+ --border-color-normal: var(--uni-textarea-field-border-color-normal, transparent);
129
+ --border-color-readonly: var(--uni-textarea-field-border-color-readonly, var(--ct_input-general_main_stroke_default));
130
+ --border-color-disabled: var(--uni-textarea-field-border-color-disabled, var(--ct_input-general_dim_container_default));
131
+ --border-color-valid: var(--uni-textarea-field-border-color-valid, var(--ct_text_success_general));
132
+ --border-color-invalid: var(--uni-textarea-field-border-color-invalid, var(--ct_text_danger_general));
133
+ --border-color-outline: var(--uni-textarea-field-border-color-outline, var(--ct_input-general_main_stroke_default));
134
+ --border-color: var(--border-color-normal);
135
+
136
+ --background-color-normal: var(--uni-textarea-field-background-color-normal, var(--ct_input-general_dim_container_default));
137
+ --background-color-readonly: var(--uni-textarea-field-background-color-readonly, var(--ct_input-general_dim_container_default));
138
+ --background-color-disabled: var(--uni-textarea-field-background-color-disabled, var(--ct_input-general_dim_container_default));
139
+ --background-color: var(--background-color-normal);
140
+
141
+ --placeholder-color-normal: var(--uni-textarea-field-placeholder-color-normal, var(--ct_text_main_subtlest));
142
+ --placeholder-color-readonly: var(--uni-textarea-field-placeholder-color-readonly, var(--ct_text_main_subtlest));
143
+ --placeholder-color-disabled: var(--uni-textarea-field-placeholder-color-disabled, var(--ct_text_main_subtlest));
144
+ --placeholder-color: var(--placeholder-color-normal);
145
+
146
+ --text-color-normal: var(--uni-textarea-field-text-color-normal, var(--ct_text_main_general));
147
+ --text-color-readonly: var(--uni-textarea-field-text-color-readonly, var(--ct_text_main_general));
148
+ --text-color-disabled: var(--uni-textarea-field-text-color-disabled, var(--ct_text_main_pale));
149
+ --text-color-invalid: var(--uni-textarea-field-text-color-invalid, var(--ct_text_danger_general));
150
+ --text-color: var(--text-color-normal);
151
+
152
+ --message-color-normal: var(--uni-textarea-field-message-color-normal, var(--ct_text_main_subtle));
153
+ --message-color-valid: var(--uni-textarea-field-message-color-valid, var(--ct_text_success_general));
154
+ --message-color-invalid: var(--uni-textarea-field-message-color-invalid, var(--ct_text_danger_general));
155
+ --message-color: var(--message-color-normal);
156
+
157
+ --subject-color: var(--uni-textarea-field-subject-color, var(--ct_text_main_subtle));
158
+ --counter-color: var(--uni-textarea-field-counter-color, var(--ct_text_main_subtle));
159
+ --caret-color: var(--uni-textarea-field-caret-color, var(--ct_input-caret_main_general));
160
+
161
+ --min-block-size: var(--uni-textarea-field-min-block-size, 100px);
162
+
163
+ /* size */
164
+ --large-border-radius: 24px;
165
+ --large-padding: 16px 12px 16px 16px;
166
+ --medium-border-radius: 24px;
167
+ --medium-padding: 8px 12px;
168
+ --small-border-radius: 24px;
169
+ --small-padding: 6.4px 4px 6.4px 12px;
170
+
171
+ --border-radius: var(--medium-border-radius);
172
+ --padding: var(--medium-padding);
173
+
174
+ --counter-display: none;
175
+
176
+ inline-size: 100%;
177
+
178
+ .main__subject {
179
+ padding-block-end: 4px;
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 4px;
183
+
184
+ &:has(.main__subject__span:empty) {
185
+ display: none;
186
+ }
187
+
188
+ .main__subject__span {
189
+ font-size: 12px;
190
+ color: var(--subject-color);
191
+ line-height: 1.667;
192
+ }
193
+
194
+ em {
195
+ inline-size: 15px;
196
+ block-size: 15px;
197
+ clip-path: path('M7.1,0C3.2,0,0,3.2,0,7.1s3.2,7.1,7.1,7.1,7.1-3.2,7.1-7.1S11.1,0,7.1,0ZM7.1,12.7c-3.1,0-5.6-2.5-5.6-5.5S4.1,1.6,7.1,1.6s5.5,2.5,5.5,5.6-2.5,5.5-5.5,5.5h0ZM7.9,6.3h-1.6v4.7h1.6v-4.7ZM7.9,3.6h-1.6v1.5h1.6v-1.5Z');
198
+ background-color: var(--subject-color);
199
+ display: block;
200
+ }
201
+ }
202
+
203
+ .main__info {
204
+ --justify-content: space-between;
205
+
206
+ padding: 4px 8px;
207
+ box-sizing: border-box;
208
+ display: flex;
209
+ gap: 16px;
210
+ justify-content: var(--justify-content);
211
+ align-items: center;
212
+
213
+ &:has(.main__info__message:empty) {
214
+ --justify-content: flex-end;
215
+ }
216
+
217
+ .main__info__message {
218
+ font-size: 11px;
219
+ color: var(--message-color);
220
+ line-height: 1.3;
221
+
222
+ &:empty {
223
+ display: none;
224
+ }
225
+ }
226
+
227
+ .main__info__counter {
228
+ flex-shrink: 0;
229
+ font-size: 12px;
230
+ color: var(--counter-color);
231
+ display: var(--counter-display);
232
+
233
+ &::after {
234
+ content: ' / ' attr(data-maxlength);
235
+ }
236
+ }
237
+ }
238
+
239
+ slot[name=textarea] {
240
+ inline-size: 100%;
241
+ display: block;
242
+ }
243
+
244
+ /* textarea */
245
+ ::slotted(textarea) {
246
+ outline: 0 none;
247
+ resize: none;
248
+ appearance: none;
249
+ box-shadow: none;
250
+
251
+ display: block;
252
+
253
+ font-size: 16px;
254
+ line-height: 1.4;
255
+ color: var(--text-color);
256
+ inline-size: 100% !important;
257
+ box-sizing: border-box;
258
+ text-overflow: ellipsis;
259
+ border: 1px solid var(--border-color);
260
+ background-color: var(--background-color);
261
+ caret-color: var(--caret-color);
262
+
263
+ min-block-size: var(--min-block-size);
264
+ field-sizing: content;
265
+ border-radius: var(--border-radius);
266
+ padding: var(--padding) !important;
267
+
268
+ &::placeholder {
269
+ color: var(--placeholder-color);
270
+ }
271
+ }
272
+ }
273
+ </style>
274
+
275
+ <div class="main" ontouchstart="" tabindex="0">
276
+ <p class="main__subject">
277
+ <em part="icon-subject"></em>
278
+ <span class="main__subject__span"></span>
279
+ </p>
280
+ <slot name="textarea"></slot>
281
+ <div class="main__info">
282
+ <p class="main__info__message"></p>
283
+ <p class="main__info__counter" data-maxlength="?">0</p>
284
+ </div>
285
+ </div>
286
+ `;
287
+
288
+ export class UniTextareaField extends HTMLElement {
289
+ #data;
290
+ #nodes;
291
+ #config;
292
+
293
+ constructor(config) {
294
+ super();
295
+
296
+ // template
297
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
298
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
299
+
300
+ // data
301
+ this.#data = {
302
+ controller: ''
303
+ };
304
+
305
+ // nodes
306
+ this.#nodes = {
307
+ styleSheet: this.shadowRoot.querySelector('style'),
308
+ textarea: this.querySelector('[slot=textarea]'),
309
+ subject: this.shadowRoot.querySelector('.main__subject__span'),
310
+ message: this.shadowRoot.querySelector('.main__info__message'),
311
+ counter: this.shadowRoot.querySelector('.main__info__counter'),
312
+ };
313
+
314
+ // config
315
+ this.#config = {
316
+ ...defaults,
317
+ ...config // new UniTextareaField(config)
318
+ };
319
+
320
+ // evts
321
+ this._onInput = this._onInput.bind(this);
322
+ this._onKeydown = this._onKeydown.bind(this);
323
+ }
324
+
325
+ async connectedCallback() {
326
+ const { config, error } = await _wcl.getWCConfig(this);
327
+ const { textarea } = this.#nodes;
328
+
329
+ if (error) {
330
+ console.warn(`${_wcl.classToTagName(this.constructor.name)}: ${error}`);
331
+ this.remove();
332
+ return;
333
+ } else {
334
+ this.#config = {
335
+ ...this.#config,
336
+ ...config
337
+ };
338
+ }
339
+
340
+ // upgradeProperty
341
+ Object.keys(defaults).forEach((key) => this.#upgradeProperty(key));
342
+
343
+ // evts
344
+ this.#data.controller = new AbortController();
345
+ const signal = this.#data.controller.signal;
346
+ textarea.addEventListener('input', this._onInput, { signal });
347
+ textarea.addEventListener('keydown', this._onKeydown, { signal });
348
+
349
+ // init
350
+ this._onInput();
351
+ }
352
+
353
+ disconnectedCallback() {
354
+ if (this.#data?.controller) {
355
+ this.#data.controller.abort();
356
+ }
357
+ }
358
+
359
+ #format(attrName, oldValue, newValue) {
360
+ const hasValue = newValue !== null;
361
+
362
+ if (!hasValue) {
363
+ if (booleanAttrs.includes(attrName)) {
364
+ this.#config[attrName] = false;
365
+ } else {
366
+ this.#config[attrName] = defaults[attrName];
367
+ }
368
+ } else {
369
+ switch (attrName) {
370
+ case 'subject':
371
+ case 'message': {
372
+ this.#config[attrName] = newValue;
373
+ break;
374
+ }
375
+
376
+ case 'stat': {
377
+ this.#config[attrName] = ['', 'valid', 'invalid'].includes(newValue) ? newValue : defaults.state;
378
+ break;
379
+ }
380
+
381
+ case 'appearance': {
382
+ this.#config[attrName] = ['filled', 'outlined'].includes(newValue) ? newValue : defaults.appearance;
383
+ break;
384
+ }
385
+
386
+ case 'size': {
387
+ this.#config[attrName] = ['large', 'medium', 'small'].includes(newValue) ? newValue : defaults.size;
388
+ break;
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ attributeChangedCallback(attrName, oldValue, newValue) {
395
+ if (!UniTextareaField.observedAttributes.includes(attrName)) {
396
+ return;
397
+ }
398
+
399
+ this.#format(attrName, oldValue, newValue);
400
+
401
+ switch (attrName) {
402
+ case 'subject': {
403
+ this.#nodes.subject.textContent = this.subject;
404
+ break;
405
+ }
406
+
407
+ case 'message': {
408
+ this.#nodes.message.textContent = this.message;
409
+ break;
410
+ }
411
+ }
412
+ }
413
+
414
+ static get observedAttributes() {
415
+ return Object.keys(defaults); // UniTextareaField.observedAttributes
416
+ }
417
+
418
+ static get supportedEvents() {
419
+ return Object.keys(custumEvents).map(
420
+ (key) => {
421
+ return custumEvents[key];
422
+ }
423
+ );
424
+ }
425
+
426
+ #upgradeProperty(prop) {
427
+ let value;
428
+
429
+ if (UniTextareaField.observedAttributes.includes(prop)) {
430
+ if (Object.prototype.hasOwnProperty.call(this, prop)) {
431
+ value = this[prop];
432
+ delete this[prop];
433
+ } else {
434
+ if (booleanAttrs.includes(prop)) {
435
+ value = (this.hasAttribute(prop) || this.#config[prop]) ? true : false;
436
+ } else if (objectAttrs.includes(prop)) {
437
+ value = this.hasAttribute(prop) ? this.getAttribute(prop) : JSON.stringify(this.#config[prop]);
438
+ } else {
439
+ value = this.hasAttribute(prop) ? this.getAttribute(prop) : this.#config[prop];
440
+ }
441
+ }
442
+
443
+ this[prop] = value;
444
+ }
445
+ }
446
+
447
+ set subject(value) {
448
+ if (value) {
449
+ this.setAttribute('subject', value);
450
+ } else {
451
+ this.removeAttribute('subject');
452
+ }
453
+ }
454
+
455
+ get subject() {
456
+ return this.#config.subject;
457
+ }
458
+
459
+ set message(value) {
460
+ if (value) {
461
+ this.setAttribute('message', value);
462
+ } else {
463
+ this.removeAttribute('message');
464
+ }
465
+ }
466
+
467
+ get message() {
468
+ return this.#config.message;
469
+ }
470
+
471
+ set stat(value) {
472
+ if (value) {
473
+ this.setAttribute('stat', value);
474
+ } else {
475
+ this.removeAttribute('stat');
476
+ }
477
+ }
478
+
479
+ get stat() {
480
+ return this.#config.stat;
481
+ }
482
+
483
+ set appearance(value) {
484
+ if (value) {
485
+ this.setAttribute('appearance', value);
486
+ } else {
487
+ this.removeAttribute('appearance');
488
+ }
489
+ }
490
+
491
+ get appearance() {
492
+ return this.#config.appearance;
493
+ }
494
+
495
+ set size(value) {
496
+ if (value) {
497
+ this.setAttribute('size', value);
498
+ } else {
499
+ this.removeAttribute('size');
500
+ }
501
+ }
502
+
503
+ get size() {
504
+ return this.#config.size;
505
+ }
506
+
507
+ _onInput(evt) {
508
+ const { textarea, counter } = this.#nodes;
509
+
510
+ counter.dataset.maxlength = textarea.maxLength;
511
+ counter.textContent = textarea.value.length;
512
+
513
+ if (evt && this.stat === 'invalid') {
514
+ this.stat = '';
515
+ }
516
+ }
517
+
518
+ _onKeydown(evt) {
519
+ const { key, isComposing } = evt;
520
+
521
+ if (key === 'Enter' && isComposing) {
522
+ event.preventDefault();
523
+ }
524
+ }
525
+
526
+ refresh() {
527
+ this.hidden = true;
528
+ this.offsetHeight;
529
+ this.hidden = false;
530
+ }
531
+ }
532
+
533
+ // define web component
534
+ const S = _wcl.supports();
535
+ const T = _wcl.classToTagName('UniTextareaField');
536
+ if (S.customElements && S.shadowDOM && S.template && !window.customElements.get(T)) {
537
+ window.customElements.define(_wcl.classToTagName('UniTextareaField'), UniTextareaField);
538
+ }