blob: c13d119cf07b5bc7e139d0fe8f20065261d33956 [file] [log] [blame]
"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
};