import Blob from '../file/Blob.js'; import * as PropertySymbol from '../PropertySymbol.js'; import File from '../file/File.js'; import type HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import type HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; import type BrowserWindow from '../window/BrowserWindow.js'; import type HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; type FormDataEntry = { name: string; value: string | File; }; /** * FormData. * * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData */ export default class FormData implements Iterable<[string, string | File]> { // Injected by WindowContextClassExtender protected declare [PropertySymbol.window]: BrowserWindow; #entries: FormDataEntry[] = []; /** * Constructor. * * @param [form] Form. * @param [submitter] The element that triggered the submission if this came from a form submit. */ constructor(form?: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement) { if (!form) { return; } if (submitter) { const formProxy = form[PropertySymbol.proxy] ? form[PropertySymbol.proxy] : form; if (submitter.form !== formProxy) { throw new this[PropertySymbol.window].DOMException( 'The specified element is not owned by this form element', DOMExceptionNameEnum.notFoundError ); } const isSubmitButton = (submitter[PropertySymbol.tagName] === 'INPUT' && (submitter.type === 'submit' || submitter.type === 'image')) || (submitter[PropertySymbol.tagName] === 'BUTTON' && submitter.type === 'submit'); if (!isSubmitButton) { throw new this[PropertySymbol.window].TypeError( 'The specified element is not a submit button' ); } } const items = form[PropertySymbol.getFormControlItems](); for (const item of items) { const name = item.name; if (name) { switch (item[PropertySymbol.tagName]) { case 'INPUT': if ((item).disabled) { break; } switch ((item).type) { case 'file': if ((item)[PropertySymbol.files].length === 0) { this.append(name, new File([], '', { type: 'application/octet-stream' })); } else { for (const file of (item)[PropertySymbol.files]) { this.append(name, file); } } break; case 'checkbox': case 'radio': if ((item).checked) { this.append(name, (item).value); } break; case 'submit': case 'reset': case 'button': if (item === submitter && (item).value) { this.append(name, (item).value); } break; default: this.append(name, (item).value); break; } break; case 'BUTTON': if (item === submitter && (item).value) { this.append(name, (item).value); } break; case 'TEXTAREA': case 'SELECT': this.append(name, (item).value); break; } } } } /** * For each. * * @param callback Callback. * @param thisArg thisArg. */ public forEach( callback: (value: string | File, key: string, parent: this) => void, thisArg?: any ): void { for (const entry of this.#entries) { callback.call(thisArg, entry.value, entry.name, this); } } /** * Appends a new value onto an existing key. * * @param name Name. * @param value Value. * @param [filename] Filename. */ public append(name: string, value: string | Blob | File, filename?: string): void { if (filename && !(value instanceof Blob)) { throw new this[PropertySymbol.window].TypeError( 'Failed to execute "append" on "FormData": parameter 2 is not of type "Blob".' ); } this.#entries.push({ name, value: this.#parseValue(value, filename) }); } /** * Removes a value. * * @param name Name. */ public delete(name: string): void { const newEntries: FormDataEntry[] = []; for (const entry of this.#entries) { if (entry.name !== name) { newEntries.push(entry); } } this.#entries = newEntries; } /** * Returns value. * * @param name Name. * @returns Value. */ public get(name: string): string | File | null { for (const entry of this.#entries) { if (entry.name === name) { return entry.value; } } return null; } /** * Returns all values associated with the given name. * * @param name Name. * @returns Values. */ public getAll(name: string): Array { const values: Array = []; for (const entry of this.#entries) { if (entry.name === name) { values.push(entry.value); } } return values; } /** * Returns whether a FormData object contains a certain key. * * @param name Name. * @returns "true" if the FormData object contains the key. */ public has(name: string): boolean { for (const entry of this.#entries) { if (entry.name === name) { return true; } } return false; } /** * Sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist. * * @param name Name. * @param value Value. * @param [filename] Filename. */ public set(name: string, value: string | Blob | File, filename?: string): void { for (const entry of this.#entries) { if (entry.name === name) { entry.value = this.#parseValue(value, filename); return; } } this.append(name, value); } /** * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object. * * @returns Iterator. */ public *keys(): ArrayIterator { for (const entry of this.#entries) { yield entry.name; } } /** * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * * @returns Iterator. */ public *values(): ArrayIterator { for (const entry of this.#entries) { yield entry.value; } } /** * Returns an iterator, allowing you to go through all key/value pairs contained in this object. * * @returns Iterator. */ public *entries(): ArrayIterator<[string, string | File]> { for (const entry of this.#entries) { yield [entry.name, entry.value]; } } /** * Iterator. * * @returns Iterator. */ public *[Symbol.iterator](): ArrayIterator<[string, string | File]> { for (const entry of this.#entries) { yield [entry.name, entry.value]; } } /** * Parses a value. * * @param value Value. * @param [filename] Filename. * @returns Parsed value. */ #parseValue(value: string | Blob | File, filename?: string): string | File { if (value instanceof File) { if (filename) { const file = new File([], filename, { type: value.type, lastModified: value.lastModified }); file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; return file; } return value; } if (value instanceof Blob) { const file = new File([], 'blob', { type: value.type }); file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; return file; } return String(value); } }