565 lines
18 KiB
JavaScript
565 lines
18 KiB
JavaScript
|
|
import HTMLElement from '../html-element/HTMLElement.js';
|
||
|
|
import * as PropertySymbol from '../../PropertySymbol.js';
|
||
|
|
import Event from '../../event/Event.js';
|
||
|
|
import SubmitEvent from '../../event/events/SubmitEvent.js';
|
||
|
|
import HTMLFormControlsCollection from './HTMLFormControlsCollection.js';
|
||
|
|
import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator.js';
|
||
|
|
import QuerySelector from '../../query-selector/QuerySelector.js';
|
||
|
|
import RadioNodeList from './RadioNodeList.js';
|
||
|
|
import WindowBrowserContext from '../../window/WindowBrowserContext.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 Form Element.
|
||
|
|
*
|
||
|
|
* Reference:
|
||
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement.
|
||
|
|
*/
|
||
|
|
export default class HTMLFormElement extends HTMLElement {
|
||
|
|
// Internal properties.
|
||
|
|
[PropertySymbol.elements] = null;
|
||
|
|
[PropertySymbol.proxy];
|
||
|
|
/* eslint-enable jsdoc/require-jsdoc */
|
||
|
|
/**
|
||
|
|
* Constructor.
|
||
|
|
*/
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
const methodBinder = new ClassMethodBinder(this, [
|
||
|
|
HTMLFormElement,
|
||
|
|
HTMLElement,
|
||
|
|
Element,
|
||
|
|
Node,
|
||
|
|
EventTarget
|
||
|
|
]);
|
||
|
|
const proxy = new Proxy(this, {
|
||
|
|
get: (target, property) => {
|
||
|
|
if (property === 'length') {
|
||
|
|
return target[PropertySymbol.getFormControlItems]().length;
|
||
|
|
}
|
||
|
|
if (property in target || typeof property === 'symbol') {
|
||
|
|
methodBinder.bind(property);
|
||
|
|
return target[property];
|
||
|
|
}
|
||
|
|
const index = Number(property);
|
||
|
|
if (!isNaN(index)) {
|
||
|
|
return target[PropertySymbol.getFormControlItems]()[index];
|
||
|
|
}
|
||
|
|
return target[PropertySymbol.getFormControlNamedItem](property) || undefined;
|
||
|
|
},
|
||
|
|
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;
|
||
|
|
},
|
||
|
|
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(target[PropertySymbol.getFormControlItems]());
|
||
|
|
},
|
||
|
|
has(target, property) {
|
||
|
|
if (property in target) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (typeof property === 'symbol') {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const items = target[PropertySymbol.getFormControlItems]();
|
||
|
|
const index = Number(property);
|
||
|
|
if (!isNaN(index) && index >= 0 && index < items.length) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
property = String(property);
|
||
|
|
for (let i = 0; i < items.length; i++) {
|
||
|
|
const item = items[i];
|
||
|
|
const name = item.getAttribute('id') || item.getAttribute('name');
|
||
|
|
if (name && name === property) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
},
|
||
|
|
defineProperty(target, property, descriptor) {
|
||
|
|
methodBinder.preventBinding(property);
|
||
|
|
if (!descriptor.value) {
|
||
|
|
Object.defineProperty(target, property, descriptor);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const index = Number(descriptor.value);
|
||
|
|
if (isNaN(index)) {
|
||
|
|
Object.defineProperty(target, property, descriptor);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
},
|
||
|
|
getOwnPropertyDescriptor(target, property) {
|
||
|
|
if (property in target) {
|
||
|
|
return Object.getOwnPropertyDescriptor(target, property);
|
||
|
|
}
|
||
|
|
const items = target[PropertySymbol.getFormControlItems]();
|
||
|
|
const index = Number(property);
|
||
|
|
if (!isNaN(index) && index >= 0 && index < items.length) {
|
||
|
|
return {
|
||
|
|
value: items[index],
|
||
|
|
writable: false,
|
||
|
|
enumerable: true,
|
||
|
|
configurable: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
for (let i = 0; i < items.length; i++) {
|
||
|
|
const item = items[i];
|
||
|
|
const name = item.getAttribute('id') || item.getAttribute('name');
|
||
|
|
if (name && name === property) {
|
||
|
|
return {
|
||
|
|
value: item,
|
||
|
|
writable: false,
|
||
|
|
enumerable: true,
|
||
|
|
configurable: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this[PropertySymbol.proxy] = proxy;
|
||
|
|
this[PropertySymbol.formNode] = proxy;
|
||
|
|
return proxy;
|
||
|
|
}
|
||
|
|
// Events
|
||
|
|
/* eslint-disable jsdoc/require-jsdoc */
|
||
|
|
get onformdata() {
|
||
|
|
return ElementEventAttributeUtility.getEventListener(this, 'onformdata');
|
||
|
|
}
|
||
|
|
set onformdata(value) {
|
||
|
|
this[PropertySymbol.propertyEventListeners].set('onformdata', value);
|
||
|
|
}
|
||
|
|
get onreset() {
|
||
|
|
return ElementEventAttributeUtility.getEventListener(this, 'onreset');
|
||
|
|
}
|
||
|
|
set onreset(value) {
|
||
|
|
this[PropertySymbol.propertyEventListeners].set('onreset', value);
|
||
|
|
}
|
||
|
|
get onsubmit() {
|
||
|
|
return ElementEventAttributeUtility.getEventListener(this, 'onsubmit');
|
||
|
|
}
|
||
|
|
set onsubmit(value) {
|
||
|
|
this[PropertySymbol.propertyEventListeners].set('onsubmit', value);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns elements.
|
||
|
|
*
|
||
|
|
* @returns Elements.
|
||
|
|
*/
|
||
|
|
get elements() {
|
||
|
|
if (!this[PropertySymbol.elements]) {
|
||
|
|
this[PropertySymbol.elements] = new HTMLFormControlsCollection(PropertySymbol.illegalConstructor, this);
|
||
|
|
}
|
||
|
|
return this[PropertySymbol.elements];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns length.
|
||
|
|
*
|
||
|
|
* @returns Length.
|
||
|
|
*/
|
||
|
|
get length() {
|
||
|
|
return this[PropertySymbol.getFormControlItems]().length;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns name.
|
||
|
|
*
|
||
|
|
* @returns Name.
|
||
|
|
*/
|
||
|
|
get name() {
|
||
|
|
return this.getAttribute('name') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets name.
|
||
|
|
*
|
||
|
|
* @param name Name.
|
||
|
|
*/
|
||
|
|
set name(name) {
|
||
|
|
this.setAttribute('name', name);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns method.
|
||
|
|
*
|
||
|
|
* @returns Method.
|
||
|
|
*/
|
||
|
|
get method() {
|
||
|
|
return this.getAttribute('method') || 'get';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets method.
|
||
|
|
*
|
||
|
|
* @param method Method.
|
||
|
|
*/
|
||
|
|
set method(method) {
|
||
|
|
this.setAttribute('method', method);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns target.
|
||
|
|
*
|
||
|
|
* @returns Target.
|
||
|
|
*/
|
||
|
|
get target() {
|
||
|
|
return this.getAttribute('target') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets target.
|
||
|
|
*
|
||
|
|
* @param target Target.
|
||
|
|
*/
|
||
|
|
set target(target) {
|
||
|
|
this.setAttribute('target', target);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns action.
|
||
|
|
*
|
||
|
|
* @returns Action.
|
||
|
|
*/
|
||
|
|
get action() {
|
||
|
|
if (!this.hasAttribute('action')) {
|
||
|
|
return this[PropertySymbol.ownerDocument].location.href;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return new URL(this.getAttribute('action'), this[PropertySymbol.ownerDocument].location.href)
|
||
|
|
.href;
|
||
|
|
}
|
||
|
|
catch (e) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets action.
|
||
|
|
*
|
||
|
|
* @param action Action.
|
||
|
|
*/
|
||
|
|
set action(action) {
|
||
|
|
this.setAttribute('action', action);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns encoding.
|
||
|
|
*
|
||
|
|
* @returns Encoding.
|
||
|
|
*/
|
||
|
|
get encoding() {
|
||
|
|
return this.getAttribute('encoding') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets encoding.
|
||
|
|
*
|
||
|
|
* @param encoding Encoding.
|
||
|
|
*/
|
||
|
|
set encoding(encoding) {
|
||
|
|
this.setAttribute('encoding', encoding);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns enctype.
|
||
|
|
*
|
||
|
|
* @returns Enctype.
|
||
|
|
*/
|
||
|
|
get enctype() {
|
||
|
|
return this.getAttribute('enctype') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets enctype.
|
||
|
|
*
|
||
|
|
* @param enctype Enctype.
|
||
|
|
*/
|
||
|
|
set enctype(enctype) {
|
||
|
|
this.setAttribute('enctype', enctype);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns autocomplete.
|
||
|
|
*
|
||
|
|
* @returns Autocomplete.
|
||
|
|
*/
|
||
|
|
get autocomplete() {
|
||
|
|
return this.getAttribute('autocomplete') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets autocomplete.
|
||
|
|
*
|
||
|
|
* @param autocomplete Autocomplete.
|
||
|
|
*/
|
||
|
|
set autocomplete(autocomplete) {
|
||
|
|
this.setAttribute('autocomplete', autocomplete);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns accept charset.
|
||
|
|
*
|
||
|
|
* @returns Accept charset.
|
||
|
|
*/
|
||
|
|
get acceptCharset() {
|
||
|
|
return this.getAttribute('acceptcharset') || '';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets accept charset.
|
||
|
|
*
|
||
|
|
* @param acceptCharset Accept charset.
|
||
|
|
*/
|
||
|
|
set acceptCharset(acceptCharset) {
|
||
|
|
this.setAttribute('acceptcharset', acceptCharset);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns no validate.
|
||
|
|
*
|
||
|
|
* @returns No validate.
|
||
|
|
*/
|
||
|
|
get noValidate() {
|
||
|
|
return this.getAttribute('novalidate') !== null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sets no validate.
|
||
|
|
*
|
||
|
|
* @param noValidate No validate.
|
||
|
|
*/
|
||
|
|
set noValidate(noValidate) {
|
||
|
|
if (!noValidate) {
|
||
|
|
this.removeAttribute('novalidate');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
this.setAttribute('novalidate', '');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Submits form. No submit event is raised. In particular, the form's "submit" event handler is not run.
|
||
|
|
*/
|
||
|
|
submit() {
|
||
|
|
this.#submit();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Submits form, reports validity and raises submit event.
|
||
|
|
*
|
||
|
|
* @param [submitter] Submitter.
|
||
|
|
*/
|
||
|
|
requestSubmit(submitter) {
|
||
|
|
const noValidate = submitter?.formNoValidate || this.noValidate;
|
||
|
|
if (noValidate || this.checkValidity()) {
|
||
|
|
const event = new SubmitEvent('submit', {
|
||
|
|
bubbles: true,
|
||
|
|
cancelable: true,
|
||
|
|
submitter: submitter || this[PropertySymbol.proxy]
|
||
|
|
});
|
||
|
|
this.dispatchEvent(event);
|
||
|
|
if (!event.defaultPrevented) {
|
||
|
|
this.#submit(submitter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resets form.
|
||
|
|
*/
|
||
|
|
reset() {
|
||
|
|
for (const element of this[PropertySymbol.getFormControlItems]()) {
|
||
|
|
switch (element[PropertySymbol.tagName]) {
|
||
|
|
case 'TEXTAREA':
|
||
|
|
element[PropertySymbol.value] = null;
|
||
|
|
break;
|
||
|
|
case 'INPUT':
|
||
|
|
element[PropertySymbol.value] = null;
|
||
|
|
element[PropertySymbol.checked] = null;
|
||
|
|
break;
|
||
|
|
case 'OUTPUT':
|
||
|
|
element.textContent = element[PropertySymbol.defaultValue];
|
||
|
|
break;
|
||
|
|
case 'SELECT':
|
||
|
|
let hasSelectedAttribute = false;
|
||
|
|
for (const option of element.options) {
|
||
|
|
if (option.hasAttribute('selected')) {
|
||
|
|
hasSelectedAttribute = true;
|
||
|
|
option.selected = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!hasSelectedAttribute && element.options.length > 0) {
|
||
|
|
element.options[0].selected = true;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.dispatchEvent(new Event('reset', { bubbles: true, cancelable: true }));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Checks validity.
|
||
|
|
*
|
||
|
|
* @returns "true" if validation does'nt fail.
|
||
|
|
*/
|
||
|
|
checkValidity() {
|
||
|
|
const radioValidationState = {};
|
||
|
|
let isFormValid = true;
|
||
|
|
for (const element of this[PropertySymbol.getFormControlItems]()) {
|
||
|
|
if (element[PropertySymbol.tagName] === 'INPUT' && element.type === 'radio' && element.name) {
|
||
|
|
if (!radioValidationState[element.name]) {
|
||
|
|
radioValidationState[element.name] = true;
|
||
|
|
if (!element.checkValidity()) {
|
||
|
|
isFormValid = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (!element.checkValidity()) {
|
||
|
|
isFormValid = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return isFormValid;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Reports validity.
|
||
|
|
*
|
||
|
|
* @returns "true" if validation does'nt fail.
|
||
|
|
*/
|
||
|
|
reportValidity() {
|
||
|
|
return this.checkValidity();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* @override
|
||
|
|
*/
|
||
|
|
[PropertySymbol.cloneNode](deep = false) {
|
||
|
|
return super[PropertySymbol.cloneNode](deep);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns form control items.
|
||
|
|
*
|
||
|
|
* @returns Form control items.
|
||
|
|
*/
|
||
|
|
[PropertySymbol.getFormControlItems]() {
|
||
|
|
const elements = (QuerySelector.querySelectorAll(this, 'input,select,textarea,button,fieldset,object,output')[PropertySymbol.items].slice());
|
||
|
|
if (this[PropertySymbol.isConnected]) {
|
||
|
|
const id = this.getAttribute('id');
|
||
|
|
if (id) {
|
||
|
|
for (const element of (QuerySelector.querySelectorAll(this[PropertySymbol.ownerDocument], `input[form="${id}"],select[form="${id}"],textarea[form="${id}"],button[form="${id}"],fieldset[form="${id}"],object[form="${id}"],output[form="${id}"]`)[PropertySymbol.items])) {
|
||
|
|
if (!elements.includes(element)) {
|
||
|
|
elements.push(element);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return elements;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns form control named item.
|
||
|
|
*
|
||
|
|
* @param name
|
||
|
|
* @returns Form control named item.
|
||
|
|
*/
|
||
|
|
[PropertySymbol.getFormControlNamedItem](name) {
|
||
|
|
const items = this[PropertySymbol.getFormControlItems]();
|
||
|
|
const namedItems = [];
|
||
|
|
name = String(name);
|
||
|
|
for (const item of items) {
|
||
|
|
if (item.getAttribute('id') === name || item.getAttribute('name') === name) {
|
||
|
|
namedItems.push(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!namedItems.length) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (namedItems.length === 1) {
|
||
|
|
return namedItems[0];
|
||
|
|
}
|
||
|
|
return new RadioNodeList(PropertySymbol.illegalConstructor, namedItems);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Submits form.
|
||
|
|
*
|
||
|
|
* @param browserFrame Browser frame. Injected by the constructor.
|
||
|
|
* @param [submitter] Submitter.
|
||
|
|
*/
|
||
|
|
#submit(submitter) {
|
||
|
|
const method = submitter?.formMethod || this.method;
|
||
|
|
if (method === 'dialog') {
|
||
|
|
let dialog = null;
|
||
|
|
let parent = this;
|
||
|
|
while (parent) {
|
||
|
|
if (parent[PropertySymbol.tagName] === 'DIALOG') {
|
||
|
|
dialog = parent;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
parent = parent.parentElement;
|
||
|
|
}
|
||
|
|
if (dialog) {
|
||
|
|
dialog.close(submitter?.value);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const action = submitter?.hasAttribute('formaction')
|
||
|
|
? submitter?.formAction || this.action
|
||
|
|
: this.action;
|
||
|
|
const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame();
|
||
|
|
if (!browserFrame) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!action) {
|
||
|
|
// The URL is invalid when the action is empty.
|
||
|
|
// This is what Chrome does when the URL is invalid.
|
||
|
|
this[PropertySymbol.ownerDocument].location.hash = '#blocked';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const formData = new this[PropertySymbol.window].FormData(this, submitter);
|
||
|
|
let targetFrame;
|
||
|
|
switch (submitter?.formTarget || this.target) {
|
||
|
|
default:
|
||
|
|
case '_self':
|
||
|
|
targetFrame = browserFrame;
|
||
|
|
break;
|
||
|
|
case '_top':
|
||
|
|
targetFrame = browserFrame.page.mainFrame;
|
||
|
|
break;
|
||
|
|
case '_parent':
|
||
|
|
targetFrame = browserFrame.parentFrame ?? browserFrame;
|
||
|
|
break;
|
||
|
|
case '_blank':
|
||
|
|
const newPage = browserFrame.page.context.newPage();
|
||
|
|
targetFrame = newPage.mainFrame;
|
||
|
|
targetFrame[PropertySymbol.openerFrame] = browserFrame;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (method === 'get') {
|
||
|
|
const url = new URL(action);
|
||
|
|
url.search = '';
|
||
|
|
for (const [key, value] of formData) {
|
||
|
|
if (typeof value === 'string') {
|
||
|
|
url.searchParams.append(key, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
BrowserFrameNavigator.navigate({
|
||
|
|
windowClass: (this[PropertySymbol.ownerDocument][PropertySymbol.defaultView]?.constructor),
|
||
|
|
frame: targetFrame,
|
||
|
|
url: url.href,
|
||
|
|
goToOptions: {
|
||
|
|
referrer: browserFrame.page.mainFrame.window.location.origin
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
BrowserFrameNavigator.navigate({
|
||
|
|
windowClass: (this[PropertySymbol.ownerDocument][PropertySymbol.defaultView]?.constructor),
|
||
|
|
frame: targetFrame,
|
||
|
|
method: method,
|
||
|
|
url: action,
|
||
|
|
formData,
|
||
|
|
goToOptions: {
|
||
|
|
referrer: browserFrame.page.mainFrame.window.location.origin
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=HTMLFormElement.js.map
|