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 '../../ClassMethodBinder.js'; import Node from '../node/Node.js'; import Element from '../element/Element.js'; import EventTarget from '../../event/EventTarget.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]; // Events onformdata = null; onreset = null; onsubmit = null; /** * 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[PropertySymbol.attributes][PropertySymbol.namedItems].get('id')?.[PropertySymbol.value] || item[PropertySymbol.attributes][PropertySymbol.namedItems].get('name')?.[PropertySymbol.value]; 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[PropertySymbol.attributes][PropertySymbol.namedItems].get('id')?.[PropertySymbol.value] || item[PropertySymbol.attributes][PropertySymbol.namedItems].get('name')?.[PropertySymbol.value]; if (name && name === property) { return { value: item, writable: false, enumerable: true, configurable: true }; } } } }); this[PropertySymbol.proxy] = proxy; this[PropertySymbol.formNode] = proxy; return proxy; } /** * 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[PropertySymbol.attributes][PropertySymbol.namedItems].get('id')?.[PropertySymbol.value]; 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[PropertySymbol.attributes][PropertySymbol.namedItems].get('id')?.[PropertySymbol.value] === name || item[PropertySymbol.attributes][PropertySymbol.namedItems].get('name')?.[PropertySymbol.value] === 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 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 method = submitter?.formMethod || this.method; const formData = new this[PropertySymbol.window].FormData(this); 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); 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