diff options
Diffstat (limited to 'alarm/node_modules/jsdom/lib/jsdom/living/nodes/HTMLInputElement-impl.js')
-rw-r--r-- | alarm/node_modules/jsdom/lib/jsdom/living/nodes/HTMLInputElement-impl.js | 1128 |
1 files changed, 1128 insertions, 0 deletions
diff --git a/alarm/node_modules/jsdom/lib/jsdom/living/nodes/HTMLInputElement-impl.js b/alarm/node_modules/jsdom/lib/jsdom/living/nodes/HTMLInputElement-impl.js new file mode 100644 index 0000000..edd299d --- /dev/null +++ b/alarm/node_modules/jsdom/lib/jsdom/living/nodes/HTMLInputElement-impl.js @@ -0,0 +1,1128 @@ +"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._legacyActivationBehaviorPreviousIndeterminateState = false; + + 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() { + if (this.type === "checkbox") { + this.checked = !this.checked; + this._legacyActivationBehaviorPreviousIndeterminateState = this.indeterminate; + this.indeterminate = false; + } else if (this.type === "radio") { + this._preCheckedRadioState = this.checked; + this.checked = true; + } + } + + _legacyCanceledActivationBehavior() { + if (this.type === "checkbox") { + this.checked = !this.checked; + this.indeterminate = this._legacyActivationBehaviorPreviousIndeterminateState; + } else if (this.type === "radio") { + if (this._preCheckedRadioState !== null) { + this.checked = this._preCheckedRadioState; + this._preCheckedRadioState = null; + } + } + } + + _activationBehavior() { + if (!this._mutable && this.type !== "checkbox" && this.type !== "radio") { + return; + } + + const { form } = this; + + if (this.type === "checkbox" || (this.type === "radio" && !this._preCheckedRadioState)) { + if (this.isConnected) { + 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(name, value, oldVal); + } + + // 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; + } + + _someInRadioGroup(name) { + if (this[name]) { + return true; + } + return this._otherRadioGroupElements.some(radioGroupElement => radioGroupElement[name]); + } + + 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; + } + + // 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 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)); + } + + get minLength() { + if (!this.hasAttributeNS(null, "minlength")) { + return 0; + } + return parseInt(this.getAttributeNS(null, "minlength")); + } + + 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 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._someInRadioGroup("_required") && !this._someInRadioGroup("checked")) { + 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 +}; |