| "use strict"; |
| const DOMException = require("domexception/webidl2js-wrapper"); |
| const FileList = require("../generated/FileList"); |
| const Decimal = require("decimal.js"); |
| const HTMLElementImpl = require("./HTMLElement-impl").implementation; |
| const idlUtils = require("../generated/utils"); |
| const DefaultConstraintValidationImpl = |
| require("../constraint-validation/DefaultConstraintValidation-impl").implementation; |
| const ValidityState = require("../generated/ValidityState"); |
| const { mixin } = require("../../utils"); |
| const { domSymbolTree, cloningSteps } = require("../helpers/internal-constants"); |
| const { getLabelsForLabelable, formOwner } = require("../helpers/form-controls"); |
| const { fireAnEvent } = require("../helpers/events"); |
| const { |
| isDisabled, |
| isValidEmailAddress, |
| isValidAbsoluteURL, |
| sanitizeValueByType |
| } = require("../helpers/form-controls"); |
| const { |
| asciiCaseInsensitiveMatch, |
| asciiLowercase, |
| parseFloatingPointNumber, |
| splitOnCommas |
| } = require("../helpers/strings"); |
| const { isDate } = require("../helpers/dates-and-times"); |
| const { |
| convertStringToNumberByType, |
| convertStringToDateByType, |
| serializeDateByType, |
| convertNumberToStringByType |
| } = require("../helpers/number-and-date-inputs"); |
| |
| const filesSymbol = Symbol("files"); |
| |
| // https://html.spec.whatwg.org/multipage/input.html#attr-input-type |
| const inputAllowedTypes = new Set([ |
| "hidden", "text", "search", "tel", "url", "email", "password", "date", |
| "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", |
| "file", "submit", "image", "reset", "button" |
| ]); |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-apply |
| |
| const variableLengthSelectionAllowedTypes = new Set(["text", "search", "url", "tel", "password"]); |
| const numericTypes = new Set(["date", "month", "week", "time", "datetime-local", "number", "range"]); |
| |
| const applicableTypesForIDLMember = { |
| valueAsDate: new Set(["date", "month", "week", "time"]), |
| valueAsNumber: numericTypes, |
| |
| select: new Set([ |
| "text", "search", "url", "tel", "email", "password", "date", "month", "week", |
| "time", "datetime-local", "number", "color", "file" |
| ]), |
| selectionStart: variableLengthSelectionAllowedTypes, |
| selectionEnd: variableLengthSelectionAllowedTypes, |
| selectionDirection: variableLengthSelectionAllowedTypes, |
| setRangeText: variableLengthSelectionAllowedTypes, |
| setSelectionRange: variableLengthSelectionAllowedTypes, |
| stepDown: numericTypes, |
| stepUp: numericTypes |
| }; |
| |
| const lengthPatternSizeTypes = new Set(["text", "search", "url", "tel", "email", "password"]); |
| const readonlyTypes = |
| new Set([...lengthPatternSizeTypes, "date", "month", "week", "time", "datetime-local", "number"]); |
| |
| const applicableTypesForContentAttribute = { |
| list: new Set(["text", "search", "url", "tel", "email", ...numericTypes, "color"]), |
| max: numericTypes, |
| maxlength: lengthPatternSizeTypes, |
| min: numericTypes, |
| minlength: lengthPatternSizeTypes, |
| multiple: new Set(["email", "file"]), |
| pattern: lengthPatternSizeTypes, |
| readonly: readonlyTypes, |
| required: new Set([...readonlyTypes, "checkbox", "radio", "file"]), |
| step: numericTypes |
| }; |
| |
| const valueAttributeDefaultMode = new Set(["hidden", "submit", "image", "reset", "button"]); |
| const valueAttributeDefaultOnMode = new Set(["checkbox", "radio"]); |
| |
| function valueAttributeMode(type) { |
| if (valueAttributeDefaultMode.has(type)) { |
| return "default"; |
| } |
| if (valueAttributeDefaultOnMode.has(type)) { |
| return "default/on"; |
| } |
| if (type === "file") { |
| return "filename"; |
| } |
| return "value"; |
| } |
| |
| function getTypeFromAttribute(typeAttribute) { |
| if (typeof typeAttribute !== "string") { |
| return "text"; |
| } |
| const type = asciiLowercase(typeAttribute); |
| return inputAllowedTypes.has(type) ? type : "text"; |
| } |
| |
| class HTMLInputElementImpl extends HTMLElementImpl { |
| constructor(globalObject, args, privateData) { |
| super(globalObject, args, privateData); |
| |
| this._selectionStart = this._selectionEnd = 0; |
| this._selectionDirection = "none"; |
| this._value = ""; |
| this._dirtyValue = false; |
| this._checkedness = false; |
| this._dirtyCheckedness = false; |
| |
| this._preCheckedRadioState = null; |
| |
| this.indeterminate = false; |
| |
| this._customValidityErrorMessage = ""; |
| |
| this._labels = null; |
| |
| this._hasActivationBehavior = true; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-number |
| get _convertStringToNumber() { |
| return convertStringToNumberByType[this.type]; |
| } |
| |
| get _convertNumberToString() { |
| return convertNumberToStringByType[this.type]; |
| } |
| |
| get _convertDateToString() { |
| return serializeDateByType[this.type]; |
| } |
| |
| get _convertStringToDate() { |
| return convertStringToDateByType[this.type]; |
| } |
| |
| _isStepAligned(v) { |
| return new Decimal(v).minus(this._stepBase) |
| .modulo(this._allowedValueStep) |
| .isZero(); |
| } |
| |
| // Returns a Decimal. |
| _stepAlign(v, roundUp) { |
| const allowedValueStep = this._allowedValueStep; |
| const stepBase = this._stepBase; |
| |
| return new Decimal(v).minus(stepBase) |
| .toNearest(allowedValueStep, roundUp ? Decimal.ROUND_UP : Decimal.ROUND_DOWN) |
| .add(stepBase); |
| } |
| |
| // For <input>, https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-value |
| // is a simple value that is gotten and set, not computed. |
| _getValue() { |
| return this._value; |
| } |
| |
| _legacyPreActivationBehavior() { |
| // The spec says we should check this._mutable here, but browsers don't seem to implement this behavior. See |
| // https://github.com/whatwg/html/issues/3239. |
| if (this.type === "checkbox") { |
| this.checked = !this.checked; |
| } else if (this.type === "radio") { |
| this._preCheckedRadioState = this.checked; |
| this.checked = true; |
| } |
| } |
| |
| _legacyCanceledActivationBehavior() { |
| // The spec says we should check this._mutable here, but browsers don't seem to implement this behavior. See |
| // https://github.com/whatwg/html/issues/3239. |
| if (this.type === "checkbox") { |
| this.checked = !this.checked; |
| } else if (this.type === "radio") { |
| if (this._preCheckedRadioState !== null) { |
| this.checked = this._preCheckedRadioState; |
| this._preCheckedRadioState = null; |
| } |
| } |
| } |
| |
| _activationBehavior() { |
| if (!this._mutable) { |
| return; |
| } |
| |
| const { form } = this; |
| |
| if (this.type === "checkbox" || (this.type === "radio" && !this._preCheckedRadioState)) { |
| fireAnEvent("input", this, undefined, { bubbles: true }); |
| fireAnEvent("change", this, undefined, { bubbles: true }); |
| } else if (form && this.type === "submit") { |
| form._doSubmit(); |
| } else if (form && this.type === "reset") { |
| form._doReset(); |
| } |
| } |
| |
| _attrModified(name, value, oldVal) { |
| const wrapper = idlUtils.wrapperForImpl(this); |
| if (!this._dirtyValue && name === "value") { |
| this._value = sanitizeValueByType(this, 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(); |
| } |
| } |
| |
| if (name === "type") { |
| const prevType = getTypeFromAttribute(oldVal); |
| const curType = getTypeFromAttribute(value); |
| // When an input element's type attribute changes state… |
| if (prevType !== curType) { |
| const prevValueMode = valueAttributeMode(prevType); |
| const curValueMode = valueAttributeMode(curType); |
| if (prevValueMode === "value" && this._value !== "" && |
| (curValueMode === "default" || curValueMode === "default/on")) { |
| this.setAttributeNS(null, "value", this._value); |
| } else if (prevValueMode !== "value" && curValueMode === "value") { |
| this._value = this.getAttributeNS(null, "value") || ""; |
| this._dirtyValue = false; |
| } else if (prevValueMode !== "filename" && curValueMode === "filename") { |
| this._value = ""; |
| } |
| |
| this._signalATypeChange(); |
| |
| this._value = sanitizeValueByType(this, this._value); |
| |
| const previouslySelectable = this._idlMemberApplies("setRangeText", prevType); |
| const nowSelectable = this._idlMemberApplies("setRangeText", curType); |
| if (!previouslySelectable && nowSelectable) { |
| this._selectionStart = 0; |
| this._selectionEnd = 0; |
| this._selectionDirection = "none"; |
| } |
| } |
| } |
| |
| super._attrModified.apply(this, arguments); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#signal-a-type-change |
| _signalATypeChange() { |
| if (this._checkedness) { |
| this._removeOtherRadioCheckedness(); |
| } |
| } |
| |
| _formReset() { |
| const wrapper = idlUtils.wrapperForImpl(this); |
| this._value = sanitizeValueByType(this, wrapper.defaultValue); |
| this._dirtyValue = false; |
| this._checkedness = wrapper.defaultChecked; |
| this._dirtyCheckedness = false; |
| if (this._checkedness) { |
| this._removeOtherRadioCheckedness(); |
| } |
| } |
| |
| _changedFormOwner() { |
| if (this._checkedness) { |
| this._removeOtherRadioCheckedness(); |
| } |
| } |
| |
| get _otherRadioGroupElements() { |
| const wrapper = idlUtils.wrapperForImpl(this); |
| const root = this._radioButtonGroupRoot; |
| if (!root) { |
| return []; |
| } |
| |
| const result = []; |
| |
| 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 !== wrapper.name) { |
| continue; |
| } |
| |
| if (candidate !== this) { |
| result.push(candidate); |
| } |
| } |
| return result; |
| } |
| |
| _removeOtherRadioCheckedness() { |
| for (const radioGroupElement of this._otherRadioGroupElements) { |
| radioGroupElement._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; |
| } |
| |
| _isRadioGroupChecked() { |
| if (this.checked) { |
| return true; |
| } |
| return this._otherRadioGroupElements.some(radioGroupElement => radioGroupElement.checked); |
| } |
| |
| get _mutable() { |
| return !isDisabled(this) && !this._hasAttributeAndApplies("readonly"); |
| } |
| |
| get labels() { |
| return getLabelsForLabelable(this); |
| } |
| |
| get form() { |
| return formOwner(this); |
| } |
| |
| get checked() { |
| return this._checkedness; |
| } |
| |
| set checked(checked) { |
| this._checkedness = Boolean(checked); |
| this._dirtyCheckedness = true; |
| if (this._checkedness) { |
| this._removeOtherRadioCheckedness(); |
| } |
| } |
| |
| get value() { |
| switch (valueAttributeMode(this.type)) { |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value |
| case "value": |
| return this._getValue(); |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default |
| case "default": { |
| const attr = this.getAttributeNS(null, "value"); |
| return attr !== null ? attr : ""; |
| } |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on |
| case "default/on": { |
| const attr = this.getAttributeNS(null, "value"); |
| return attr !== null ? attr : "on"; |
| } |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename |
| case "filename": |
| return this.files.length ? "C:\\fakepath\\" + this.files[0].name : ""; |
| default: |
| throw new Error("jsdom internal error: unknown value attribute mode"); |
| } |
| } |
| |
| set value(val) { |
| switch (valueAttributeMode(this.type)) { |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value |
| case "value": { |
| const oldValue = this._value; |
| this._value = sanitizeValueByType(this, val); |
| this._dirtyValue = true; |
| |
| if (oldValue !== this._value) { |
| this._selectionStart = this._selectionEnd = this._getValueLength(); |
| this._selectionDirection = "none"; |
| } |
| break; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on |
| case "default": |
| case "default/on": |
| this.setAttributeNS(null, "value", val); |
| break; |
| |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename |
| case "filename": |
| if (val === "") { |
| this.files.length = 0; |
| } else { |
| throw DOMException.create(this._globalObject, [ |
| "This input element accepts a filename, which may only be programmatically set to the empty string.", |
| "InvalidStateError" |
| ]); |
| } |
| break; |
| |
| default: |
| throw new Error("jsdom internal error: unknown value attribute mode"); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasdate |
| get valueAsDate() { |
| if (!this._idlMemberApplies("valueAsDate")) { |
| return null; |
| } |
| |
| const window = this._ownerDocument._defaultView; |
| const convertedValue = this._convertStringToDate(this._value); |
| |
| if (convertedValue instanceof Date) { |
| return new window.Date(convertedValue.getTime()); |
| } |
| |
| return null; |
| } |
| |
| set valueAsDate(v) { |
| if (!this._idlMemberApplies("valueAsDate")) { |
| throw DOMException.create(this._globalObject, [ |
| "Failed to set the 'valueAsDate' property on 'HTMLInputElement': This input element does not support Date " + |
| "values.", |
| "InvalidStateError" |
| ]); |
| } |
| |
| if (v !== null && !isDate(v)) { |
| throw new TypeError("Failed to set the 'valueAsDate' property on 'HTMLInputElement': The provided value is " + |
| "not a Date."); |
| } |
| |
| if (v === null || isNaN(v)) { |
| this._value = ""; |
| } |
| |
| this._value = this._convertDateToString(v); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber |
| get valueAsNumber() { |
| if (!this._idlMemberApplies("valueAsNumber")) { |
| return NaN; |
| } |
| |
| const parsedValue = this._convertStringToNumber(this._value); |
| return parsedValue !== null ? parsedValue : NaN; |
| } |
| |
| set valueAsNumber(v) { |
| if (!isFinite(v)) { |
| throw new TypeError("Failed to set infinite value as Number"); |
| } |
| |
| if (!this._idlMemberApplies("valueAsNumber")) { |
| throw DOMException.create(this._globalObject, [ |
| "Failed to set the 'valueAsNumber' property on 'HTMLInputElement': This input element does not support " + |
| "Number values.", |
| "InvalidStateError" |
| ]); |
| } |
| |
| this._value = this._convertNumberToString(v); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup |
| _stepUpdate(n, isUp) { |
| const methodName = isUp ? "stepUp" : "stepDown"; |
| if (!this._idlMemberApplies(methodName)) { |
| throw DOMException.create(this._globalObject, [ |
| `Failed to invoke '${methodName}' method on 'HTMLInputElement': ` + |
| "This input element does not support Number values.", |
| "InvalidStateError" |
| ]); |
| } |
| |
| const allowedValueStep = this._allowedValueStep; |
| if (allowedValueStep === null) { |
| throw DOMException.create(this._globalObject, [ |
| `Failed to invoke '${methodName}' method on 'HTMLInputElement': ` + |
| "This input element does not support value step.", |
| "InvalidStateError" |
| ]); |
| } |
| |
| const min = this._minimum; |
| const max = this._maximum; |
| |
| if (min !== null && max !== null) { |
| if (min > max) { |
| return; |
| } |
| |
| const candidateStepValue = this._stepAlign(Decimal.add(min, allowedValueStep), /* roundUp = */ false); |
| if (candidateStepValue.lt(min) || candidateStepValue.gt(max)) { |
| return; |
| } |
| } |
| |
| let value = 0; |
| try { |
| value = this.valueAsNumber; |
| if (isNaN(value)) { // Empty value is parsed as NaN. |
| value = 0; |
| } |
| } catch (error) { |
| // Step 5. Default value is 0. |
| } |
| value = new Decimal(value); |
| |
| const valueBeforeStepping = value; |
| |
| if (!this._isStepAligned(value)) { |
| value = this._stepAlign(value, /* roundUp = */ isUp); |
| } else { |
| let delta = Decimal.mul(n, allowedValueStep); |
| if (!isUp) { |
| delta = delta.neg(); |
| } |
| value = value.add(delta); |
| } |
| |
| if (min !== null && value.lt(min)) { |
| value = this._stepAlign(min, /* roundUp = */ true); |
| } |
| |
| if (max !== null && value.gt(max)) { |
| value = this._stepAlign(max, /* roundUp = */ false); |
| } |
| |
| if (isUp ? value.lt(valueBeforeStepping) : value.gt(valueBeforeStepping)) { |
| return; |
| } |
| |
| this._value = this._convertNumberToString(value.toNumber()); |
| } |
| |
| stepDown(n = 1) { |
| return this._stepUpdate(n, false); |
| } |
| |
| stepUp(n = 1) { |
| return this._stepUpdate(n, true); |
| } |
| |
| get files() { |
| if (this.type === "file") { |
| this[filesSymbol] = this[filesSymbol] || FileList.createImpl(this._globalObject); |
| } else { |
| this[filesSymbol] = null; |
| } |
| return this[filesSymbol]; |
| } |
| |
| set files(value) { |
| if (this.type === "file" && value !== null) { |
| this[filesSymbol] = value; |
| } |
| } |
| |
| get type() { |
| const typeAttribute = this.getAttributeNS(null, "type"); |
| return getTypeFromAttribute(typeAttribute); |
| } |
| |
| set type(type) { |
| this.setAttributeNS(null, "type", type); |
| } |
| |
| _dispatchSelectEvent() { |
| fireAnEvent("select", this, undefined, { bubbles: true, cancelable: true }); |
| } |
| |
| _getValueLength() { |
| return typeof this.value === "string" ? this.value.length : 0; |
| } |
| |
| select() { |
| if (!this._idlMemberApplies("select")) { |
| return; |
| } |
| |
| this._selectionStart = 0; |
| this._selectionEnd = this._getValueLength(); |
| this._selectionDirection = "none"; |
| this._dispatchSelectEvent(); |
| } |
| |
| get selectionStart() { |
| if (!this._idlMemberApplies("selectionStart")) { |
| return null; |
| } |
| |
| return this._selectionStart; |
| } |
| |
| set selectionStart(start) { |
| if (!this._idlMemberApplies("selectionStart")) { |
| throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); |
| } |
| |
| this.setSelectionRange(start, Math.max(start, this._selectionEnd), this._selectionDirection); |
| } |
| |
| get selectionEnd() { |
| if (!this._idlMemberApplies("selectionEnd")) { |
| return null; |
| } |
| |
| return this._selectionEnd; |
| } |
| |
| set selectionEnd(end) { |
| if (!this._idlMemberApplies("selectionEnd")) { |
| throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); |
| } |
| |
| this.setSelectionRange(this._selectionStart, end, this._selectionDirection); |
| } |
| |
| get selectionDirection() { |
| if (!this._idlMemberApplies("selectionDirection")) { |
| return null; |
| } |
| |
| return this._selectionDirection; |
| } |
| |
| set selectionDirection(dir) { |
| if (!this._idlMemberApplies("selectionDirection")) { |
| throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); |
| } |
| |
| this.setSelectionRange(this._selectionStart, this._selectionEnd, dir); |
| } |
| |
| setSelectionRange(start, end, dir) { |
| if (!this._idlMemberApplies("setSelectionRange")) { |
| throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); |
| } |
| |
| 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 = "preserve") { |
| if (!this._idlMemberApplies("setRangeText")) { |
| throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); |
| } |
| |
| if (arguments.length < 2) { |
| start = this._selectionStart; |
| end = this._selectionEnd; |
| } else if (start > end) { |
| throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]); |
| } |
| |
| 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); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#the-list-attribute |
| get list() { |
| const id = this._getAttributeIfApplies("list"); |
| if (!id) { |
| return null; |
| } |
| |
| const el = this.getRootNode({}).getElementById(id); |
| |
| if (el && el.localName === "datalist") { |
| return el; |
| } |
| |
| return null; |
| } |
| |
| set maxLength(value) { |
| if (value < 0) { |
| throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]); |
| } |
| this.setAttributeNS(null, "maxlength", String(value)); |
| } |
| |
| // Reflected IDL attribute does not care about whether the content attribute applies. |
| get maxLength() { |
| if (!this.hasAttributeNS(null, "maxlength")) { |
| return 524288; // stole this from chrome |
| } |
| return parseInt(this.getAttributeNS(null, "maxlength")); |
| } |
| |
| set minLength(value) { |
| if (value < 0) { |
| throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]); |
| } |
| this.setAttributeNS(null, "minlength", String(value)); |
| } |
| |
| get minLength() { |
| if (!this.hasAttributeNS(null, "minlength")) { |
| return 0; |
| } |
| return parseInt(this.getAttributeNS(null, "minlength")); |
| } |
| |
| get size() { |
| if (!this.hasAttributeNS(null, "size")) { |
| return 20; |
| } |
| return parseInt(this.getAttributeNS(null, "size")); |
| } |
| |
| set size(value) { |
| if (value <= 0) { |
| throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]); |
| } |
| this.setAttributeNS(null, "size", String(value)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes |
| get _minimum() { |
| let min = this._defaultMinimum; |
| const attr = this._getAttributeIfApplies("min"); |
| if (attr !== null && this._convertStringToNumber !== undefined) { |
| const parsed = this._convertStringToNumber(attr); |
| if (parsed !== null) { |
| min = parsed; |
| } |
| } |
| return min; |
| } |
| |
| get _maximum() { |
| let max = this._defaultMaximum; |
| const attr = this._getAttributeIfApplies("max"); |
| if (attr !== null && this._convertStringToNumber !== undefined) { |
| const parsed = this._convertStringToNumber(attr); |
| if (parsed !== null) { |
| max = parsed; |
| } |
| } |
| return max; |
| } |
| |
| get _defaultMinimum() { |
| if (this.type === "range") { |
| return 0; |
| } |
| return null; |
| } |
| |
| get _defaultMaximum() { |
| if (this.type === "range") { |
| return 100; |
| } |
| return null; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-step |
| get _allowedValueStep() { |
| if (!this._contentAttributeApplies("step")) { |
| return null; |
| } |
| const attr = this.getAttributeNS(null, "step"); |
| if (attr === null) { |
| return this._defaultStep * this._stepScaleFactor; |
| } |
| if (asciiCaseInsensitiveMatch(attr, "any")) { |
| return null; |
| } |
| const parsedStep = parseFloatingPointNumber(attr); |
| if (parsedStep === null || parsedStep <= 0) { |
| return this._defaultStep * this._stepScaleFactor; |
| } |
| return parsedStep * this._stepScaleFactor; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale |
| get _stepScaleFactor() { |
| const dayInMilliseconds = 24 * 60 * 60 * 1000; |
| switch (this.type) { |
| case "week": |
| return 7 * dayInMilliseconds; |
| case "date": |
| return dayInMilliseconds; |
| case "datetime-local": |
| case "datetime": |
| case "time": |
| return 1000; |
| } |
| return 1; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-default |
| get _defaultStep() { |
| if (this.type === "datetime-local" || this.type === "datetime" || this.type === "time") { |
| return 60; |
| } |
| return 1; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-min-zero |
| get _stepBase() { |
| if (this._hasAttributeAndApplies("min")) { |
| const min = this._convertStringToNumber(this.getAttributeNS(null, "min")); |
| if (min !== null) { |
| return min; |
| } |
| } |
| if (this.hasAttributeNS(null, "value")) { |
| const value = this._convertStringToNumber(this.getAttributeNS(null, "value")); |
| if (value !== null) { |
| return value; |
| } |
| } |
| if (this._defaultStepBase !== null) { |
| return this._defaultStepBase; |
| } |
| return 0; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-default-base |
| get _defaultStepBase() { |
| if (this.type === "week") { |
| // The start of week 1970-W01 |
| return -259200000; |
| } |
| return null; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#common-input-element-attributes |
| // When an attribute doesn't apply to an input element, user agents must ignore the attribute. |
| _contentAttributeApplies(attribute) { |
| return applicableTypesForContentAttribute[attribute].has(this.type); |
| } |
| |
| _hasAttributeAndApplies(attribute) { |
| return this._contentAttributeApplies(attribute) && this.hasAttributeNS(null, attribute); |
| } |
| |
| _getAttributeIfApplies(attribute) { |
| if (this._contentAttributeApplies(attribute)) { |
| return this.getAttributeNS(null, attribute); |
| } |
| return null; |
| } |
| |
| _idlMemberApplies(member, type = this.type) { |
| return applicableTypesForIDLMember[member].has(type); |
| } |
| |
| _barredFromConstraintValidationSpecialization() { |
| // https://html.spec.whatwg.org/multipage/input.html#hidden-state-(type=hidden) |
| // https://html.spec.whatwg.org/multipage/input.html#reset-button-state-(type=reset) |
| // https://html.spec.whatwg.org/multipage/input.html#button-state-(type=button) |
| const willNotValidateTypes = new Set(["hidden", "reset", "button"]); |
| // https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly |
| const readOnly = this._hasAttributeAndApplies("readonly"); |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled |
| return willNotValidateTypes.has(this.type) || readOnly; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#concept-input-required |
| get _required() { |
| return this._hasAttributeAndApplies("required"); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#has-a-periodic-domain |
| get _hasAPeriodicDomain() { |
| return this.type === "time"; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/input.html#has-a-reversed-range |
| get _hasAReversedRange() { |
| return this._hasAPeriodicDomain && this._maximum < this._minimum; |
| } |
| |
| get validity() { |
| if (!this._validity) { |
| // Constraint validation: When an element has a reversed range, and the result of applying |
| // the algorithm to convert a string to a number to the string given by the element's value |
| // is a number, and the number obtained from that algorithm is more than the maximum and less |
| // than the minimum, the element is simultaneously suffering from an underflow and suffering |
| // from an overflow. |
| const reversedRangeSufferingOverUnderflow = () => { |
| const parsedValue = this._convertStringToNumber(this._value); |
| return parsedValue !== null && parsedValue > this._maximum && parsedValue < this._minimum; |
| }; |
| |
| const state = { |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-missing |
| valueMissing: () => { |
| // https://html.spec.whatwg.org/multipage/input.html#the-required-attribute |
| // Constraint validation: If the element is required, and its value IDL attribute applies |
| // and is in the mode value, and the element is mutable, and the element's value is the |
| // empty string, then the element is suffering from being missing. |
| // |
| // Note: As of today, the value IDL attribute always applies. |
| if (this._required && valueAttributeMode(this.type) === "value" && this._mutable && this._value === "") { |
| return true; |
| } |
| |
| switch (this.type) { |
| // https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox) |
| // Constraint validation: If the element is required and its checkedness is |
| // false, then the element is suffering from being missing. |
| case "checkbox": |
| if (this._required && !this._checkedness) { |
| return true; |
| } |
| break; |
| |
| // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio) |
| // Constraint validation: If an element in the radio button group is required, |
| // and all of the input elements in the radio button group have a checkedness |
| // that is false, then the element is suffering from being missing. |
| case "radio": |
| if (this._required && !this._isRadioGroupChecked()) { |
| return true; |
| } |
| break; |
| |
| // https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file) |
| // Constraint validation: If the element is required and the list of selected files is |
| // empty, then the element is suffering from being missing. |
| case "file": |
| if (this._required && this.files.length === 0) { |
| return true; |
| } |
| break; |
| } |
| |
| return false; |
| }, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-too-long |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-maxlength |
| // jsdom has no way at the moment to emulate a user interaction, so tooLong/tooShort have |
| // to be set to false. |
| tooLong: () => false, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-too-short |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-minlength |
| tooShort: () => false, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-an-overflow |
| rangeOverflow: () => { |
| // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes |
| if (this._hasAReversedRange) { |
| return reversedRangeSufferingOverUnderflow(); |
| } |
| // Constraint validation: When the element has a maximum and does not have a reversed |
| // range, and the result of applying the algorithm to convert a string to a number to the |
| // string given by the element's value is a number, and the number obtained from that |
| // algorithm is more than the maximum, the element is suffering from an overflow. |
| if (this._maximum !== null) { |
| const parsedValue = this._convertStringToNumber(this._value); |
| if (parsedValue !== null && parsedValue > this._maximum) { |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-an-underflow |
| rangeUnderflow: () => { |
| // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes |
| if (this._hasAReversedRange) { |
| return reversedRangeSufferingOverUnderflow(); |
| } |
| // Constraint validation: When the element has a minimum and does not have a reversed |
| // range, and the result of applying the algorithm to convert a string to a number to the |
| // string given by the element's value is a number, and the number obtained from that |
| // algorithm is less than the minimum, the element is suffering from an underflow. |
| if (this._minimum !== null) { |
| const parsedValue = this._convertStringToNumber(this._value); |
| if (parsedValue !== null && parsedValue < this._minimum) { |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-pattern-mismatch |
| patternMismatch: () => { |
| // https://html.spec.whatwg.org/multipage/input.html#the-pattern-attribute |
| if (this._value === "" || !this._hasAttributeAndApplies("pattern")) { |
| return false; |
| } |
| let regExp; |
| try { |
| const pattern = this.getAttributeNS(null, "pattern"); |
| // The pattern attribute should be matched against the entire value, not just any |
| // subset, so add ^ and $ anchors. But also check the validity of the regex itself |
| // first. |
| new RegExp(pattern, "u"); // eslint-disable-line no-new |
| regExp = new RegExp("^(?:" + pattern + ")$", "u"); |
| } catch (e) { |
| return false; |
| } |
| if (this._hasAttributeAndApplies("multiple")) { |
| return !splitOnCommas(this._value).every(value => regExp.test(value)); |
| } |
| return !regExp.test(this._value); |
| }, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-step-mismatch |
| // https://html.spec.whatwg.org/multipage/input.html#attr-input-step |
| stepMismatch: () => { |
| const allowedValueStep = this._allowedValueStep; |
| if (allowedValueStep === null) { |
| return false; |
| } |
| const number = this._convertStringToNumber(this._value); |
| return number !== null && !this._isStepAligned(number); |
| }, |
| |
| // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-type-mismatch |
| typeMismatch: () => { |
| switch (this.type) { |
| // https://html.spec.whatwg.org/multipage/input.html#url-state-(type=url) |
| // Constraint validation: While the value of the element is neither the empty string |
| // nor a valid absolute URL, the element is suffering from a type mismatch. |
| case "url": |
| if (this._value !== "" && !isValidAbsoluteURL(this._value)) { |
| return true; |
| } |
| break; |
| |
| // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type=email) |
| // Constraint validation [multiple=false]: While the value of the element is neither the empty |
| // string nor a single valid e - mail address, the element is suffering from a type mismatch. |
| // Constraint validation [multiple=true]: While the value of the element is not a valid e-mail address list, |
| // the element is suffering from a type mismatch. |
| case "email": |
| if (this._value !== "" && !isValidEmailAddress(this._getValue(), this.hasAttributeNS(null, "multiple"))) { |
| return true; |
| } |
| break; |
| } |
| return false; |
| } |
| }; |
| |
| this._validity = ValidityState.createImpl(this._globalObject, [], { |
| element: this, |
| state |
| }); |
| } |
| return this._validity; |
| } |
| |
| [cloningSteps](copy, node) { |
| copy._value = node._value; |
| copy._checkedness = node._checkedness; |
| copy._dirtyValue = node._dirtyValue; |
| copy._dirtyCheckedness = node._dirtyCheckedness; |
| } |
| } |
| |
| mixin(HTMLInputElementImpl.prototype, DefaultConstraintValidationImpl.prototype); |
| |
| module.exports = { |
| implementation: HTMLInputElementImpl |
| }; |