- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
552 lines
14 KiB
JavaScript
552 lines
14 KiB
JavaScript
import Event from '../../event/Event.js';
|
|
import * as PropertySymbol from '../../PropertySymbol.js';
|
|
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
|
|
import HTMLElement from '../html-element/HTMLElement.js';
|
|
import HTMLInputElementSelectionDirectionEnum from '../html-input-element/HTMLInputElementSelectionDirectionEnum.js';
|
|
import HTMLInputElementSelectionModeEnum from '../html-input-element/HTMLInputElementSelectionModeEnum.js';
|
|
import ValidityState from '../../validity-state/ValidityState.js';
|
|
import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js';
|
|
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
|
|
/**
|
|
* HTML Text Area Element.
|
|
*
|
|
* Reference:
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement.
|
|
*/
|
|
export default class HTMLTextAreaElement extends HTMLElement {
|
|
type = 'textarea';
|
|
// Internal properties
|
|
[PropertySymbol.validationMessage] = '';
|
|
[PropertySymbol.validity] = new ValidityState(this);
|
|
[PropertySymbol.value] = null;
|
|
[PropertySymbol.textAreaNode] = this;
|
|
[PropertySymbol.formNode] = null;
|
|
// Private properties
|
|
#selectionStart = null;
|
|
#selectionEnd = null;
|
|
#selectionDirection = HTMLInputElementSelectionDirectionEnum.none;
|
|
// Events
|
|
/* eslint-disable jsdoc/require-jsdoc */
|
|
get oninput() {
|
|
return ElementEventAttributeUtility.getEventListener(this, 'oninput');
|
|
}
|
|
set oninput(value) {
|
|
this[PropertySymbol.propertyEventListeners].set('oninput', value);
|
|
}
|
|
get onselectionchange() {
|
|
return ElementEventAttributeUtility.getEventListener(this, 'onselectionchange');
|
|
}
|
|
set onselectionchange(value) {
|
|
this[PropertySymbol.propertyEventListeners].set('onselectionchange', value);
|
|
}
|
|
/* eslint-enable jsdoc/require-jsdoc */
|
|
/**
|
|
* Returns validation message.
|
|
*
|
|
* @returns Validation message.
|
|
*/
|
|
get validationMessage() {
|
|
return this[PropertySymbol.validationMessage];
|
|
}
|
|
/**
|
|
* Returns validity.
|
|
*
|
|
* @returns Validity.
|
|
*/
|
|
get validity() {
|
|
return this[PropertySymbol.validity];
|
|
}
|
|
/**
|
|
* Returns the default value.
|
|
*
|
|
* @returns Default value.
|
|
*/
|
|
get defaultValue() {
|
|
return this.textContent;
|
|
}
|
|
/**
|
|
* Sets the default value.
|
|
*
|
|
* @param defaultValue Default value.
|
|
*/
|
|
set defaultValue(defaultValue) {
|
|
this.textContent = defaultValue;
|
|
}
|
|
/**
|
|
* Returns minlength.
|
|
*
|
|
* @returns Min length.
|
|
*/
|
|
get minLength() {
|
|
const minLength = this.getAttribute('minlength');
|
|
if (minLength !== null) {
|
|
return parseInt(minLength);
|
|
}
|
|
return -1;
|
|
}
|
|
/**
|
|
* Sets minlength.
|
|
*
|
|
* @param minLength Min length.
|
|
*/
|
|
set minLength(minlength) {
|
|
this.setAttribute('minlength', String(minlength));
|
|
}
|
|
/**
|
|
* Returns maxlength.
|
|
*
|
|
* @returns Max length.
|
|
*/
|
|
get maxLength() {
|
|
const maxLength = this.getAttribute('maxlength');
|
|
if (maxLength !== null) {
|
|
return parseInt(maxLength);
|
|
}
|
|
return -1;
|
|
}
|
|
/**
|
|
* Sets maxlength.
|
|
*
|
|
* @param maxlength Max length.
|
|
*/
|
|
set maxLength(maxLength) {
|
|
this.setAttribute('maxlength', String(maxLength));
|
|
}
|
|
/**
|
|
* Returns name.
|
|
*
|
|
* @returns Name.
|
|
*/
|
|
get name() {
|
|
return this.getAttribute('name') || '';
|
|
}
|
|
/**
|
|
* Sets name.
|
|
*
|
|
* @param name Name.
|
|
*/
|
|
set name(name) {
|
|
this.setAttribute('name', name);
|
|
}
|
|
/**
|
|
* Returns placeholder.
|
|
*
|
|
* @returns Placeholder.
|
|
*/
|
|
get placeholder() {
|
|
return this.getAttribute('placeholder') || '';
|
|
}
|
|
/**
|
|
* Sets placeholder.
|
|
*
|
|
* @param placeholder Placeholder.
|
|
*/
|
|
set placeholder(placeholder) {
|
|
this.setAttribute('placeholder', placeholder);
|
|
}
|
|
/**
|
|
* Returns inputmode.
|
|
*
|
|
* @returns Inputmode.
|
|
*/
|
|
get inputMode() {
|
|
return this.getAttribute('inputmode') || '';
|
|
}
|
|
/**
|
|
* Sets inputmode.
|
|
*
|
|
* @param inputmode Inputmode.
|
|
*/
|
|
set inputMode(inputmode) {
|
|
this.setAttribute('inputmode', inputmode);
|
|
}
|
|
/**
|
|
* Returns cols.
|
|
*
|
|
* @returns Cols.
|
|
*/
|
|
get cols() {
|
|
return this.getAttribute('cols') || '';
|
|
}
|
|
/**
|
|
* Sets cols.
|
|
*
|
|
* @param cols Cols.
|
|
*/
|
|
set cols(cols) {
|
|
this.setAttribute('cols', cols);
|
|
}
|
|
/**
|
|
* Returns rows.
|
|
*
|
|
* @returns Rows.
|
|
*/
|
|
get rows() {
|
|
return this.getAttribute('rows') || '';
|
|
}
|
|
/**
|
|
* Sets rows.
|
|
*
|
|
* @param rows Rows.
|
|
*/
|
|
set rows(rows) {
|
|
this.setAttribute('rows', rows);
|
|
}
|
|
/**
|
|
* Returns autocomplete.
|
|
*
|
|
* @returns Autocomplete.
|
|
*/
|
|
get autocomplete() {
|
|
return this.getAttribute('autocomplete') || '';
|
|
}
|
|
/**
|
|
* Sets autocomplete.
|
|
*
|
|
* @param autocomplete Autocomplete.
|
|
*/
|
|
set autocomplete(autocomplete) {
|
|
this.setAttribute('autocomplete', autocomplete);
|
|
}
|
|
/**
|
|
* Returns readOnly.
|
|
*
|
|
* @returns ReadOnly.
|
|
*/
|
|
get readOnly() {
|
|
return this.getAttribute('readonly') !== null;
|
|
}
|
|
/**
|
|
* Sets readOnly.
|
|
*
|
|
* @param readOnly ReadOnly.
|
|
*/
|
|
set readOnly(readOnly) {
|
|
if (!readOnly) {
|
|
this.removeAttribute('readonly');
|
|
}
|
|
else {
|
|
this.setAttribute('readonly', '');
|
|
}
|
|
}
|
|
/**
|
|
* Returns disabled.
|
|
*
|
|
* @returns Disabled.
|
|
*/
|
|
get disabled() {
|
|
return this.getAttribute('disabled') !== null;
|
|
}
|
|
/**
|
|
* Sets disabled.
|
|
*
|
|
* @param disabled Disabled.
|
|
*/
|
|
set disabled(disabled) {
|
|
if (!disabled) {
|
|
this.removeAttribute('disabled');
|
|
}
|
|
else {
|
|
this.setAttribute('disabled', '');
|
|
}
|
|
}
|
|
/**
|
|
* Returns autofocus.
|
|
*
|
|
* @returns Autofocus.
|
|
*/
|
|
get autofocus() {
|
|
return this.getAttribute('autofocus') !== null;
|
|
}
|
|
/**
|
|
* Sets autofocus.
|
|
*
|
|
* @param autofocus Autofocus.
|
|
*/
|
|
set autofocus(autofocus) {
|
|
if (!autofocus) {
|
|
this.removeAttribute('autofocus');
|
|
}
|
|
else {
|
|
this.setAttribute('autofocus', '');
|
|
}
|
|
}
|
|
/**
|
|
* Returns required.
|
|
*
|
|
* @returns Required.
|
|
*/
|
|
get required() {
|
|
return this.getAttribute('required') !== null;
|
|
}
|
|
/**
|
|
* Sets required.
|
|
*
|
|
* @param required Required.
|
|
*/
|
|
set required(required) {
|
|
if (!required) {
|
|
this.removeAttribute('required');
|
|
}
|
|
else {
|
|
this.setAttribute('required', '');
|
|
}
|
|
}
|
|
/**
|
|
* Returns value.
|
|
*
|
|
* @returns Value.
|
|
*/
|
|
get value() {
|
|
if (this[PropertySymbol.value] === null) {
|
|
return this.textContent;
|
|
}
|
|
return this[PropertySymbol.value];
|
|
}
|
|
/**
|
|
* Sets value.
|
|
*
|
|
* @param value Value.
|
|
*/
|
|
set value(value) {
|
|
const oldValue = this[PropertySymbol.value];
|
|
this[PropertySymbol.value] = value;
|
|
if (oldValue !== this[PropertySymbol.value]) {
|
|
this.#selectionStart = this[PropertySymbol.value].length;
|
|
this.#selectionEnd = this[PropertySymbol.value].length;
|
|
this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none;
|
|
}
|
|
}
|
|
/**
|
|
* Returns selection start.
|
|
*
|
|
* @returns Selection start.
|
|
*/
|
|
get selectionStart() {
|
|
if (this.#selectionStart === null) {
|
|
return this.value.length;
|
|
}
|
|
return this.#selectionStart;
|
|
}
|
|
/**
|
|
* Sets selection start.
|
|
*
|
|
* @param start Start.
|
|
*/
|
|
set selectionStart(start) {
|
|
this.setSelectionRange(start, Math.max(start, this.selectionEnd), this.#selectionDirection);
|
|
}
|
|
/**
|
|
* Returns selection end.
|
|
*
|
|
* @returns Selection end.
|
|
*/
|
|
get selectionEnd() {
|
|
if (this.#selectionEnd === null) {
|
|
return this.value.length;
|
|
}
|
|
return this.#selectionEnd;
|
|
}
|
|
/**
|
|
* Sets selection end.
|
|
*
|
|
* @param end End.
|
|
*/
|
|
set selectionEnd(end) {
|
|
this.setSelectionRange(this.selectionStart, end, this.#selectionDirection);
|
|
}
|
|
/**
|
|
* Returns selection direction.
|
|
*
|
|
* @returns Selection direction.
|
|
*/
|
|
get selectionDirection() {
|
|
return this.#selectionDirection;
|
|
}
|
|
/**
|
|
* Sets selection direction.
|
|
*
|
|
* @param direction Direction.
|
|
*/
|
|
set selectionDirection(direction) {
|
|
this.setSelectionRange(this.selectionStart, this.selectionEnd, direction);
|
|
}
|
|
/**
|
|
* Returns the parent form element.
|
|
*
|
|
* @returns Form.
|
|
*/
|
|
get form() {
|
|
if (this[PropertySymbol.formNode]) {
|
|
return this[PropertySymbol.formNode];
|
|
}
|
|
const id = this.getAttribute('form');
|
|
if (!id || !this[PropertySymbol.isConnected]) {
|
|
return null;
|
|
}
|
|
return this[PropertySymbol.ownerDocument].getElementById(id);
|
|
}
|
|
/**
|
|
* Returns text length.
|
|
*
|
|
* @param Text Length.
|
|
*/
|
|
get textLength() {
|
|
return this.value.length;
|
|
}
|
|
/**
|
|
* Returns the associated label elements.
|
|
*
|
|
* @returns Label elements.
|
|
*/
|
|
get labels() {
|
|
return HTMLLabelElementUtility.getAssociatedLabelElements(this);
|
|
}
|
|
/**
|
|
* @override
|
|
*/
|
|
get tabIndex() {
|
|
const tabIndex = this.getAttribute('tabindex');
|
|
if (tabIndex !== null) {
|
|
const parsed = Number(tabIndex);
|
|
return isNaN(parsed) ? 0 : parsed;
|
|
}
|
|
return 0;
|
|
}
|
|
/**
|
|
* @override
|
|
*/
|
|
set tabIndex(tabIndex) {
|
|
super.tabIndex = tabIndex;
|
|
}
|
|
/**
|
|
* Selects the text.
|
|
*/
|
|
select() {
|
|
this.#selectionStart = 0;
|
|
this.#selectionEnd = this.value.length;
|
|
this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none;
|
|
this.dispatchEvent(new Event('select', { bubbles: true, cancelable: false }));
|
|
}
|
|
/**
|
|
* Set selection range.
|
|
*
|
|
* @param start Start.
|
|
* @param end End.
|
|
* @param [direction="none"] Direction.
|
|
*/
|
|
setSelectionRange(start, end, direction = 'none') {
|
|
this.#selectionEnd = Math.min(end, this.value.length);
|
|
this.#selectionStart = Math.min(start, this.selectionEnd);
|
|
this.#selectionDirection =
|
|
direction === HTMLInputElementSelectionDirectionEnum.forward ||
|
|
direction === HTMLInputElementSelectionDirectionEnum.backward
|
|
? direction
|
|
: HTMLInputElementSelectionDirectionEnum.none;
|
|
this.dispatchEvent(new Event('select', { bubbles: true, cancelable: false }));
|
|
}
|
|
/**
|
|
* Set range text.
|
|
*
|
|
* @param replacement Replacement.
|
|
* @param [start] Start.
|
|
* @param [end] End.
|
|
* @param [direction] Direction.
|
|
* @param selectionMode
|
|
*/
|
|
setRangeText(replacement, start = null, end = null, selectionMode = HTMLInputElementSelectionModeEnum.preserve) {
|
|
if (start === null) {
|
|
start = this.#selectionStart;
|
|
}
|
|
if (end === null) {
|
|
end = this.#selectionEnd;
|
|
}
|
|
if (start > end) {
|
|
throw new this[PropertySymbol.window].DOMException('The index is not in the allowed range.', DOMExceptionNameEnum.invalidStateError);
|
|
}
|
|
start = Math.min(start, this.value.length);
|
|
end = Math.min(end, this.value.length);
|
|
const val = this.value;
|
|
let selectionStart = this.#selectionStart;
|
|
let selectionEnd = this.#selectionEnd;
|
|
this.value = val.slice(0, start) + replacement + val.slice(end);
|
|
const newEnd = start + this.value.length;
|
|
switch (selectionMode) {
|
|
case HTMLInputElementSelectionModeEnum.select:
|
|
this.setSelectionRange(start, newEnd);
|
|
break;
|
|
case HTMLInputElementSelectionModeEnum.start:
|
|
this.setSelectionRange(start, start);
|
|
break;
|
|
case HTMLInputElementSelectionModeEnum.end:
|
|
this.setSelectionRange(newEnd, newEnd);
|
|
break;
|
|
default:
|
|
const delta = replacement.length - (end - start);
|
|
if (selectionStart > end) {
|
|
selectionStart += delta;
|
|
}
|
|
else if (selectionStart > start) {
|
|
selectionStart = start;
|
|
}
|
|
if (selectionEnd > end) {
|
|
selectionEnd += delta;
|
|
}
|
|
else if (selectionEnd > start) {
|
|
selectionEnd = newEnd;
|
|
}
|
|
this.setSelectionRange(selectionStart, selectionEnd);
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* Sets validation message.
|
|
*
|
|
* @param message Message.
|
|
*/
|
|
setCustomValidity(message) {
|
|
this[PropertySymbol.validationMessage] = String(message);
|
|
}
|
|
/**
|
|
* Checks validity.
|
|
*
|
|
* @returns "true" if the field is valid.
|
|
*/
|
|
checkValidity() {
|
|
const valid = this.disabled || this.readOnly || this[PropertySymbol.validity].valid;
|
|
if (!valid) {
|
|
this.dispatchEvent(new Event('invalid', { bubbles: true, cancelable: true }));
|
|
}
|
|
return valid;
|
|
}
|
|
/**
|
|
* Reports validity.
|
|
*
|
|
* @returns "true" if the field is valid.
|
|
*/
|
|
reportValidity() {
|
|
return this.checkValidity();
|
|
}
|
|
/**
|
|
* @override
|
|
*/
|
|
[PropertySymbol.cloneNode](deep = false) {
|
|
const clone = super[PropertySymbol.cloneNode](deep);
|
|
clone[PropertySymbol.value] = this[PropertySymbol.value];
|
|
clone.#selectionStart = this.#selectionStart;
|
|
clone.#selectionEnd = this.#selectionEnd;
|
|
clone.#selectionDirection = this.#selectionDirection;
|
|
return clone;
|
|
}
|
|
/**
|
|
* Resets selection.
|
|
*/
|
|
[PropertySymbol.resetSelection]() {
|
|
if (this[PropertySymbol.value] === null) {
|
|
this.#selectionStart = null;
|
|
this.#selectionEnd = null;
|
|
this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none;
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=HTMLTextAreaElement.js.map
|