"use strict"; const HTMLElementImpl = require("./HTMLElement-impl").implementation; const idlUtils = require("../generated/utils"); const Event = require("../generated/Event"); const DOMException = require("../../web-idl/DOMException"); const internalConstants = require("../helpers/internal-constants"); const domSymbolTree = internalConstants.domSymbolTree; const cloningSteps = internalConstants.cloningSteps; const closest = require("../helpers/traversal").closest; const isDisabled = require("../helpers/form-controls").isDisabled; const filesSymbol = Symbol("files"); const selectAllowedTypes = new Set(["text", "search", "tel", "url", "password", "email", "date", "month", "week", "time", "datetime-local", "color", "file", "number"]); const variableLengthSelectionAllowedTypes = new Set(["text", "search", "tel", "url", "password"]); function allowSelect(type) { return selectAllowedTypes.has(type.toLowerCase()); } function allowVariableLengthSelection(type) { return variableLengthSelectionAllowedTypes.has(type.toLowerCase()); } class HTMLInputElementImpl extends HTMLElementImpl { constructor(args, privateData) { super(args, privateData); if (!this.type) { this.type = "text"; } this._selectionStart = this._selectionEnd = 0; this._selectionDirection = "none"; this._value = null; this._dirtyValue = false; this._checkedness = false; this._dirtyCheckedness = false; // This is used to implement the canceled activation steps for radio inputs: // "The canceled activation steps consist of setting the checkedness and the element's indeterminate IDL // attribute back to the values they had before the pre-click activation steps were run." this._preCancelState = null; } _getValue() { return this._value; } _preClickActivationSteps() { if (this.type === "checkbox") { this.checked = !this.checked; } else if (this.type === "radio") { this._preCancelState = this.checked; this.checked = true; } } _canceledActivationSteps() { if (this.type === "checkbox") { this.checked = !this.checked; } else if (this.type === "radio") { if (this._preCancelState !== null) { this.checked = this._preCancelState; this._preCancelState = null; } } } _activationBehavior() { if (isDisabled(this)) { return; } if (this.type === "checkbox") { const inputEvent = Event.createImpl(["input", { bubbles: true, cancelable: true }], {}); this.dispatchEvent(inputEvent); const changeEvent = Event.createImpl(["change", { bubbles: true, cancelable: true }], {}); this.dispatchEvent(changeEvent); } else if (this.type === "submit") { const form = this.form; if (form) { form._dispatchSubmitEvent(); } } } _attrModified(name) { const wrapper = idlUtils.wrapperForImpl(this); if (!this._dirtyValue && name === "value") { this._value = wrapper.defaultValue; } if (!this._dirtyCheckedness && name === "checked") { this._checkedness = wrapper.defaultChecked; if (this._checkedness) { this._removeOtherRadioCheckedness(); } } if (name === "name" || name === "type") { if (this._checkedness) { this._removeOtherRadioCheckedness(); } } super._attrModified.apply(this, arguments); } _formReset() { const wrapper = idlUtils.wrapperForImpl(this); this._value = wrapper.defaultValue; this._dirtyValue = false; this._checkedness = wrapper.defaultChecked; this._dirtyCheckedness = false; if (this._checkedness) { this._removeOtherRadioCheckedness(); } } _changedFormOwner() { if (this._checkedness) { this._removeOtherRadioCheckedness(); } } _removeOtherRadioCheckedness() { const wrapper = idlUtils.wrapperForImpl(this); const root = this._radioButtonGroupRoot; if (!root) { return; } const name = wrapper.name.toLowerCase(); const descendants = domSymbolTree.treeIterator(root); for (const candidate of descendants) { if (candidate._radioButtonGroupRoot !== root) { continue; } const candidateWrapper = idlUtils.wrapperForImpl(candidate); if (!candidateWrapper.name || candidateWrapper.name.toLowerCase() !== name) { continue; } if (candidate !== this) { candidate._checkedness = false; } } } get _radioButtonGroupRoot() { const wrapper = idlUtils.wrapperForImpl(this); if (this.type !== "radio" || !wrapper.name) { return null; } let e = domSymbolTree.parent(this); while (e) { // root node of this home sub tree // or the form element we belong to if (!domSymbolTree.parent(e) || e.nodeName.toUpperCase() === "FORM") { return e; } e = domSymbolTree.parent(e); } return null; } get form() { return closest(this, "form"); } get checked() { return this._checkedness; } set checked(checked) { this._checkedness = Boolean(checked); this._dirtyCheckedness = true; if (this._checkedness) { this._removeOtherRadioCheckedness(); } } get value() { if (this._value === null) { return ""; } return this._value; } set value(val) { this._dirtyValue = true; if (val === null) { this._value = null; } else { this._value = String(val); } this._selectionStart = 0; this._selectionEnd = 0; this._selectionDirection = "none"; } get files() { if (this.type === "file") { this[filesSymbol] = this[filesSymbol] || new this._core.FileList(); } else { this[filesSymbol] = null; } return this[filesSymbol]; } get type() { const type = this.getAttribute("type"); return type ? type.toLowerCase() : "text"; } set type(type) { this.setAttribute("type", type); } _dispatchSelectEvent() { const event = this._ownerDocument.createEvent("HTMLEvents"); event.initEvent("select", true, true); this.dispatchEvent(event); } _getValueLength() { return typeof this.value === "string" ? this.value.length : 0; } select() { if (!allowSelect(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } this._selectionStart = 0; this._selectionEnd = this._getValueLength(); this._selectionDirection = "none"; this._dispatchSelectEvent(); } get selectionStart() { if (!allowVariableLengthSelection(this.type)) { return null; } return this._selectionStart; } set selectionStart(start) { if (!allowVariableLengthSelection(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } this.setSelectionRange(start, Math.max(start, this._selectionEnd), this._selectionDirection); } get selectionEnd() { if (!allowVariableLengthSelection(this.type)) { return null; } return this._selectionEnd; } set selectionEnd(end) { if (!allowVariableLengthSelection(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } this.setSelectionRange(this._selectionStart, end, this._selectionDirection); } get selectionDirection() { if (!allowVariableLengthSelection(this.type)) { return null; } return this._selectionDirection; } set selectionDirection(dir) { if (!allowVariableLengthSelection(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } this.setSelectionRange(this._selectionStart, this._selectionEnd, dir); } setSelectionRange(start, end, dir) { if (!allowVariableLengthSelection(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } this._selectionEnd = Math.min(end, this._getValueLength()); this._selectionStart = Math.min(start, this._selectionEnd); this._selectionDirection = dir === "forward" || dir === "backward" ? dir : "none"; this._dispatchSelectEvent(); } setRangeText(repl, start, end, selectionMode) { if (!allowVariableLengthSelection(this.type)) { throw new DOMException(DOMException.INVALID_STATE_ERR); } if (arguments.length < 2) { start = this._selectionStart; end = this._selectionEnd; } else if (start > end) { throw new DOMException(DOMException.INDEX_SIZE_ERR); } start = Math.min(start, this._getValueLength()); end = Math.min(end, this._getValueLength()); const val = this.value; let selStart = this._selectionStart; let selEnd = this._selectionEnd; this.value = val.slice(0, start) + repl + val.slice(end); const newEnd = start + this.value.length; if (selectionMode === "select") { this.setSelectionRange(start, newEnd); } else if (selectionMode === "start") { this.setSelectionRange(start, start); } else if (selectionMode === "end") { this.setSelectionRange(newEnd, newEnd); } else { // preserve const delta = repl.length - (end - start); if (selStart > end) { selStart += delta; } else if (selStart > start) { selStart = start; } if (selEnd > end) { selEnd += delta; } else if (selEnd > start) { selEnd = newEnd; } this.setSelectionRange(selStart, selEnd); } } set maxLength(value) { if (value < 0) { throw new DOMException(DOMException.INDEX_SIZE_ERR); } this.setAttribute("maxlength", String(value)); } get maxLength() { if (!this.hasAttribute("maxlength")) { return 524288; // stole this from chrome } return parseInt(this.getAttribute("maxlength")); } set minLength(value) { if (value < 0) { throw new DOMException(DOMException.INDEX_SIZE_ERR); } this.setAttribute("minlength", String(value)); } get minLength() { if (!this.hasAttribute("minlength")) { return 0; } return parseInt(this.getAttribute("minlength")); } get size() { if (!this.hasAttribute("size")) { return 20; } return parseInt(this.getAttribute("size")); } set size(value) { if (value <= 0) { throw new DOMException(DOMException.INDEX_SIZE_ERR); } this.setAttribute("size", String(value)); } [cloningSteps](copy, node) { copy._value = node._value; copy._checkedness = node._checkedness; copy._dirtyValue = node._dirtyValue; copy._dirtyCheckedness = node._dirtyCheckedness; } } module.exports = { implementation: HTMLInputElementImpl };