secure-ui-components 0.2.3 → 0.2.4
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/secure-card/secure-card.js +1 -766
- package/dist/components/secure-datetime/secure-datetime.js +1 -570
- package/dist/components/secure-file-upload/secure-file-upload.js +1 -868
- package/dist/components/secure-form/secure-form.js +1 -797
- package/dist/components/secure-input/secure-input.js +1 -867
- package/dist/components/secure-password-confirm/secure-password-confirm.js +1 -329
- package/dist/components/secure-select/secure-select.js +1 -589
- package/dist/components/secure-submit-button/secure-submit-button.js +1 -378
- package/dist/components/secure-table/secure-table.js +33 -528
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.js +1 -201
- package/dist/components/secure-textarea/secure-textarea.js +1 -491
- package/dist/core/base-component.js +1 -500
- package/dist/core/security-config.js +1 -242
- package/dist/core/types.js +0 -2
- package/dist/index.js +1 -17
- package/dist/package.json +4 -2
- package/package.json +4 -2
|
@@ -27,494 +27,4 @@
|
|
|
27
27
|
*
|
|
28
28
|
* @module secure-textarea
|
|
29
29
|
* @license MIT
|
|
30
|
-
*/
|
|
31
|
-
import { SecureBaseComponent } from '../../core/base-component.js';
|
|
32
|
-
/**
|
|
33
|
-
* Secure Textarea Web Component
|
|
34
|
-
*
|
|
35
|
-
* Provides a security-hardened textarea field with progressive enhancement.
|
|
36
|
-
* The component works as a standard form textarea without JavaScript and
|
|
37
|
-
* enhances with security features when JavaScript is available.
|
|
38
|
-
*
|
|
39
|
-
* @extends SecureBaseComponent
|
|
40
|
-
*/
|
|
41
|
-
export class SecureTextarea extends SecureBaseComponent {
|
|
42
|
-
/**
|
|
43
|
-
* Textarea element reference
|
|
44
|
-
* @private
|
|
45
|
-
*/
|
|
46
|
-
#textareaElement = null;
|
|
47
|
-
/**
|
|
48
|
-
* Label element reference
|
|
49
|
-
* @private
|
|
50
|
-
*/
|
|
51
|
-
#labelElement = null;
|
|
52
|
-
/**
|
|
53
|
-
* Error container element reference
|
|
54
|
-
* @private
|
|
55
|
-
*/
|
|
56
|
-
#errorContainer = null;
|
|
57
|
-
/**
|
|
58
|
-
* Threat feedback container — separate from validation errors so
|
|
59
|
-
* #clearErrors() never clobbers an active threat message.
|
|
60
|
-
* @private
|
|
61
|
-
*/
|
|
62
|
-
#threatContainer = null;
|
|
63
|
-
/**
|
|
64
|
-
* Character count display element
|
|
65
|
-
* @private
|
|
66
|
-
*/
|
|
67
|
-
#charCountElement = null;
|
|
68
|
-
/**
|
|
69
|
-
* Unique ID for this textarea instance
|
|
70
|
-
* @private
|
|
71
|
-
*/
|
|
72
|
-
#instanceId = `secure-textarea-${Math.random().toString(36).substring(2, 11)}`;
|
|
73
|
-
/**
|
|
74
|
-
* Observed attributes for this component
|
|
75
|
-
*
|
|
76
|
-
* @static
|
|
77
|
-
*/
|
|
78
|
-
static get observedAttributes() {
|
|
79
|
-
return [
|
|
80
|
-
...super.observedAttributes,
|
|
81
|
-
'name',
|
|
82
|
-
'label',
|
|
83
|
-
'placeholder',
|
|
84
|
-
'required',
|
|
85
|
-
'minlength',
|
|
86
|
-
'maxlength',
|
|
87
|
-
'rows',
|
|
88
|
-
'cols',
|
|
89
|
-
'wrap',
|
|
90
|
-
'value'
|
|
91
|
-
];
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Constructor
|
|
95
|
-
*/
|
|
96
|
-
constructor() {
|
|
97
|
-
super();
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Render the textarea component
|
|
101
|
-
*
|
|
102
|
-
* Security Note: We use a native <textarea> element wrapped in our web component
|
|
103
|
-
* to ensure progressive enhancement. The native textarea works without JavaScript,
|
|
104
|
-
* and we enhance it with security features when JS is available.
|
|
105
|
-
*
|
|
106
|
-
* @protected
|
|
107
|
-
*/
|
|
108
|
-
render() {
|
|
109
|
-
const fragment = document.createDocumentFragment();
|
|
110
|
-
const container = document.createElement('div');
|
|
111
|
-
container.className = 'textarea-container';
|
|
112
|
-
container.setAttribute('part', 'container');
|
|
113
|
-
// Create label
|
|
114
|
-
const label = this.getAttribute('label');
|
|
115
|
-
if (label) {
|
|
116
|
-
this.#labelElement = document.createElement('label');
|
|
117
|
-
this.#labelElement.htmlFor = this.#instanceId;
|
|
118
|
-
this.#labelElement.textContent = this.sanitizeValue(label);
|
|
119
|
-
this.#labelElement.setAttribute('part', 'label');
|
|
120
|
-
container.appendChild(this.#labelElement);
|
|
121
|
-
}
|
|
122
|
-
// Create textarea wrapper for progressive enhancement
|
|
123
|
-
const textareaWrapper = document.createElement('div');
|
|
124
|
-
textareaWrapper.className = 'textarea-wrapper';
|
|
125
|
-
textareaWrapper.setAttribute('part', 'wrapper');
|
|
126
|
-
// Create the actual textarea element
|
|
127
|
-
this.#textareaElement = document.createElement('textarea');
|
|
128
|
-
this.#textareaElement.id = this.#instanceId;
|
|
129
|
-
this.#textareaElement.className = 'textarea-field';
|
|
130
|
-
this.#textareaElement.setAttribute('part', 'textarea');
|
|
131
|
-
// Apply attributes from web component to native textarea
|
|
132
|
-
this.#applyTextareaAttributes();
|
|
133
|
-
// Set up event listeners
|
|
134
|
-
this.#attachEventListeners();
|
|
135
|
-
textareaWrapper.appendChild(this.#textareaElement);
|
|
136
|
-
container.appendChild(textareaWrapper);
|
|
137
|
-
// Create character count display
|
|
138
|
-
this.#charCountElement = document.createElement('span');
|
|
139
|
-
this.#charCountElement.className = 'char-count';
|
|
140
|
-
this.#updateCharCount();
|
|
141
|
-
container.appendChild(this.#charCountElement);
|
|
142
|
-
// Create error container
|
|
143
|
-
// role="alert" already implies aria-live="assertive" — do not override with polite
|
|
144
|
-
this.#errorContainer = document.createElement('div');
|
|
145
|
-
this.#errorContainer.className = 'error-container hidden';
|
|
146
|
-
this.#errorContainer.setAttribute('role', 'alert');
|
|
147
|
-
this.#errorContainer.setAttribute('part', 'error');
|
|
148
|
-
this.#errorContainer.id = `${this.#instanceId}-error`;
|
|
149
|
-
container.appendChild(this.#errorContainer);
|
|
150
|
-
// Threat feedback container — kept separate from #errorContainer so
|
|
151
|
-
// #clearErrors() (called on every input event) never clobbers a threat message.
|
|
152
|
-
this.#threatContainer = document.createElement('div');
|
|
153
|
-
this.#threatContainer.className = 'threat-container hidden';
|
|
154
|
-
this.#threatContainer.setAttribute('role', 'alert');
|
|
155
|
-
this.#threatContainer.setAttribute('part', 'threat');
|
|
156
|
-
this.#threatContainer.id = `${this.#instanceId}-threat`;
|
|
157
|
-
container.appendChild(this.#threatContainer);
|
|
158
|
-
// Add component styles (CSP-compliant via adoptedStyleSheets)
|
|
159
|
-
this.addComponentStyles(this.#getComponentStyles());
|
|
160
|
-
fragment.appendChild(container);
|
|
161
|
-
return fragment;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Apply attributes from the web component to the native textarea
|
|
165
|
-
*
|
|
166
|
-
* Security Note: This is where we enforce tier-specific security controls
|
|
167
|
-
* like autocomplete, caching, and validation rules.
|
|
168
|
-
*
|
|
169
|
-
* @private
|
|
170
|
-
*/
|
|
171
|
-
#applyTextareaAttributes() {
|
|
172
|
-
const config = this.config;
|
|
173
|
-
// Name attribute (required for form submission)
|
|
174
|
-
const name = this.getAttribute('name');
|
|
175
|
-
if (name) {
|
|
176
|
-
this.#textareaElement.name = this.sanitizeValue(name);
|
|
177
|
-
}
|
|
178
|
-
// Accessible name fallback when no visible label is provided
|
|
179
|
-
if (!this.getAttribute('label') && name) {
|
|
180
|
-
this.#textareaElement.setAttribute('aria-label', this.sanitizeValue(name));
|
|
181
|
-
}
|
|
182
|
-
// Link textarea to its error container for screen readers
|
|
183
|
-
this.#textareaElement.setAttribute('aria-describedby', `${this.#instanceId}-error`);
|
|
184
|
-
// Placeholder
|
|
185
|
-
const placeholder = this.getAttribute('placeholder');
|
|
186
|
-
if (placeholder) {
|
|
187
|
-
this.#textareaElement.placeholder = this.sanitizeValue(placeholder);
|
|
188
|
-
}
|
|
189
|
-
// Required attribute
|
|
190
|
-
if (this.hasAttribute('required') || config.validation.required) {
|
|
191
|
-
this.#textareaElement.required = true;
|
|
192
|
-
this.#textareaElement.setAttribute('aria-required', 'true');
|
|
193
|
-
}
|
|
194
|
-
// Length constraints
|
|
195
|
-
const minLength = this.getAttribute('minlength');
|
|
196
|
-
if (minLength) {
|
|
197
|
-
this.#textareaElement.minLength = parseInt(minLength, 10);
|
|
198
|
-
}
|
|
199
|
-
const maxLength = this.getAttribute('maxlength') || config.validation.maxLength;
|
|
200
|
-
if (maxLength) {
|
|
201
|
-
this.#textareaElement.maxLength = parseInt(String(maxLength), 10);
|
|
202
|
-
}
|
|
203
|
-
// Rows and columns
|
|
204
|
-
const rows = this.getAttribute('rows') || 3;
|
|
205
|
-
this.#textareaElement.rows = parseInt(String(rows), 10);
|
|
206
|
-
const cols = this.getAttribute('cols');
|
|
207
|
-
if (cols) {
|
|
208
|
-
this.#textareaElement.cols = parseInt(cols, 10);
|
|
209
|
-
}
|
|
210
|
-
// Wrap attribute
|
|
211
|
-
const wrap = this.getAttribute('wrap') || 'soft';
|
|
212
|
-
this.#textareaElement.wrap = wrap;
|
|
213
|
-
// CRITICAL SECURITY: Autocomplete control based on tier
|
|
214
|
-
// For SENSITIVE and CRITICAL tiers, we disable autocomplete to prevent
|
|
215
|
-
// browser storage of sensitive data
|
|
216
|
-
if (config.storage.allowAutocomplete) {
|
|
217
|
-
const autocomplete = this.getAttribute('autocomplete') || 'on';
|
|
218
|
-
this.#textareaElement.autocomplete = autocomplete;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
this.#textareaElement.autocomplete = 'off';
|
|
222
|
-
}
|
|
223
|
-
// Disabled state
|
|
224
|
-
if (this.hasAttribute('disabled')) {
|
|
225
|
-
this.#textareaElement.disabled = true;
|
|
226
|
-
}
|
|
227
|
-
// Readonly state
|
|
228
|
-
if (this.hasAttribute('readonly')) {
|
|
229
|
-
this.#textareaElement.readOnly = true;
|
|
230
|
-
}
|
|
231
|
-
// Initial value
|
|
232
|
-
const value = this.getAttribute('value');
|
|
233
|
-
if (value) {
|
|
234
|
-
this.#textareaElement.value = value;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Attach event listeners to the textarea
|
|
239
|
-
*
|
|
240
|
-
* @private
|
|
241
|
-
*/
|
|
242
|
-
#attachEventListeners() {
|
|
243
|
-
// Focus event - audit logging + telemetry
|
|
244
|
-
this.#textareaElement.addEventListener('focus', () => {
|
|
245
|
-
this.recordTelemetryFocus();
|
|
246
|
-
this.audit('textarea_focused', {
|
|
247
|
-
name: this.#textareaElement.name
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
// Input event - real-time validation and character counting + telemetry
|
|
251
|
-
this.#textareaElement.addEventListener('input', (e) => {
|
|
252
|
-
this.recordTelemetryInput(e);
|
|
253
|
-
this.#handleInput(e);
|
|
254
|
-
});
|
|
255
|
-
// Blur event - final validation + telemetry
|
|
256
|
-
this.#textareaElement.addEventListener('blur', () => {
|
|
257
|
-
this.recordTelemetryBlur();
|
|
258
|
-
this.#validateAndShowErrors();
|
|
259
|
-
this.audit('textarea_blurred', {
|
|
260
|
-
name: this.#textareaElement.name,
|
|
261
|
-
hasValue: this.#textareaElement.value.length > 0
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
// Change event - audit logging
|
|
265
|
-
this.#textareaElement.addEventListener('change', () => {
|
|
266
|
-
this.audit('textarea_changed', {
|
|
267
|
-
name: this.#textareaElement.name,
|
|
268
|
-
valueLength: this.#textareaElement.value.length
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Handle input events
|
|
274
|
-
*
|
|
275
|
-
* Security Note: This is where we implement real-time validation and character counting.
|
|
276
|
-
*
|
|
277
|
-
* @private
|
|
278
|
-
*/
|
|
279
|
-
#handleInput(_event) {
|
|
280
|
-
this.detectInjection(this.#textareaElement.value, this.#textareaElement.name);
|
|
281
|
-
// Update character count
|
|
282
|
-
this.#updateCharCount();
|
|
283
|
-
// Clear previous errors on input (improve UX)
|
|
284
|
-
this.#clearErrors();
|
|
285
|
-
// Dispatch custom event for parent forms
|
|
286
|
-
this.dispatchEvent(new CustomEvent('secure-textarea', {
|
|
287
|
-
detail: {
|
|
288
|
-
name: this.#textareaElement.name,
|
|
289
|
-
value: this.#textareaElement.value,
|
|
290
|
-
tier: this.securityTier
|
|
291
|
-
},
|
|
292
|
-
bubbles: true,
|
|
293
|
-
composed: true
|
|
294
|
-
}));
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Update character count display
|
|
298
|
-
*
|
|
299
|
-
* @private
|
|
300
|
-
*/
|
|
301
|
-
#updateCharCount() {
|
|
302
|
-
const currentLength = this.#textareaElement.value.length;
|
|
303
|
-
const maxLength = this.#textareaElement.maxLength;
|
|
304
|
-
if (maxLength > 0) {
|
|
305
|
-
this.#charCountElement.textContent = `${currentLength} / ${maxLength}`;
|
|
306
|
-
// Warn when approaching limit
|
|
307
|
-
if (currentLength > maxLength * 0.9) {
|
|
308
|
-
this.#charCountElement.classList.add('warning');
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
this.#charCountElement.classList.remove('warning');
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
this.#charCountElement.textContent = `${currentLength}`;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Validate the textarea and show error messages
|
|
320
|
-
*
|
|
321
|
-
* @private
|
|
322
|
-
*/
|
|
323
|
-
#validateAndShowErrors() {
|
|
324
|
-
// Check rate limit first
|
|
325
|
-
const rateLimitCheck = this.checkRateLimit();
|
|
326
|
-
if (!rateLimitCheck.allowed) {
|
|
327
|
-
this.#showError(`Too many attempts. Please wait ${Math.ceil(rateLimitCheck.retryAfter / 1000)} seconds.`);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
// Perform validation
|
|
331
|
-
const minLength = this.getAttribute('minlength');
|
|
332
|
-
const maxLength = this.getAttribute('maxlength');
|
|
333
|
-
const validation = this.validateInput(this.#textareaElement.value, {
|
|
334
|
-
required: this.hasAttribute('required') || this.config.validation.required,
|
|
335
|
-
minLength: minLength ? parseInt(minLength, 10) : 0,
|
|
336
|
-
maxLength: maxLength ? parseInt(maxLength, 10) : this.config.validation.maxLength
|
|
337
|
-
});
|
|
338
|
-
if (!validation.valid) {
|
|
339
|
-
this.#showError(validation.errors.join(', '));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Show error message
|
|
344
|
-
*
|
|
345
|
-
* @private
|
|
346
|
-
*/
|
|
347
|
-
#showError(message) {
|
|
348
|
-
this.#errorContainer.textContent = message;
|
|
349
|
-
// Force reflow so browser registers the hidden state with content,
|
|
350
|
-
// then remove hidden to trigger the CSS transition
|
|
351
|
-
void this.#errorContainer.offsetHeight;
|
|
352
|
-
this.#errorContainer.classList.remove('hidden');
|
|
353
|
-
this.#textareaElement.classList.add('error');
|
|
354
|
-
this.#textareaElement.setAttribute('aria-invalid', 'true');
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Clear error messages
|
|
358
|
-
*
|
|
359
|
-
* @private
|
|
360
|
-
*/
|
|
361
|
-
#clearErrors() {
|
|
362
|
-
// Start the hide animation first, clear text only after transition ends
|
|
363
|
-
this.#errorContainer.classList.add('hidden');
|
|
364
|
-
this.#errorContainer.addEventListener('transitionend', () => {
|
|
365
|
-
if (this.#errorContainer.classList.contains('hidden')) {
|
|
366
|
-
this.#errorContainer.textContent = '';
|
|
367
|
-
}
|
|
368
|
-
}, { once: true });
|
|
369
|
-
this.#textareaElement.classList.remove('error');
|
|
370
|
-
this.#textareaElement.removeAttribute('aria-invalid');
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Get component-specific styles
|
|
374
|
-
*
|
|
375
|
-
* @private
|
|
376
|
-
*/
|
|
377
|
-
#getComponentStyles() {
|
|
378
|
-
return new URL('./secure-textarea.css', import.meta.url).href;
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Handle attribute changes
|
|
382
|
-
*
|
|
383
|
-
* @protected
|
|
384
|
-
*/
|
|
385
|
-
handleAttributeChange(name, _oldValue, newValue) {
|
|
386
|
-
if (!this.#textareaElement)
|
|
387
|
-
return;
|
|
388
|
-
switch (name) {
|
|
389
|
-
case 'disabled':
|
|
390
|
-
this.#textareaElement.disabled = this.hasAttribute('disabled');
|
|
391
|
-
break;
|
|
392
|
-
case 'readonly':
|
|
393
|
-
this.#textareaElement.readOnly = this.hasAttribute('readonly');
|
|
394
|
-
break;
|
|
395
|
-
case 'value':
|
|
396
|
-
if (newValue !== this.#textareaElement.value) {
|
|
397
|
-
this.#textareaElement.value = newValue || '';
|
|
398
|
-
this.#updateCharCount();
|
|
399
|
-
}
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Get the current value
|
|
405
|
-
*
|
|
406
|
-
* @public
|
|
407
|
-
*/
|
|
408
|
-
get value() {
|
|
409
|
-
return this.#textareaElement ? this.#textareaElement.value : '';
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Set the value
|
|
413
|
-
*
|
|
414
|
-
* @public
|
|
415
|
-
*/
|
|
416
|
-
set value(value) {
|
|
417
|
-
if (this.#textareaElement) {
|
|
418
|
-
this.#textareaElement.value = value || '';
|
|
419
|
-
this.#updateCharCount();
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Get the textarea name
|
|
424
|
-
*
|
|
425
|
-
* @public
|
|
426
|
-
*/
|
|
427
|
-
get name() {
|
|
428
|
-
return this.#textareaElement ? this.#textareaElement.name : '';
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Check if the textarea is valid
|
|
432
|
-
*
|
|
433
|
-
* @public
|
|
434
|
-
*/
|
|
435
|
-
get valid() {
|
|
436
|
-
const minLength = this.getAttribute('minlength');
|
|
437
|
-
const maxLength = this.getAttribute('maxlength');
|
|
438
|
-
const validation = this.validateInput(this.#textareaElement.value, {
|
|
439
|
-
required: this.hasAttribute('required') || this.config.validation.required,
|
|
440
|
-
minLength: minLength ? parseInt(minLength, 10) : 0,
|
|
441
|
-
maxLength: maxLength ? parseInt(maxLength, 10) : this.config.validation.maxLength
|
|
442
|
-
});
|
|
443
|
-
return validation.valid;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Focus the textarea
|
|
447
|
-
*
|
|
448
|
-
* @public
|
|
449
|
-
*/
|
|
450
|
-
focus() {
|
|
451
|
-
if (this.#textareaElement) {
|
|
452
|
-
this.#textareaElement.focus();
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Blur the textarea
|
|
457
|
-
*
|
|
458
|
-
* @public
|
|
459
|
-
*/
|
|
460
|
-
blur() {
|
|
461
|
-
if (this.#textareaElement) {
|
|
462
|
-
this.#textareaElement.blur();
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Cleanup on disconnect
|
|
467
|
-
*/
|
|
468
|
-
/**
|
|
469
|
-
* Show inline threat feedback inside the component's shadow DOM.
|
|
470
|
-
* @protected
|
|
471
|
-
*/
|
|
472
|
-
showThreatFeedback(patternId, tier) {
|
|
473
|
-
if (!this.#threatContainer || !this.#textareaElement)
|
|
474
|
-
return;
|
|
475
|
-
this.#threatContainer.textContent = '';
|
|
476
|
-
const msg = document.createElement('span');
|
|
477
|
-
msg.className = 'threat-message';
|
|
478
|
-
msg.textContent = this.getThreatLabel(patternId);
|
|
479
|
-
const patternBadge = document.createElement('span');
|
|
480
|
-
patternBadge.className = 'threat-badge';
|
|
481
|
-
patternBadge.textContent = patternId;
|
|
482
|
-
const tierBadge = document.createElement('span');
|
|
483
|
-
tierBadge.className = `threat-tier threat-tier--${tier}`;
|
|
484
|
-
tierBadge.textContent = tier;
|
|
485
|
-
this.#threatContainer.appendChild(msg);
|
|
486
|
-
this.#threatContainer.appendChild(patternBadge);
|
|
487
|
-
this.#threatContainer.appendChild(tierBadge);
|
|
488
|
-
void this.#threatContainer.offsetHeight;
|
|
489
|
-
this.#threatContainer.classList.remove('hidden');
|
|
490
|
-
this.#textareaElement.classList.add('threat');
|
|
491
|
-
this.#textareaElement.setAttribute('aria-invalid', 'true');
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Clear the threat feedback container.
|
|
495
|
-
* @protected
|
|
496
|
-
*/
|
|
497
|
-
clearThreatFeedback() {
|
|
498
|
-
if (!this.#threatContainer || !this.#textareaElement)
|
|
499
|
-
return;
|
|
500
|
-
this.#threatContainer.classList.add('hidden');
|
|
501
|
-
this.#threatContainer.addEventListener('transitionend', () => {
|
|
502
|
-
if (this.#threatContainer.classList.contains('hidden')) {
|
|
503
|
-
this.#threatContainer.textContent = '';
|
|
504
|
-
}
|
|
505
|
-
}, { once: true });
|
|
506
|
-
this.#textareaElement.classList.remove('threat');
|
|
507
|
-
this.#textareaElement.removeAttribute('aria-invalid');
|
|
508
|
-
}
|
|
509
|
-
disconnectedCallback() {
|
|
510
|
-
super.disconnectedCallback();
|
|
511
|
-
// Clear sensitive data from memory
|
|
512
|
-
if (this.#textareaElement) {
|
|
513
|
-
this.#textareaElement.value = '';
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
// Define the custom element
|
|
518
|
-
customElements.define('secure-textarea', SecureTextarea);
|
|
519
|
-
export default SecureTextarea;
|
|
520
|
-
//# sourceMappingURL=secure-textarea.js.map
|
|
30
|
+
*/import{SecureBaseComponent as u}from"../../core/base-component.js";class h extends u{#t=null;#a=null;#i=null;#e=null;#s=null;#r=`secure-textarea-${Math.random().toString(36).substring(2,11)}`;static get observedAttributes(){return[...super.observedAttributes,"name","label","placeholder","required","minlength","maxlength","rows","cols","wrap","value"]}constructor(){super()}render(){const t=document.createDocumentFragment(),e=document.createElement("div");e.className="textarea-container",e.setAttribute("part","container");const i=this.getAttribute("label");i&&(this.#a=document.createElement("label"),this.#a.htmlFor=this.#r,this.#a.textContent=this.sanitizeValue(i),this.#a.setAttribute("part","label"),e.appendChild(this.#a));const s=document.createElement("div");return s.className="textarea-wrapper",s.setAttribute("part","wrapper"),this.#t=document.createElement("textarea"),this.#t.id=this.#r,this.#t.className="textarea-field",this.#t.setAttribute("part","textarea"),this.#l(),this.#o(),s.appendChild(this.#t),e.appendChild(s),this.#s=document.createElement("span"),this.#s.className="char-count",this.#n(),e.appendChild(this.#s),this.#i=document.createElement("div"),this.#i.className="error-container hidden",this.#i.setAttribute("role","alert"),this.#i.setAttribute("part","error"),this.#i.id=`${this.#r}-error`,e.appendChild(this.#i),this.#e=document.createElement("div"),this.#e.className="threat-container hidden",this.#e.setAttribute("role","alert"),this.#e.setAttribute("part","threat"),this.#e.id=`${this.#r}-threat`,e.appendChild(this.#e),this.addComponentStyles(this.#m()),t.appendChild(e),t}#l(){const t=this.config,e=this.getAttribute("name");e&&(this.#t.name=this.sanitizeValue(e)),!this.getAttribute("label")&&e&&this.#t.setAttribute("aria-label",this.sanitizeValue(e)),this.#t.setAttribute("aria-describedby",`${this.#r}-error`);const i=this.getAttribute("placeholder");i&&(this.#t.placeholder=this.sanitizeValue(i)),(this.hasAttribute("required")||t.validation.required)&&(this.#t.required=!0,this.#t.setAttribute("aria-required","true"));const s=this.getAttribute("minlength");s&&(this.#t.minLength=parseInt(s,10));const a=this.getAttribute("maxlength")||t.validation.maxLength;a&&(this.#t.maxLength=parseInt(String(a),10));const l=this.getAttribute("rows")||3;this.#t.rows=parseInt(String(l),10);const r=this.getAttribute("cols");r&&(this.#t.cols=parseInt(r,10));const o=this.getAttribute("wrap")||"soft";if(this.#t.wrap=o,t.storage.allowAutocomplete){const d=this.getAttribute("autocomplete")||"on";this.#t.autocomplete=d}else this.#t.autocomplete="off";this.hasAttribute("disabled")&&(this.#t.disabled=!0),this.hasAttribute("readonly")&&(this.#t.readOnly=!0);const n=this.getAttribute("value");n&&(this.#t.value=n)}#o(){this.#t.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.audit("textarea_focused",{name:this.#t.name})}),this.#t.addEventListener("input",t=>{this.recordTelemetryInput(t),this.#d(t)}),this.#t.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#u(),this.audit("textarea_blurred",{name:this.#t.name,hasValue:this.#t.value.length>0})}),this.#t.addEventListener("change",()=>{this.audit("textarea_changed",{name:this.#t.name,valueLength:this.#t.value.length})})}#d(t){this.detectInjection(this.#t.value,this.#t.name),this.#n(),this.#c(),this.dispatchEvent(new CustomEvent("secure-textarea",{detail:{name:this.#t.name,value:this.#t.value,tier:this.securityTier},bubbles:!0,composed:!0}))}#n(){const t=this.#t.value.length,e=this.#t.maxLength;e>0?(this.#s.textContent=`${t} / ${e}`,t>e*.9?this.#s.classList.add("warning"):this.#s.classList.remove("warning")):this.#s.textContent=`${t}`}#u(){const t=this.checkRateLimit();if(!t.allowed){this.#h(`Too many attempts. Please wait ${Math.ceil(t.retryAfter/1e3)} seconds.`);return}const e=this.getAttribute("minlength"),i=this.getAttribute("maxlength"),s=this.validateInput(this.#t.value,{required:this.hasAttribute("required")||this.config.validation.required,minLength:e?parseInt(e,10):0,maxLength:i?parseInt(i,10):this.config.validation.maxLength});s.valid||this.#h(s.errors.join(", "))}#h(t){this.#i.textContent=t,this.#i.offsetHeight,this.#i.classList.remove("hidden"),this.#t.classList.add("error"),this.#t.setAttribute("aria-invalid","true")}#c(){this.#i.classList.add("hidden"),this.#i.addEventListener("transitionend",()=>{this.#i.classList.contains("hidden")&&(this.#i.textContent="")},{once:!0}),this.#t.classList.remove("error"),this.#t.removeAttribute("aria-invalid")}#m(){return new URL("./secure-textarea.css",import.meta.url).href}handleAttributeChange(t,e,i){if(this.#t)switch(t){case"disabled":this.#t.disabled=this.hasAttribute("disabled");break;case"readonly":this.#t.readOnly=this.hasAttribute("readonly");break;case"value":i!==this.#t.value&&(this.#t.value=i||"",this.#n());break}}get value(){return this.#t?this.#t.value:""}set value(t){this.#t&&(this.#t.value=t||"",this.#n())}get name(){return this.#t?this.#t.name:""}get valid(){const t=this.getAttribute("minlength"),e=this.getAttribute("maxlength");return this.validateInput(this.#t.value,{required:this.hasAttribute("required")||this.config.validation.required,minLength:t?parseInt(t,10):0,maxLength:e?parseInt(e,10):this.config.validation.maxLength}).valid}focus(){this.#t&&this.#t.focus()}blur(){this.#t&&this.#t.blur()}showThreatFeedback(t,e){if(!this.#e||!this.#t)return;this.#e.textContent="";const i=document.createElement("span");i.className="threat-message",i.textContent=this.getThreatLabel(t);const s=document.createElement("span");s.className="threat-badge",s.textContent=t;const a=document.createElement("span");a.className=`threat-tier threat-tier--${e}`,a.textContent=e,this.#e.appendChild(i),this.#e.appendChild(s),this.#e.appendChild(a),this.#e.offsetHeight,this.#e.classList.remove("hidden"),this.#t.classList.add("threat"),this.#t.setAttribute("aria-invalid","true")}clearThreatFeedback(){!this.#e||!this.#t||(this.#e.classList.add("hidden"),this.#e.addEventListener("transitionend",()=>{this.#e.classList.contains("hidden")&&(this.#e.textContent="")},{once:!0}),this.#t.classList.remove("threat"),this.#t.removeAttribute("aria-invalid"))}disconnectedCallback(){super.disconnectedCallback(),this.#t&&(this.#t.value="")}}customElements.define("secure-textarea",h);var p=h;export{h as SecureTextarea,p as default};
|