feat: Reinitialize frontend with SvelteKit and TypeScript

- 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
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,45 @@
import HTMLCollection from '../element/HTMLCollection.js';
import HTMLSelectElement from './HTMLSelectElement.js';
import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
/**
* HTML Options Collection.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection.
*/
export default class HTMLOptionsCollection extends HTMLCollection<HTMLOptionElement> {
private [PropertySymbol.ownerElement];
/**
* Constructor.
*
* @param illegalConstructorSymbol Illegal constructor symbol.
* @param ownerElement Select element.
*/
constructor(illegalConstructorSymbol: symbol, ownerElement: HTMLSelectElement);
/**
* Returns selectedIndex.
*
* @returns SelectedIndex.
*/
get selectedIndex(): number;
/**
* Sets selectedIndex.
*
* @param selectedIndex SelectedIndex.
*/
set selectedIndex(selectedIndex: number);
/**
*
* @param element
* @param before
*/
add(element: HTMLOptionElement, before?: number | HTMLOptionElement): void;
/**
* Removes indexed element from collection.
*
* @param index Index.
*/
remove(index: number): void;
}
//# sourceMappingURL=HTMLOptionsCollection.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"HTMLOptionsCollection.d.ts","sourceRoot":"","sources":["../../../src/nodes/html-select-element/HTMLOptionsCollection.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,8BAA8B,CAAC;AAC1D,OAAO,iBAAiB,MAAM,wBAAwB,CAAC;AACvD,OAAO,iBAAiB,MAAM,6CAA6C,CAAC;AAC5E,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAC;AAG1D;;;;;GAKG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,cAAc,CAAC,iBAAiB,CAAC;IACnF,QAAgB,CAAC,cAAc,CAAC,YAAY,CAAC,CAAoB;IAEjE;;;;;OAKG;gBACS,wBAAwB,EAAE,MAAM,EAAE,YAAY,EAAE,iBAAiB;IAY7E;;;;OAIG;IACH,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;;;OAIG;IACH,IAAW,aAAa,CAAC,aAAa,EAAE,MAAM,EAE7C;IAED;;;;OAIG;IACI,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAIjF;;;;OAIG;IACI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAGlC"}

View File

@@ -0,0 +1,54 @@
import HTMLCollection from '../element/HTMLCollection.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import QuerySelector from '../../query-selector/QuerySelector.js';
/**
* HTML Options Collection.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection.
*/
export default class HTMLOptionsCollection extends HTMLCollection {
/**
* Constructor.
*
* @param illegalConstructorSymbol Illegal constructor symbol.
* @param ownerElement Select element.
*/
constructor(illegalConstructorSymbol, ownerElement) {
super(illegalConstructorSymbol, () => (QuerySelector.querySelectorAll(ownerElement, 'option')[PropertySymbol.items]));
this[PropertySymbol.ownerElement] = ownerElement;
}
/**
* Returns selectedIndex.
*
* @returns SelectedIndex.
*/
get selectedIndex() {
return this[PropertySymbol.ownerElement].selectedIndex;
}
/**
* Sets selectedIndex.
*
* @param selectedIndex SelectedIndex.
*/
set selectedIndex(selectedIndex) {
this[PropertySymbol.ownerElement].selectedIndex = selectedIndex;
}
/**
*
* @param element
* @param before
*/
add(element, before) {
this[PropertySymbol.ownerElement].add(element, before);
}
/**
* Removes indexed element from collection.
*
* @param index Index.
*/
remove(index) {
this[PropertySymbol.ownerElement].remove(index);
}
}
//# sourceMappingURL=HTMLOptionsCollection.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"HTMLOptionsCollection.js","sourceRoot":"","sources":["../../../src/nodes/html-select-element/HTMLOptionsCollection.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,8BAA8B,CAAC;AAG1D,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAC;AAC1D,OAAO,aAAa,MAAM,uCAAuC,CAAC;AAElE;;;;;GAKG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,cAAiC;IAGnF;;;;;OAKG;IACH,YAAY,wBAAgC,EAAE,YAA+B;QAC5E,KAAK,CACJ,wBAAwB,EACxB,GAAG,EAAE,CACiB,CACpB,aAAa,CAAC,gBAAgB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAC5E,CACF,CAAC;QAEF,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,IAAW,aAAa;QACvB,OAAO,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,aAAa,CAAC;IACxD,CAAC;IAED;;;;OAIG;IACH,IAAW,aAAa,CAAC,aAAqB;QAC7C,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,aAAa,GAAG,aAAa,CAAC;IACjE,CAAC;IAED;;;;OAIG;IACI,GAAG,CAAC,OAA0B,EAAE,MAAmC;QACzE,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACxD,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,KAAa;QAC1B,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;CACD"}

