HTMLTextAreaElement-impl.js 6.37 KB
"use strict";

const HTMLElementImpl = require("./HTMLElement-impl").implementation;

const DefaultConstraintValidationImpl =
  require("../constraint-validation/DefaultConstraintValidation-impl").implementation;
const ValidityState = require("../generated/ValidityState");
const { mixin } = require("../../utils");

const DOMException = require("domexception/webidl2js-wrapper");
const { cloningSteps } = require("../helpers/internal-constants");
const { isDisabled, normalizeToCRLF, getLabelsForLabelable, formOwner } = require("../helpers/form-controls");
const { childTextContent } = require("../helpers/text");
const { fireAnEvent } = require("../helpers/events");

class HTMLTextAreaElementImpl extends HTMLElementImpl {
  constructor(globalObject, args, privateData) {
    super(globalObject, args, privateData);

    this._selectionStart = this._selectionEnd = 0;
    this._selectionDirection = "none";
    this._rawValue = "";
    this._dirtyValue = false;

    this._customValidityErrorMessage = "";

    this._labels = null;
  }

  _formReset() {
    this._rawValue = childTextContent(this);
    this._dirtyValue = false;
  }

  _getAPIValue() {
    return this._rawValue.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  }

  // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-wrapping-transformation
  _getValue() {
    // Hard-wrapping omitted, for now.
    return normalizeToCRLF(this._rawValue);
  }

  _childTextContentChangeSteps() {
    super._childTextContentChangeSteps();

    if (this._dirtyValue === false) {
      this._rawValue = 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._rawValue = 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() {
    fireAnEvent("select", this, undefined, { bubbles: true, cancelable: true });
  }

  _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 + 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);
    }
  }

  get cols() {
    if (!this.hasAttributeNS(null, "cols")) {
      return 20;
    }
    return parseInt(this.getAttributeNS(null, "cols"));
  }

  set cols(value) {
    if (value <= 0) {
      throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]);
    }
    this.setAttributeNS(null, "cols", String(value));
  }

  get rows() {
    if (!this.hasAttributeNS(null, "rows")) {
      return 2;
    }
    return parseInt(this.getAttributeNS(null, "rows"));
  }

  set rows(value) {
    if (value <= 0) {
      throw DOMException.create(this._globalObject, ["The index is not in the allowed range.", "IndexSizeError"]);
    }
    this.setAttributeNS(null, "rows", String(value));
  }

  _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;
  }

  [cloningSteps](copy, node) {
    copy._dirtyValue = node._dirtyValue;
    copy._rawValue = node._rawValue;
  }
}

mixin(HTMLTextAreaElementImpl.prototype, DefaultConstraintValidationImpl.prototype);

module.exports = {
  implementation: HTMLTextAreaElementImpl
};