jsdom
Version:
A JavaScript implementation of many web standards
256 lines (203 loc) • 6.81 kB
JavaScript
"use strict";
const HTMLElementImpl = require("./HTMLElement-impl").implementation;
const idlUtils = require("../../../generated/idl/utils.js");
const DefaultConstraintValidationImpl =
require("../constraint-validation/DefaultConstraintValidation-impl").implementation;
const ValidityState = require("../../../generated/idl/ValidityState");
const { mixin } = require("../../utils");
const DOMException = require("../../../generated/idl/DOMException");
const { isDisabled, getLabelsForLabelable, formOwner } = require("../helpers/form-controls");
const { childTextContent } = require("../helpers/text");
const { fireAnEvent } = require("../helpers/events");
class HTMLTextAreaElementImpl extends HTMLElementImpl {
#rawValue = "";
#dirtyValue = false;
constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);
this._selectionStart = this._selectionEnd = 0;
this._selectionDirection = "none";
this._customValidityErrorMessage = "";
this._labels = null;
}
_formReset() {
this.#setRawValue(childTextContent(this));
this.#dirtyValue = false;
}
_getAPIValue() {
return this.#rawValue.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
#setRawValue(value) {
if (this.#rawValue !== value) {
this.#rawValue = value;
this._invalidateSelectorState();
}
}
// https://html.spec.whatwg.org/multipage/form-elements.html#textarea-wrapping-transformation
_getValue() {
const apiValue = this._getAPIValue();
const wrap = this.getAttributeNS(null, "wrap");
return wrap === "hard" ?
textareaWrappingTransformation(apiValue, idlUtils.wrapperForImpl(this).cols) :
apiValue;
}
_childrenChangedSteps() {
super._childrenChangedSteps();
if (this.#dirtyValue === false) {
this.#setRawValue(childTextContent(this));
}
}
get labels() {
return getLabelsForLabelable(this);
}
get form() {
return formOwner(this);
}
get defaultValue() {
return childTextContent(this);
}
set defaultValue(val) {
this.textContent = val;
}
get value() {
return this._getAPIValue();
}
set value(val) {
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-value
const oldAPIValue = this._getAPIValue();
this.#setRawValue(val);
this.#dirtyValue = true;
if (oldAPIValue !== this._getAPIValue()) {
this._selectionStart = this._selectionEnd = this._getValueLength();
this._selectionDirection = "none";
}
}
get textLength() {
return this.value.length; // code unit length (16 bit)
}
get type() {
return "textarea";
}
_dispatchSelectEvent() {
this._globalObject._document._queueATask(() => {
fireAnEvent("select", this, undefined, { bubbles: true, cancelable: false });
});
}
_getValueLength() {
return typeof this.value === "string" ? this.value.length : 0;
}
select() {
this._selectionStart = 0;
this._selectionEnd = this._getValueLength();
this._selectionDirection = "none";
this._dispatchSelectEvent();
}
get selectionStart() {
return this._selectionStart;
}
set selectionStart(start) {
this.setSelectionRange(start, Math.max(start, this._selectionEnd), this._selectionDirection);
}
get selectionEnd() {
return this._selectionEnd;
}
set selectionEnd(end) {
this.setSelectionRange(this._selectionStart, end, this._selectionDirection);
}
get selectionDirection() {
return this._selectionDirection;
}
set selectionDirection(dir) {
this.setSelectionRange(this._selectionStart, this._selectionEnd, dir);
}
setSelectionRange(start, end, dir) {
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 (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 + repl.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);
}
}
_barredFromConstraintValidationSpecialization() {
return this.hasAttributeNS(null, "readonly");
}
get _mutable() {
return !isDisabled(this) && !this.hasAttributeNS(null, "readonly");
}
// https://html.spec.whatwg.org/multipage/form-elements.html#attr-textarea-required
get validity() {
if (!this._validity) {
const state = {
valueMissing: () => this.hasAttributeNS(null, "required") && this._mutable && this.value === ""
};
this._validity = ValidityState.createImpl(this._globalObject, [], {
element: this,
state
});
}
return this._validity;
}
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-node-clone-ext
_cloningSteps(copy) {
copy.#dirtyValue = this.#dirtyValue;
copy.#rawValue = this.#rawValue;
}
}
mixin(HTMLTextAreaElementImpl.prototype, DefaultConstraintValidationImpl.prototype);
module.exports = {
implementation: HTMLTextAreaElementImpl
};
function textareaWrappingTransformation(text, cols) {
let lineStart = 0;
let lineEnd = text.indexOf("\n");
if (lineEnd === -1) {
lineEnd = text.length;
}
while (lineStart < text.length) {
const lineLength = lineEnd - lineStart;
if (lineLength > cols) {
// split the line
lineEnd = lineStart + cols;
text = text.slice(0, lineEnd) + "\n" + text.slice(lineEnd);
}
// move to next line
lineStart = lineEnd + 1; // step over the newline
lineEnd = text.indexOf("\n", lineStart);
if (lineEnd === -1) {
lineEnd = text.length;
}
}
return text;
}