View File

@@ -0,0 +1,227 @@
import HTMLElement from '../html-element/HTMLElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import HTMLFormElement from '../html-form-element/HTMLFormElement.js';
import ValidityState from '../../validity-state/ValidityState.js';
import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js';
import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js';
import HTMLOptionsCollection from './HTMLOptionsCollection.js';
import Event from '../../event/Event.js';
import HTMLCollection from '../element/HTMLCollection.js';
import NodeList from '../node/NodeList.js';
/**
* HTML Select Element.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement.
*/
export default class HTMLSelectElement extends HTMLElement {
#private;
[PropertySymbol.validationMessage]: string;
[PropertySymbol.validity]: ValidityState;
[PropertySymbol.options]: HTMLOptionsCollection | null;
[PropertySymbol.selectedOptions]: HTMLCollection<HTMLOptionElement> | null;
[PropertySymbol.selectedIndex]: number;
[PropertySymbol.proxy]: HTMLSelectElement;
/**
* Constructor.
*/
constructor();
get onchange(): ((event: Event) => void) | null;
set onchange(value: ((event: Event) => void) | null);
get oninput(): ((event: Event) => void) | null;
set oninput(value: ((event: Event) => void) | null);
/**
* Returns length.
*
* @returns Length.
*/
get length(): number;
/**
* Returns options.
*
* @returns Options.
*/
get options(): HTMLOptionsCollection;
/**
* Returns validation message.
*
* @returns Validation message.
*/
get validationMessage(): string;
/**
* Returns validity.
*
* @returns Validity.
*/
get validity(): ValidityState;
/**
* Returns name.
*
* @returns Name.
*/
get name(): string;
/**
* Sets name.
*
* @param name Name.
*/
set name(name: string);
/**
* Returns disabled.
*
* @returns Disabled.
*/
get disabled(): boolean;
/**
* Sets disabled.
*
* @param disabled Disabled.
*/
set disabled(disabled: boolean);
/**
* Returns multiple.
*
* @returns Multiple.
*/
get multiple(): boolean;
/**
* Sets multiple.
*
* @param multiple Multiple.
*/
set multiple(multiple: boolean);
/**
* Returns autofocus.
*
* @returns Autofocus.
*/
get autofocus(): boolean;
/**
* Sets autofocus.
*
* @param autofocus Autofocus.
*/
set autofocus(autofocus: boolean);
/**
* Returns required.
*
* @returns Required.
*/
get required(): boolean;
/**
* Sets required.
*
* @param required Required.
*/
set required(required: boolean);
/**
* Returns type.
*
* @returns type.
*/
get type(): string;
/**
* Returns value.
*
* @returns Value.
*/
get value(): string;
/**
* Sets value.
*
* @param value Value.
*/
set value(value: string);
/**
* Returns value.
*
* @returns Value.
*/
get selectedIndex(): number;
/**
* Sets value.
*
* @param selectedIndex Selected index.
*/
set selectedIndex(selectedIndex: number);
/**
* Returns selected options.
*
* @returns HTMLCollection.
*/
get selectedOptions(): HTMLCollection<HTMLOptionElement>;
/**
* Returns the associated label elements.
*
* @returns Label elements.
*/
get labels(): NodeList<HTMLLabelElement>;
/**
* Returns the parent form element.
*
* @returns Form.
*/
get form(): HTMLFormElement;
/**
* Returns "true" if it will validate.
*
* @returns "true" if it will validate.
*/
get willValidate(): boolean;
/**
* @override
*/
get tabIndex(): number;
/**
* @override
*/
set tabIndex(tabIndex: number);
/**
* Returns item from options collection by index.
*
* @param index Index.
*/
item(index: number): HTMLOptionElement;
/**
* Adds new option to options collection.
*
* @param element HTMLOptionElement to add.
* @param before HTMLOptionElement or index number.
*/
add(element: HTMLOptionElement, before?: number | HTMLOptionElement): void;
/**
* Removes indexed element from collection or the select element.
*
* @param [index] Index.
*/
remove(index?: number): void;
/**
* Sets validation message.
*
* @param message Message.
*/
setCustomValidity(message: string): void;
/**
* Checks validity.
*
* @returns "true" if the field is valid.
*/
checkValidity(): boolean;
/**
* Reports validity.
*
* @returns "true" if the field is valid.
*/
reportValidity(): boolean;
/**
* Updates option item.
*
* Based on:
* https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js
*
* @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
* @param [selectedOption] Selected option.
*/
[PropertySymbol.updateSelectedness](selectedOption?: HTMLOptionElement): void;
}
//# sourceMappingURL=HTMLSelectElement.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"HTMLSelectElement.d.ts","sourceRoot":"","sources":["../../../src/nodes/html-select-element/HTMLSelectElement.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,644 @@
import HTMLElement from '../html-element/HTMLElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import ValidityState from '../../validity-state/ValidityState.js';
import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js';
import HTMLOptionsCollection from './HTMLOptionsCollection.js';
import Event from '../../event/Event.js';
import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js';
import HTMLCollection from '../element/HTMLCollection.js';
import NodeTypeEnum from '../node/NodeTypeEnum.js';
import QuerySelector from '../../query-selector/QuerySelector.js';
import ClassMethodBinder from '../../utilities/ClassMethodBinder.js';
import Node from '../node/Node.js';
import Element from '../element/Element.js';
import EventTarget from '../../event/EventTarget.js';
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
/**
* HTML Select Element.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement.
*/
export default class HTMLSelectElement extends HTMLElement {
// Internal properties.
[PropertySymbol.validationMessage] = '';
[PropertySymbol.validity] = new ValidityState(this);
[PropertySymbol.options] = null;
[PropertySymbol.selectedOptions] = null;
[PropertySymbol.selectedIndex] = -1;
[PropertySymbol.proxy];
/**
* Constructor.
*/
constructor() {
super();
const methodBinder = new ClassMethodBinder(this, [
HTMLSelectElement,
HTMLElement,
Element,
Node,
EventTarget
]);
const proxy = new Proxy(this, {
get: (target, property) => {
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
if (!isNaN(index)) {
return QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index];
}
},
set(target, property, newValue) {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
}
const index = Number(property);
if (isNaN(index)) {
target[property] = newValue;
return true;
}
if (!newValue || !(newValue instanceof HTMLOptionElement)) {
throw new this[PropertySymbol.window].Error(`TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.`);
}
const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
const childNodes = target[PropertySymbol.nodeArray];
while (childNodes.length) {
target[PropertySymbol.removeChild](childNodes[0]);
}
for (let i = 0; i <= index; i++) {
if (i === index) {
target[PropertySymbol.appendChild](newValue);
}
else if (options[i]) {
target[PropertySymbol.appendChild](options[i]);
}
else {
target[PropertySymbol.appendChild](target[PropertySymbol.ownerDocument].createElement('option'));
}
}
return true;
},
deleteProperty(target, property) {
if (typeof property === 'symbol') {
delete target[property];
return true;
}
const index = Number(property);
if (isNaN(index)) {
delete target[property];
}
return true;
},
ownKeys(target) {
return Object.keys(QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]);
},
has(target, property) {
if (property in target) {
return true;
}
if (typeof property === 'symbol') {
return false;
}
const index = Number(property);
if (!isNaN(index)) {
return !!QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index];
}
return false;
},
defineProperty(target, property, descriptor) {
methodBinder.preventBinding(property);
const index = Number(property);
if (isNaN(index)) {
Object.defineProperty(target, property, descriptor);
return true;
}
if (!descriptor.value || !(descriptor.value instanceof HTMLOptionElement)) {
throw new this[PropertySymbol.window].Error(`TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.`);
}
const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
const childNodes = target[PropertySymbol.nodeArray];
while (childNodes.length) {
target[PropertySymbol.removeChild](childNodes[0]);
}
for (let i = 0; i <= index; i++) {
if (i === index) {
target[PropertySymbol.appendChild](descriptor.value);
}
else if (options[i]) {
target[PropertySymbol.appendChild](options[i]);
}
else {
target[PropertySymbol.appendChild](target[PropertySymbol.ownerDocument].createElement('option'));
}
}
return true;
},
getOwnPropertyDescriptor(target, property) {
if (property in target) {
return Object.getOwnPropertyDescriptor(target, property);
}
const index = Number(property);
if (isNaN(index)) {
return;
}
const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
if (!options[index]) {
return;
}
return {
value: options[index],
writable: true,
enumerable: true,
configurable: true
};
}
});
this[PropertySymbol.proxy] = proxy;
this[PropertySymbol.selectNode] = proxy;
return proxy;
}
// Events
/* eslint-disable jsdoc/require-jsdoc */
get onchange() {
return ElementEventAttributeUtility.getEventListener(this, 'onchange');
}
set onchange(value) {
this[PropertySymbol.propertyEventListeners].set('onchange', value);
}
get oninput() {
return ElementEventAttributeUtility.getEventListener(this, 'oninput');
}
set oninput(value) {
this[PropertySymbol.propertyEventListeners].set('oninput', value);
}
/* eslint-enable jsdoc/require-jsdoc */
/**
* Returns length.
*
* @returns Length.
*/
get length() {
return QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items].length;
}
/**
* Returns options.
*
* @returns Options.
*/
get options() {
if (!this[PropertySymbol.options]) {
this[PropertySymbol.options] = new HTMLOptionsCollection(PropertySymbol.illegalConstructor, this);
}
return this[PropertySymbol.options];
}
/**
* Returns validation message.
*
* @returns Validation message.
*/
get validationMessage() {
return this[PropertySymbol.validationMessage];
}
/**
* Returns validity.
*
* @returns Validity.
*/
get validity() {
return this[PropertySymbol.validity];
}
/**
* Returns name.
*
* @returns Name.
*/
get name() {
return this.getAttribute('name') || '';
}
/**
* Sets name.
*
* @param name Name.
*/
set name(name) {
this.setAttribute('name', name);
}
/**
* 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 multiple.
*
* @returns Multiple.
*/
get multiple() {
return this.getAttribute('multiple') !== null;
}
/**
* Sets multiple.
*
* @param multiple Multiple.
*/
set multiple(multiple) {
if (!multiple) {
this.removeAttribute('multiple');
}
else {
this.setAttribute('multiple', '');
}
}
/**
* 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 type.
*
* @returns type.
*/
get type() {
return this.hasAttributeNS(null, 'multiple') ? 'select-multiple' : 'select-one';
}
/**
* Returns value.
*
* @returns Value.
*/
get value() {
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
for (let i = 0, max = options.length; i < max; i++) {
const option = options[i];
if (option[PropertySymbol.selectedness]) {
return option.value;
}
}
return '';
}
/**
* Sets value.
*
* @param value Value.
*/
set value(value) {
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
this[PropertySymbol.selectedIndex] = -1;
for (let i = 0, max = options.length; i < max; i++) {
const option = options[i];
if (option.value === value) {
option[PropertySymbol.selectedness] = true;
option[PropertySymbol.dirtyness] = true;
this[PropertySymbol.selectedIndex] = i;
}
else {
option[PropertySymbol.selectedness] = false;
}
}
}
/**
* Returns value.
*
* @returns Value.
*/
get selectedIndex() {
return this[PropertySymbol.selectedIndex];
}
/**
* Sets value.
*
* @param selectedIndex Selected index.
*/
set selectedIndex(selectedIndex) {
selectedIndex = Number(selectedIndex);
if (isNaN(selectedIndex)) {
return;
}
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
this[PropertySymbol.selectedIndex] = -1;
if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) {
for (let i = 0, max = options.length; i < max; i++) {
options[i][PropertySymbol.selectedness] = false;
}
const selectedOption = options[selectedIndex];
if (selectedOption) {
selectedOption[PropertySymbol.selectedness] = true;
selectedOption[PropertySymbol.dirtyness] = true;
this[PropertySymbol.selectedIndex] = selectedIndex;
}
}
}
/**
* Returns selected options.
*
* @returns HTMLCollection.
*/
get selectedOptions() {
if (!this[PropertySymbol.selectedOptions]) {
this[PropertySymbol.selectedOptions] = new HTMLCollection(PropertySymbol.illegalConstructor, () => {
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
// If we hit the cache, we should recieve the same Array instance as before, which will then contain the selected options.
if (options[PropertySymbol.selectedOptions]) {
return options[PropertySymbol.selectedOptions];
}
const selectedOptions = [];
for (let i = 0, max = options.length; i < max; i++) {
const option = options[i];
if (option[PropertySymbol.selectedness]) {
selectedOptions.push(option);
}
}
options[PropertySymbol.selectedOptions] = selectedOptions;
return selectedOptions;
});
}
return this[PropertySymbol.selectedOptions];
}
/**
* Returns the associated label elements.
*
* @returns Label elements.
*/
get labels() {
return HTMLLabelElementUtility.getAssociatedLabelElements(this);
}
/**
* 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 "true" if it will validate.
*
* @returns "true" if it will validate.
*/
get willValidate() {
return (this.type !== 'hidden' &&
this.type !== 'reset' &&
this.type !== 'button' &&
!this.disabled &&
!this['readOnly']);
}
/**
* @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;
}
/**
* Returns item from options collection by index.
*
* @param index Index.
*/
item(index) {
return this[PropertySymbol.options].item(index);
}
/**
* Adds new option to options collection.
*
* @param element HTMLOptionElement to add.
* @param before HTMLOptionElement or index number.
*/
add(element, before) {
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
if (!before && before !== 0) {
const childNodes = this[PropertySymbol.nodeArray];
while (childNodes.length) {
this[PropertySymbol.removeChild](childNodes[0]);
}
for (const option of options) {
this[PropertySymbol.appendChild](option);
}
this[PropertySymbol.appendChild](element);
return;
}
const window = this[PropertySymbol.window];
if (typeof before !== 'number') {
if (!(before instanceof HTMLOptionElement)) {
throw new window.DOMException("Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not an 'HTMLOptionElement'.");
}
before = options.indexOf(before);
}
const optionsElement = options[before];
if (!optionsElement) {
throw new window.DOMException("Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not a child of this node.");
}
const childNodes = this[PropertySymbol.nodeArray];
while (childNodes.length) {
this[PropertySymbol.removeChild](childNodes[0]);
}
for (let i = 0, max = options.length; i < max; i++) {
if (i === before) {
this[PropertySymbol.appendChild](element);
}
this[PropertySymbol.appendChild](options[i]);
}
}
/**
* Removes indexed element from collection or the select element.
*
* @param [index] Index.
*/
remove(index) {
if (typeof index === 'number') {
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
if (!options[index]) {
return;
}
const childNodes = this[PropertySymbol.nodeArray];
while (childNodes.length) {
this[PropertySymbol.removeChild](childNodes[0]);
}
for (let i = 0, max = options.length; i < max; i++) {
if (i !== index) {
this[PropertySymbol.appendChild](options[i]);
}
}
}
else {
super.remove();
}
}
/**
* 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[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();
}
/**
* Updates option item.
*
* Based on:
* https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js
*
* @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
* @param [selectedOption] Selected option.
*/
[PropertySymbol.updateSelectedness](selectedOption) {
const isMultiple = this.hasAttribute('multiple');
const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
const selected = [];
if (selectedOption) {
this[PropertySymbol.selectedIndex] = -1;
}
if (!isMultiple) {
for (let i = 0, max = options.length; i < max; i++) {
const option = options[i];
if (selectedOption) {
option[PropertySymbol.selectedness] = option === selectedOption;
if (option === selectedOption) {
this[PropertySymbol.selectedIndex] = i;
}
}
if (option[PropertySymbol.selectedness]) {
selected.push(option);
}
}
}
const size = this.#getDisplaySize();
if (size === 1 && !selected.length) {
this[PropertySymbol.selectedIndex] = -1;
for (let i = 0, max = options.length; i < max; i++) {
const option = options[i];
const parentNode = option[PropertySymbol.parentNode];
let disabled = option.hasAttributeNS(null, 'disabled');
if (parentNode &&
parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode &&
parentNode[PropertySymbol.tagName] === 'OPTGROUP' &&
parentNode.hasAttributeNS(null, 'disabled')) {
disabled = true;
}
if (!disabled) {
option[PropertySymbol.selectedness] = true;
this[PropertySymbol.selectedIndex] = i;
break;
}
}
}
else if (selected.length >= 2) {
this[PropertySymbol.selectedIndex] = -1;
for (let i = 0, max = options.length; i < max; i++) {
options[i][PropertySymbol.selectedness] = i === selected.length - 1;
if (i === selected.length - 1) {
this[PropertySymbol.selectedIndex] = i;
}
}
}
}
/**
* Returns display size.
*
* @returns Display size.
*/
#getDisplaySize() {
if (this.hasAttributeNS(null, 'size')) {
const size = parseInt(this.getAttribute('size'));
if (!isNaN(size) && size >= 0) {
return size;
}
}
return this.hasAttributeNS(null, 'multiple') ? 4 : 1;
}
}
//# sourceMappingURL=HTMLSelectElement.js.map

File diff suppressed because one or more lines are too long