/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* globals XULTreeElement */

"use strict";

// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
  const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
  );

  class MozTreeChildren extends MozElements.BaseControl {
    constructor() {
      super();

      /**
       * If there is no modifier key, we select on mousedown, not
       * click, so that drags work correctly.
       */
      this.addEventListener("mousedown", event => {
        if (this.parentNode.disabled) {
          return;
        }
        if (
          ((!event.getModifierState("Accel") ||
            !this.parentNode.pageUpOrDownMovesSelection) &&
            !event.shiftKey &&
            !event.metaKey) ||
          this.parentNode.view.selection.single
        ) {
          var b = this.parentNode;
          var cell = b.getCellAt(event.clientX, event.clientY);
          var view = this.parentNode.view;

          // save off the last selected row
          this._lastSelectedRow = cell.row;

          if (cell.row == -1) {
            return;
          }

          if (cell.childElt == "twisty") {
            return;
          }

          if (cell.col && event.button == 0) {
            if (cell.col.cycler) {
              view.cycleCell(cell.row, cell.col);
              return;
            } else if (cell.col.type == window.TreeColumn.TYPE_CHECKBOX) {
              if (
                this.parentNode.editable &&
                cell.col.editable &&
                view.isEditable(cell.row, cell.col)
              ) {
                var value = view.getCellValue(cell.row, cell.col);
                value = value == "true" ? "false" : "true";
                view.setCellValue(cell.row, cell.col, value);
                return;
              }
            }
          }

          if (!view.selection.isSelected(cell.row)) {
            view.selection.select(cell.row);
            b.ensureRowIsVisible(cell.row);
          }
        }
      });

      /**
       * On a click (up+down on the same item), deselect everything
       * except this item.
       */
      this.addEventListener("click", event => {
        if (event.button != 0) {
          return;
        }
        if (this.parentNode.disabled) {
          return;
        }
        var b = this.parentNode;
        var cell = b.getCellAt(event.clientX, event.clientY);
        var view = this.parentNode.view;

        if (cell.row == -1) {
          return;
        }

        if (cell.childElt == "twisty") {
          if (
            view.selection.currentIndex >= 0 &&
            view.isContainerOpen(cell.row)
          ) {
            var parentIndex = view.getParentIndex(view.selection.currentIndex);
            while (parentIndex >= 0 && parentIndex != cell.row) {
              parentIndex = view.getParentIndex(parentIndex);
            }
            if (parentIndex == cell.row) {
              var parentSelectable = true;
              if (parentSelectable) {
                view.selection.select(parentIndex);
              }
            }
          }
          this.parentNode.changeOpenState(cell.row);
          return;
        }

        if (!view.selection.single) {
          var augment = event.getModifierState("Accel");
          if (event.shiftKey) {
            view.selection.rangedSelect(-1, cell.row, augment);
            b.ensureRowIsVisible(cell.row);
            return;
          }
          if (augment) {
            view.selection.toggleSelect(cell.row);
            b.ensureRowIsVisible(cell.row);
            view.selection.currentIndex = cell.row;
            return;
          }
        }

        /* We want to deselect all the selected items except what was
          clicked, UNLESS it was a right-click.  We have to do this
          in click rather than mousedown so that you can drag a
          selected group of items */

        if (!cell.col) {
          return;
        }

        // if the last row has changed in between the time we
        // mousedown and the time we click, don't fire the select handler.
        // see bug #92366
        if (
          !cell.col.cycler &&
          this._lastSelectedRow == cell.row &&
          cell.col.type != window.TreeColumn.TYPE_CHECKBOX
        ) {
          view.selection.select(cell.row);
          b.ensureRowIsVisible(cell.row);
        }
      });

      /**
       * double-click
       */
      this.addEventListener("dblclick", event => {
        if (this.parentNode.disabled) {
          return;
        }
        var tree = this.parentNode;
        var view = this.parentNode.view;
        var row = view.selection.currentIndex;

        if (row == -1) {
          return;
        }

        var cell = tree.getCellAt(event.clientX, event.clientY);

        if (cell.childElt != "twisty") {
          this.parentNode.startEditing(row, cell.col);
        }

        if (this.parentNode._editingColumn || !view.isContainer(row)) {
          return;
        }

        // Cyclers and twisties respond to single clicks, not double clicks
        if (cell.col && !cell.col.cycler && cell.childElt != "twisty") {
          this.parentNode.changeOpenState(row);
        }
      });
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.setAttribute("slot", "treechildren");

      this._lastSelectedRow = -1;

      if ("_ensureColumnOrder" in this.parentNode) {
        this.parentNode._ensureColumnOrder();
      }
    }
  }

  customElements.define("treechildren", MozTreeChildren);

  class MozTreecolPicker extends MozElements.BaseControl {
    static get markup() {
      return `
      <button class="tree-columnpicker-button"/>
      <menupopup anonid="popup">
        <menuseparator anonid="menuseparator"/>
        <menuitem anonid="menuitem" data-l10n-id="tree-columnpicker-restore-order"/>
      </menupopup>
      `;
    }
    constructor() {
      super();

      window.MozXULElement.insertFTLIfNeeded("toolkit/global/tree.ftl");
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.textContent = "";
      this.appendChild(this.constructor.fragment);

      let button = this.querySelector(".tree-columnpicker-button");
      let popup = this.querySelector('[anonid="popup"]');
      let menuitem = this.querySelector('[anonid="menuitem"]');

      button.addEventListener("command", e => {
        this.buildPopup(popup);
        popup.openPopup(this, "after_end");
        e.preventDefault();
      });

      menuitem.addEventListener("command", e => {
        let tree = this.parentNode.parentNode;
        tree.stopEditing(true);
        this.style.order = "";
        tree._ensureColumnOrder(tree.NATURAL_ORDER);
        e.preventDefault();
      });
    }

    buildPopup(aPopup) {
      // We no longer cache the picker content, remove the old content related to
      // the cols - menuitem and separator should stay.
      aPopup.querySelectorAll("[colindex]").forEach(e => {
        e.remove();
      });

      var refChild = aPopup.firstChild;

      var tree = this.parentNode.parentNode;
      for (
        var currCol = tree.columns.getFirstColumn();
        currCol;
        currCol = currCol.getNext()
      ) {
        // Construct an entry for each column in the row, unless
        // it is not being shown.
        var currElement = currCol.element;
        if (!currElement.hasAttribute("ignoreincolumnpicker")) {
          var popupChild = document.createXULElement("menuitem");
          popupChild.setAttribute("type", "checkbox");
          var columnName =
            currElement.getAttribute("display") ||
            currElement.getAttribute("label");
          popupChild.setAttribute("label", columnName);
          popupChild.setAttribute("colindex", currCol.index);
          if (!currElement.hasAttribute("hidden")) {
            popupChild.setAttribute("checked", "true");
          }
          if (currCol.primary) {
            popupChild.setAttribute("disabled", "true");
          }
          if (currElement.hasAttribute("closemenu")) {
            popupChild.setAttribute(
              "closemenu",
              currElement.getAttribute("closemenu")
            );
          }

          popupChild.addEventListener("command", function () {
            let colindex = this.getAttribute("colindex");
            let column = tree.columns[colindex];
            if (column) {
              var element = column.element;
              element.hidden = !element.hidden;
            }
          });

          aPopup.insertBefore(popupChild, refChild);
        }
      }

      var hidden = !tree.enableColumnDrag;
      aPopup.querySelectorAll(":scope > :not([colindex])").forEach(e => {
        e.hidden = hidden;
      });
    }
  }

  customElements.define("treecolpicker", MozTreecolPicker);

  class MozTreecol extends MozElements.BaseControl {
    static get observedAttributes() {
      return ["primary", ...super.observedAttributes];
    }

    static get inheritedAttributes() {
      return {
        ".treecol-sortdirection": "sortdirection,hidden=hideheader",
        ".treecol-text": "value=label,crop",
      };
    }

    static get markup() {
      return `
        <label class="treecol-text" flex="1" crop="end"></label>
        <image class="treecol-sortdirection"></image>
      `;
    }

    get _tree() {
      return this.parentNode?.parentNode;
    }

    _invalidate() {
      let tree = this._tree;
      if (!tree || !XULTreeElement.isInstance(tree)) {
        return;
      }
      tree.invalidate();
      tree.columns?.invalidateColumns();
    }

    constructor() {
      super();

      this.addEventListener("mousedown", event => {
        if (event.button != 0) {
          return;
        }
        if (this._tree.enableColumnDrag) {
          var XUL_NS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          var cols = this.parentNode.getElementsByTagNameNS(XUL_NS, "treecol");

          // only start column drag operation if there are at least 2 visible columns
          var visible = 0;
          for (var i = 0; i < cols.length; ++i) {
            if (cols[i].getBoundingClientRect().width > 0) {
              ++visible;
            }
          }

          if (visible > 1) {
            window.addEventListener("mousemove", this._onDragMouseMove, true);
            window.addEventListener("mouseup", this._onDragMouseUp, true);
            document.treecolDragging = this;
            this.mDragGesturing = true;
            this.mStartDragX = event.clientX;
            this.mStartDragY = event.clientY;
          }
        }
      });

      this.addEventListener("click", event => {
        if (event.button != 0) {
          return;
        }
        if (event.target != event.originalTarget) {
          return;
        }

        // On Windows multiple clicking on tree columns only cycles one time
        // every 2 clicks.
        if (AppConstants.platform == "win" && event.detail % 2 == 0) {
          return;
        }

        var tree = this._tree;
        if (tree.columns) {
          tree.view.cycleHeader(tree.columns.getColumnFor(this));
        }
      });
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.textContent = "";
      this.appendChild(this.constructor.fragment);
      this.initializeAttributeInheritance();
      if (this.hasAttribute("ordinal")) {
        this.style.order = this.getAttribute("ordinal");
      }
      if (this.hasAttribute("width")) {
        this.style.width = this.getAttribute("width") + "px";
      }

      this._resizeObserver = new ResizeObserver(() => {
        this._invalidate();
      });
      this._resizeObserver.observe(this);
    }

    disconnectedCallback() {
      this._resizeObserver?.unobserve(this);
      this._resizeObserver = null;
    }

    attributeChangedCallback(name, oldValue, newValue) {
      super.attributeChangedCallback(name, oldValue, newValue);
      this._invalidate();
    }

    set ordinal(val) {
      this.style.order = val;
      this.setAttribute("ordinal", val);
    }

    get ordinal() {
      var val = this.style.order;
      if (val == "") {
        return "1";
      }

      return "" + (val == "0" ? 0 : parseInt(val));
    }

    get _previousVisibleColumn() {
      var tree = this.parentNode.parentNode;
      let sib = tree.columns.getColumnFor(this).previousColumn;
      while (sib) {
        if (sib.element && sib.element.getBoundingClientRect().width > 0) {
          return sib.element;
        }

        sib = sib.previousColumn;
      }

      return null;
    }

    _onDragMouseMove(aEvent) {
      var col = document.treecolDragging;
      if (!col) {
        return;
      }

      // determine if we have moved the mouse far enough
      // to initiate a drag
      if (col.mDragGesturing) {
        if (
          Math.abs(aEvent.clientX - col.mStartDragX) < 5 &&
          Math.abs(aEvent.clientY - col.mStartDragY) < 5
        ) {
          return;
        }
        col.mDragGesturing = false;
        col.setAttribute("dragging", "true");
        window.addEventListener("click", col._onDragMouseClick, true);
      }

      var pos = {};
      var targetCol = col.parentNode.parentNode._getColumnAtX(
        aEvent.clientX,
        0.5,
        pos
      );

      // bail if we haven't mousemoved to a different column
      if (col.mTargetCol == targetCol && col.mTargetDir == pos.value) {
        return;
      }

      var tree = col.parentNode.parentNode;
      var sib;
      var column;
      if (col.mTargetCol) {
        // remove previous insertbefore/after attributes
        col.mTargetCol.removeAttribute("insertbefore");
        col.mTargetCol.removeAttribute("insertafter");
        column = tree.columns.getColumnFor(col.mTargetCol);
        tree.invalidateColumn(column);
        sib = col.mTargetCol._previousVisibleColumn;
        if (sib) {
          sib.removeAttribute("insertafter");
          column = tree.columns.getColumnFor(sib);
          tree.invalidateColumn(column);
        }
        col.mTargetCol = null;
        col.mTargetDir = null;
      }

      if (targetCol) {
        // set insertbefore/after attributes
        if (pos.value == "after") {
          targetCol.setAttribute("insertafter", "true");
        } else {
          targetCol.setAttribute("insertbefore", "true");
          sib = targetCol._previousVisibleColumn;
          if (sib) {
            sib.setAttribute("insertafter", "true");
            column = tree.columns.getColumnFor(sib);
            tree.invalidateColumn(column);
          }
        }
        column = tree.columns.getColumnFor(targetCol);
        tree.invalidateColumn(column);
        col.mTargetCol = targetCol;
        col.mTargetDir = pos.value;
      }
    }

    _onDragMouseUp() {
      var col = document.treecolDragging;
      if (!col) {
        return;
      }

      if (!col.mDragGesturing) {
        if (col.mTargetCol) {
          // remove insertbefore/after attributes
          var before = col.mTargetCol.hasAttribute("insertbefore");
          col.mTargetCol.removeAttribute(
            before ? "insertbefore" : "insertafter"
          );

          var sib = col.mTargetCol._previousVisibleColumn;
          if (before && sib) {
            sib.removeAttribute("insertafter");
          }

          // Move the column only if it will result in a different column
          // ordering
          var move = true;

          // If this is a before move and the previous visible column is
          // the same as the column we're moving, don't move
          if (before && col == sib) {
            move = false;
          } else if (!before && col == col.mTargetCol) {
            // If this is an after move and the column we're moving is
            // the same as the target column, don't move.
            move = false;
          }

          if (move) {
            col.parentNode.parentNode._reorderColumn(
              col,
              col.mTargetCol,
              before
            );
          }

          // repaint to remove lines
          col.parentNode.parentNode.invalidate();

          col.mTargetCol = null;
        }
      } else {
        col.mDragGesturing = false;
      }

      document.treecolDragging = null;
      col.removeAttribute("dragging");

      window.removeEventListener("mousemove", col._onDragMouseMove, true);
      window.removeEventListener("mouseup", col._onDragMouseUp, true);
      // we have to wait for the click event to fire before removing
      // cancelling handler
      var clickHandler = function (handler) {
        window.removeEventListener("click", handler, true);
      };
      window.setTimeout(clickHandler, 0, col._onDragMouseClick);
    }

    _onDragMouseClick(aEvent) {
      // prevent click event from firing after column drag and drop
      aEvent.stopPropagation();
      aEvent.preventDefault();
    }
  }

  customElements.define("treecol", MozTreecol);

  class MozTreecols extends MozElements.BaseControl {
    static get inheritedAttributes() {
      return {
        treecolpicker: "tooltiptext=pickertooltiptext",
      };
    }

    static get markup() {
      return `
      <treecolpicker fixed="true"></treecolpicker>
      `;
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.setAttribute("slot", "treecols");

      if (!this.querySelector("treecolpicker")) {
        this.appendChild(this.constructor.fragment);
        this.initializeAttributeInheritance();
      }

      // Set resizeafter="farthest" on the splitters if nothing else has been
      // specified.
      for (let splitter of this.getElementsByTagName("splitter")) {
        if (!splitter.hasAttribute("resizeafter")) {
          splitter.setAttribute("resizeafter", "farthest");
        }
      }
    }
  }

  customElements.define("treecols", MozTreecols);

  class MozTree extends MozElements.BaseControlMixin(
    MozElements.MozElementMixin(XULTreeElement)
  ) {
    static get markup() {
      return `
      <html:link rel="stylesheet" href="chrome://global/content/widgets.css" />
      <html:slot name="treecols"></html:slot>
      <stack class="tree-stack" flex="1">
        <hbox class="tree-rows" flex="1">
          <html:slot name="treechildren"></html:slot>
          <scrollbar vertical="true" class="scrollbar-topmost" />
        </hbox>
        <html:input class="tree-input" type="text" hidden="true"/>
      </stack>
      `;
    }

    constructor() {
      super();

      // These enumerated constants are used as the first argument to
      // _ensureColumnOrder to specify what column ordering should be used.
      this.CURRENT_ORDER = 0;
      this.NATURAL_ORDER = 1; // The original order, which is the DOM ordering

      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(this.constructor.fragment);

      let stopAndPrevent = e => {
        e.stopPropagation();
        e.preventDefault();
      };
      let stopProp = e => e.stopPropagation();
      this.#verticalScrollbar = this.shadowRoot.querySelector("scrollbar");
      this.#verticalScrollbar.addEventListener("click", stopAndPrevent);
      this.#verticalScrollbar.addEventListener("contextmenu", stopAndPrevent);
      this.#verticalScrollbar.addEventListener("dblclick", stopProp);
      this.#verticalScrollbar.addEventListener("command", stopProp);
    }

    static get inheritedAttributes() {
      return {
        scrollbar: "collapsed=hidevscroll",
      };
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }
      if (!this._eventListenersSetup) {
        this._eventListenersSetup = true;
        this.setupEventListeners();
      }

      this.setAttribute("hidevscroll", "true");

      this.initializeAttributeInheritance();

      this.pageUpOrDownMovesSelection = AppConstants.platform != "macosx";

      this._inputField = null;

      this._editingRow = -1;

      this._editingColumn = null;

      this._columnsDirty = true;

      this._lastKeyTime = 0;

      this._incrementalString = "";

      this._touchY = -1;
    }

    setupEventListeners() {
      this.addEventListener("underflow", event => {
        // Scrollport event orientation
        // 0: vertical
        // 1: horizontal
        // 2: both (not used)
        if (event.target.tagName != "treechildren") {
          return;
        }
        if (event.detail == 0) {
          this.setAttribute("hidevscroll", "true");
        }
        event.stopPropagation();
      });

      this.addEventListener("overflow", event => {
        if (event.target.tagName != "treechildren") {
          return;
        }
        if (event.detail == 0) {
          this.removeAttribute("hidevscroll");
        }
        event.stopPropagation();
      });

      this.addEventListener("touchstart", event => {
        function isScrollbarElement(target) {
          return (
            (target.localName == "thumb" || target.localName == "slider") &&
            target.namespaceURI ==
              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          );
        }
        if (
          event.touches.length > 1 ||
          isScrollbarElement(event.touches[0].target)
        ) {
          // Multiple touch points detected, abort. In particular this aborts
          // the panning gesture when the user puts a second finger down after
          // already panning with one finger. Aborting at this point prevents
          // the pan gesture from being resumed until all fingers are lifted
          // (as opposed to when the user is back down to one finger).
          // Additionally, if the user lands on the scrollbar don't use this
          // code for scrolling, instead allow gecko to handle scrollbar
          // interaction normally.
          this._touchY = -1;
        } else {
          this._touchY = event.touches[0].screenY;
        }
      });

      this.addEventListener("touchmove", event => {
        if (event.touches.length == 1 && this._touchY >= 0) {
          var deltaY = this._touchY - event.touches[0].screenY;
          var lines = Math.trunc(deltaY / this.rowHeight);
          if (Math.abs(lines) > 0) {
            this.scrollByLines(lines);
            deltaY -= lines * this.rowHeight;
            this._touchY = event.touches[0].screenY + deltaY;
          }
          event.preventDefault();
        }
      });

      this.addEventListener("touchend", () => {
        this._touchY = -1;
      });

      // This event doesn't retarget, so listen on the shadow DOM directly
      this.shadowRoot.addEventListener("MozMousePixelScroll", event => {
        if (this.#canScroll(event)) {
          event.preventDefault();
        }
      });

      // This event doesn't retarget, so listen on the shadow DOM directly
      this.shadowRoot.addEventListener("DOMMouseScroll", event => {
        if (!this.#canScroll(event)) {
          return;
        }

        event.preventDefault();

        if (this._editingColumn) {
          return;
        }

        var rows = event.detail;
        if (rows == UIEvent.SCROLL_PAGE_UP) {
          this.scrollByPages(-1);
        } else if (rows == UIEvent.SCROLL_PAGE_DOWN) {
          this.scrollByPages(1);
        } else {
          let children = this.querySelector("treechildren");
          let maxRows = Math.floor(children.scrollHeight / this.rowHeight);
          if (Math.abs(rows) > maxRows) {
            rows = rows > 0 ? maxRows : -maxRows;
          }
          this.scrollByLines(rows);
        }
      });

      this.addEventListener("MozSwipeGesture", event => {
        // Figure out which row to show
        let targetRow = 0;

        // Only handle swipe gestures up and down
        switch (event.direction) {
          case event.DIRECTION_DOWN:
            targetRow = this.view.rowCount - 1;
          // Fall through for actual action
          case event.DIRECTION_UP:
            this.ensureRowIsVisible(targetRow);
            break;
        }
      });

      this.addEventListener("select", event => {
        if (event.originalTarget == this) {
          this.stopEditing(true);
        }
      });

      this.addEventListener("focus", () => {
        this.focused = true;
        if (this.currentIndex == -1 && this.view.rowCount > 0) {
          this.currentIndex = this.getFirstVisibleRow();
        }
      });

      this.addEventListener(
        "blur",
        event => {
          this.focused = false;
          if (event.target == this.inputField) {
            this.stopEditing(true);
          }
        },
        true
      );

      this.addEventListener("keydown", event => {
        if (event.altKey) {
          return;
        }

        let toggleClose = () => {
          if (this._editingColumn) {
            return;
          }

          let row = this.currentIndex;
          if (row < 0) {
            return;
          }

          if (this.changeOpenState(this.currentIndex, false)) {
            event.preventDefault();
            return;
          }

          let parentIndex = this.view.getParentIndex(this.currentIndex);
          if (parentIndex >= 0) {
            this.view.selection.select(parentIndex);
            this.ensureRowIsVisible(parentIndex);
            event.preventDefault();
          }
        };

        let toggleOpen = () => {
          if (this._editingColumn) {
            return;
          }

          let row = this.currentIndex;
          if (row < 0) {
            return;
          }

          if (this.changeOpenState(row, true)) {
            event.preventDefault();
            return;
          }
          let c = row + 1;
          let view = this.view;
          if (c < view.rowCount && view.getParentIndex(c) == row) {
            // If already opened, select the first child.
            // The getParentIndex test above ensures that the children
            // are already populated and ready.
            this.view.selection.timedSelect(c, this._selectDelay);
            this.ensureRowIsVisible(c);
            event.preventDefault();
          }
        };

        switch (event.keyCode) {
          case KeyEvent.DOM_VK_RETURN: {
            if (this._handleEnter(event)) {
              event.stopPropagation();
              event.preventDefault();
            }
            break;
          }
          case KeyEvent.DOM_VK_ESCAPE: {
            if (this._editingColumn) {
              this.stopEditing(false);
              this.focus();
              event.stopPropagation();
              event.preventDefault();
            }
            break;
          }
          case KeyEvent.DOM_VK_LEFT: {
            if (!this.isRTL) {
              toggleClose();
            } else {
              toggleOpen();
            }
            break;
          }
          case KeyEvent.DOM_VK_RIGHT: {
            if (!this.isRTL) {
              toggleOpen();
            } else {
              toggleClose();
            }
            break;
          }
          case KeyEvent.DOM_VK_UP: {
            if (this._editingColumn) {
              return;
            }

            if (event.getModifierState("Shift")) {
              this._moveByOffsetShift(-1, 0, event);
            } else {
              this._moveByOffset(-1, 0, event);
            }
            break;
          }
          case KeyEvent.DOM_VK_DOWN: {
            if (this._editingColumn) {
              return;
            }
            if (event.getModifierState("Shift")) {
              this._moveByOffsetShift(1, this.view.rowCount - 1, event);
            } else {
              this._moveByOffset(1, this.view.rowCount - 1, event);
            }
            break;
          }
          case KeyEvent.DOM_VK_PAGE_UP: {
            if (this._editingColumn) {
              return;
            }

            if (event.getModifierState("Shift")) {
              this._moveByPageShift(-1, 0, event);
            } else {
              this._moveByPage(-1, 0, event);
            }
            break;
          }
          case KeyEvent.DOM_VK_PAGE_DOWN: {
            if (this._editingColumn) {
              return;
            }

            if (event.getModifierState("Shift")) {
              this._moveByPageShift(1, this.view.rowCount - 1, event);
            } else {
              this._moveByPage(1, this.view.rowCount - 1, event);
            }
            break;
          }
          case KeyEvent.DOM_VK_HOME: {
            if (this._editingColumn) {
              return;
            }

            if (event.getModifierState("Shift")) {
              this._moveToEdgeShift(0, event);
            } else {
              this._moveToEdge(0, event);
            }
            break;
          }
          case KeyEvent.DOM_VK_END: {
            if (this._editingColumn) {
              return;
            }

            if (event.getModifierState("Shift")) {
              this._moveToEdgeShift(this.view.rowCount - 1, event);
            } else {
              this._moveToEdge(this.view.rowCount - 1, event);
            }
            break;
          }
        }
      });

      this.addEventListener("keypress", event => {
        if (this._editingColumn) {
          return;
        }

        if (event.charCode == " ".charCodeAt(0)) {
          var c = this.currentIndex;
          if (
            !this.view.selection.isSelected(c) ||
            (!this.view.selection.single && event.getModifierState("Accel"))
          ) {
            this.view.selection.toggleSelect(c);
            event.preventDefault();
          }
        } else if (
          !this.disableKeyNavigation &&
          event.charCode > 0 &&
          !event.altKey &&
          !event.getModifierState("Accel") &&
          !event.metaKey &&
          !event.ctrlKey
        ) {
          var l = this._keyNavigate(event);
          if (l >= 0) {
            this.view.selection.timedSelect(l, this._selectDelay);
            this.ensureRowIsVisible(l);
          }
          event.preventDefault();
        }
      });
    }

    get body() {
      return this.treeBody;
    }

    get isRTL() {
      return document.defaultView.getComputedStyle(this).direction == "rtl";
    }

    set editable(val) {
      if (val) {
        this.setAttribute("editable", "true");
      } else {
        this.removeAttribute("editable");
      }
    }

    get editable() {
      return this.getAttribute("editable") == "true";
    }
    /**
     * ///////////////// nsIDOMXULSelectControlElement /////////////////  ///////////////// nsIDOMXULMultiSelectControlElement /////////////////
     */
    set selType(val) {
      this.setAttribute("seltype", val);
    }

    get selType() {
      return this.getAttribute("seltype") || "";
    }

    set currentIndex(val) {
      if (this.view) {
        this.view.selection.currentIndex = val;
      }
    }

    get currentIndex() {
      if (this.view && this.view.selection) {
        return this.view.selection.currentIndex;
      }
      return -1;
    }

    set keepCurrentInView(val) {
      if (val) {
        this.setAttribute("keepcurrentinview", "true");
      } else {
        this.removeAttribute("keepcurrentinview");
      }
    }

    get keepCurrentInView() {
      return this.getAttribute("keepcurrentinview") == "true";
    }

    set enableColumnDrag(val) {
      if (val) {
        this.setAttribute("enableColumnDrag", "true");
      } else {
        this.removeAttribute("enableColumnDrag");
      }
    }

    get enableColumnDrag() {
      return this.hasAttribute("enableColumnDrag");
    }

    get inputField() {
      if (!this._inputField) {
        this._inputField = this.shadowRoot.querySelector(".tree-input");
        this._inputField.addEventListener("blur", () => this.stopEditing(true));
      }
      return this._inputField;
    }

    set disableKeyNavigation(val) {
      if (val) {
        this.setAttribute("disableKeyNavigation", "true");
      } else {
        this.removeAttribute("disableKeyNavigation");
      }
    }

    get disableKeyNavigation() {
      return this.hasAttribute("disableKeyNavigation");
    }

    get editingRow() {
      return this._editingRow;
    }

    get editingColumn() {
      return this._editingColumn;
    }

    set _selectDelay(val) {
      this.setAttribute("_selectDelay", val);
    }

    get _selectDelay() {
      return this.getAttribute("_selectDelay") || 50;
    }

    // The first argument (order) can be either one of these constants:
    //   this.CURRENT_ORDER
    //   this.NATURAL_ORDER
    _ensureColumnOrder(order = this.CURRENT_ORDER) {
      if (this.columns) {
        // update the ordinal position of each column to assure that it is
        // an odd number and 2 positions above its next sibling
        var cols = [];

        if (order == this.CURRENT_ORDER) {
          for (
            let col = this.columns.getFirstColumn();
            col;
            col = col.getNext()
          ) {
            cols.push(col.element);
          }
        } else {
          // order == this.NATURAL_ORDER
          cols = this.getElementsByTagName("treecol");
        }

        for (let i = 0; i < cols.length; ++i) {
          cols[i].ordinal = i * 2 + 1;
        }
        // update the ordinal positions of splitters to even numbers, so that
        // they are in between columns
        var splitters = this.getElementsByTagName("splitter");
        for (let i = 0; i < splitters.length; ++i) {
          splitters[i].style.order = (i + 1) * 2;
        }
      }
    }

    _reorderColumn(aColMove, aColBefore, aBefore) {
      this._ensureColumnOrder();

      var i;
      var cols = [];
      var col = this.columns.getColumnFor(aColBefore);
      if (parseInt(aColBefore.ordinal) < parseInt(aColMove.ordinal)) {
        if (aBefore) {
          cols.push(aColBefore);
        }
        for (
          col = col.getNext();
          col.element != aColMove;
          col = col.getNext()
        ) {
          cols.push(col.element);
        }

        aColMove.ordinal = cols[0].ordinal;
        for (i = 0; i < cols.length; ++i) {
          cols[i].ordinal = parseInt(cols[i].ordinal) + 2;
        }
      } else if (aColBefore.ordinal != aColMove.ordinal) {
        if (!aBefore) {
          cols.push(aColBefore);
        }
        for (
          col = col.getPrevious();
          col.element != aColMove;
          col = col.getPrevious()
        ) {
          cols.push(col.element);
        }

        aColMove.ordinal = cols[0].ordinal;
        for (i = 0; i < cols.length; ++i) {
          cols[i].ordinal = parseInt(cols[i].ordinal) - 2;
        }
      } else {
        return;
      }
      this.columns.invalidateColumns();
    }

    _getColumnAtX(aX, aThresh, aPos) {
      let isRTL = this.isRTL;

      if (aPos) {
        aPos.value = isRTL ? "after" : "before";
      }

      var columns = [];
      var col = this.columns.getFirstColumn();
      while (col) {
        columns.push(col);
        col = col.getNext();
      }
      if (isRTL) {
        columns.reverse();
      }
      var currentX = this.getBoundingClientRect().x;
      for (var i = 0; i < columns.length; ++i) {
        col = columns[i];
        var cw = col.element.getBoundingClientRect().width;
        if (cw > 0) {
          currentX += cw;
          if (currentX - cw * aThresh > aX) {
            return col.element;
          }
        }
      }

      if (aPos) {
        aPos.value = isRTL ? "before" : "after";
      }
      return columns.pop().element;
    }

    changeOpenState(row, openState) {
      if (row < 0 || !this.view.isContainer(row)) {
        return false;
      }

      if (this.view.isContainerOpen(row) != openState) {
        this.view.toggleOpenState(row);
        if (row == this.currentIndex) {
          // Only fire event when current row is expanded or collapsed
          // because that's all the assistive technology really cares about.
          var event = document.createEvent("Events");
          event.initEvent("OpenStateChange", true, true);
          this.dispatchEvent(event);
        }
        return true;
      }
      return false;
    }

    _keyNavigate(event) {
      var key = String.fromCharCode(event.charCode).toLowerCase();
      if (event.timeStamp - this._lastKeyTime > 1000) {
        this._incrementalString = key;
      } else {
        this._incrementalString += key;
      }
      this._lastKeyTime = event.timeStamp;

      var length = this._incrementalString.length;
      var incrementalString = this._incrementalString;
      var charIndex = 1;
      while (
        charIndex < length &&
        incrementalString[charIndex] == incrementalString[charIndex - 1]
      ) {
        charIndex++;
      }
      // If all letters in incremental string are same, just try to match the first one
      if (charIndex == length) {
        length = 1;
        incrementalString = incrementalString.substring(0, length);
      }

      var keyCol = this.columns.getKeyColumn();
      var rowCount = this.view.rowCount;
      var start = 1;

      var c = this.currentIndex;
      if (length > 1) {
        start = 0;
        if (c < 0) {
          c = 0;
        }
      }

      for (var i = 0; i < rowCount; i++) {
        var l = (i + start + c) % rowCount;
        var cellText = this.view.getCellText(l, keyCol);
        cellText = cellText.substring(0, length).toLowerCase();
        if (cellText == incrementalString) {
          return l;
        }
      }
      return -1;
    }

    startEditing(row, column) {
      if (!this.editable) {
        return false;
      }
      if (row < 0 || row >= this.view.rowCount || !column) {
        return false;
      }
      if (column.type !== window.TreeColumn.TYPE_TEXT) {
        return false;
      }
      if (column.cycler || !this.view.isEditable(row, column)) {
        return false;
      }

      // Beyond this point, we are going to edit the cell.
      if (this._editingColumn) {
        this.stopEditing();
      }

      var input = this.inputField;

      this.ensureCellIsVisible(row, column);

      // Get the coordinates of the text inside the cell.
      var textRect = this.getCoordsForCellItem(row, column, "text");

      // Get the coordinates of the cell itself.
      var cellRect = this.getCoordsForCellItem(row, column, "cell");

      // Calculate the top offset of the textbox.
      var style = window.getComputedStyle(input);
      var topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop);
      input.style.top = `${textRect.y - topadj}px`;

      // The left side of the textbox is aligned to the left side of the text
      // in LTR mode, and left side of the cell in RTL mode.
      let left = style.direction == "rtl" ? cellRect.x : textRect.x;
      let scrollbarWidth = window.windowUtils.getBoundsWithoutFlushing(
        this.#verticalScrollbar
      ).width;
      // Note: this won't be quite right in RTL for trees using twisties
      // or indentation. bug 1708159 tracks fixing the implementation
      // of getCoordsForCellItem which we called above so it provides
      // better numbers in those cases.
      let widthdiff = Math.abs(textRect.x - cellRect.x);
      // The cell width spans the vertical scrollbar's gutter. In LTR the
      // scrollbar is on the trailing edge, so reserve its width to keep the
      // input and its focus outline from overflowing the tree's clipped edge
      // (bug 1805367). RTL positioning is offset via `left` above and is
      // tracked separately in bug 1708159.
      if (style.direction == "rtl") {
        widthdiff -= scrollbarWidth;
      } else {
        widthdiff += scrollbarWidth;
      }

      input.style.left = `${left}px`;
      input.style.height = `${
        textRect.height +
        topadj +
        parseInt(style.borderBottomWidth) +
        parseInt(style.paddingBottom)
      }px`;
      input.style.width = `${cellRect.width - widthdiff}px`;
      input.hidden = false;

      input.value = this.view.getCellText(row, column);

      input.select();
      input.focus();

      this._editingRow = row;
      this._editingColumn = column;
      this.setAttribute("editing", "true");

      this.invalidateCell(row, column);
      return true;
    }

    stopEditing(accept) {
      if (!this._editingColumn) {
        return;
      }

      var input = this.inputField;
      var editingRow = this._editingRow;
      var editingColumn = this._editingColumn;
      this._editingRow = -1;
      this._editingColumn = null;

      // `this.view` could be null if the tree was hidden before we were called.
      if (accept && this.view) {
        var value = input.value;
        this.view.setCellText(editingRow, editingColumn, value);
      }
      input.hidden = true;
      input.value = "";
      this.removeAttribute("editing");
    }

    _moveByOffset(offset, edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (event.getModifierState("Accel") && this.view.selection.single) {
        this.scrollByLines(offset);
        return;
      }

      var c = this.currentIndex + offset;
      if (offset > 0 ? c > edge : c < edge) {
        if (
          this.view.selection.isSelected(edge) &&
          this.view.selection.count <= 1
        ) {
          return;
        }
        c = edge;
      }

      if (!event.getModifierState("Accel")) {
        this.view.selection.timedSelect(c, this._selectDelay);
      }
      // Ctrl+Up/Down moves the anchor without selecting
      else {
        this.currentIndex = c;
      }
      this.ensureRowIsVisible(c);
    }

    _moveByOffsetShift(offset, edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (this.view.selection.single) {
        this.scrollByLines(offset);
        return;
      }

      if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
        this.view.selection.timedSelect(0, this._selectDelay);
        return;
      }

      var c = this.currentIndex;
      if (c == -1) {
        c = 0;
      }

      if (c == edge) {
        if (this.view.selection.isSelected(c)) {
          return;
        }
      }

      // Extend the selection from the existing pivot, if any
      this.view.selection.rangedSelect(
        -1,
        c + offset,
        event.getModifierState("Accel")
      );
      this.ensureRowIsVisible(c + offset);
    }

    _moveByPage(offset, edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (this.pageUpOrDownMovesSelection == event.getModifierState("Accel")) {
        this.scrollByPages(offset);
        return;
      }

      if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
        this.view.selection.timedSelect(0, this._selectDelay);
        return;
      }

      var c = this.currentIndex;
      if (c == -1) {
        return;
      }

      if (c == edge && this.view.selection.isSelected(c)) {
        this.ensureRowIsVisible(c);
        return;
      }
      var i = this.getFirstVisibleRow();
      var p = this.getPageLength();

      if (offset > 0) {
        i += p - 1;
        if (c >= i) {
          i = c + p;
          this.ensureRowIsVisible(i > edge ? edge : i);
        }
        i = i > edge ? edge : i;
      } else if (c <= i) {
        i = c <= p ? 0 : c - p;
        this.ensureRowIsVisible(i);
      }
      this.view.selection.timedSelect(i, this._selectDelay);
    }

    _moveByPageShift(offset, edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (
        this.view.rowCount == 1 &&
        !this.view.selection.isSelected(0) &&
        !(this.pageUpOrDownMovesSelection == event.getModifierState("Accel"))
      ) {
        this.view.selection.timedSelect(0, this._selectDelay);
        return;
      }

      if (this.view.selection.single) {
        return;
      }

      var c = this.currentIndex;
      if (c == -1) {
        return;
      }
      if (c == edge && this.view.selection.isSelected(c)) {
        this.ensureRowIsVisible(edge);
        return;
      }
      var i = this.getFirstVisibleRow();
      var p = this.getPageLength();

      if (offset > 0) {
        i += p - 1;
        if (c >= i) {
          i = c + p;
          this.ensureRowIsVisible(i > edge ? edge : i);
        }
        // Extend the selection from the existing pivot, if any
        this.view.selection.rangedSelect(
          -1,
          i > edge ? edge : i,
          event.getModifierState("Accel")
        );
      } else {
        if (c <= i) {
          i = c <= p ? 0 : c - p;
          this.ensureRowIsVisible(i);
        }
        // Extend the selection from the existing pivot, if any
        this.view.selection.rangedSelect(
          -1,
          i,
          event.getModifierState("Accel")
        );
      }
    }

    _moveToEdge(edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (
        this.view.selection.isSelected(edge) &&
        this.view.selection.count == 1
      ) {
        this.currentIndex = edge;
        return;
      }

      // Normal behaviour is to select the first/last row
      if (!event.getModifierState("Accel")) {
        this.view.selection.timedSelect(edge, this._selectDelay);
      }
      // In a multiselect tree Ctrl+Home/End moves the anchor
      else if (!this.view.selection.single) {
        this.currentIndex = edge;
      }

      this.ensureRowIsVisible(edge);
    }

    _moveToEdgeShift(edge, event) {
      event.preventDefault();

      if (this.view.rowCount == 0) {
        return;
      }

      if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
        this.view.selection.timedSelect(0, this._selectDelay);
        return;
      }

      if (
        this.view.selection.single ||
        (this.view.selection.isSelected(edge) &&
          this.view.selection.isSelected(this.currentIndex))
      ) {
        return;
      }

      // Extend the selection from the existing pivot, if any.
      // -1 doesn't work here, so using currentIndex instead
      this.view.selection.rangedSelect(
        this.currentIndex,
        edge,
        event.getModifierState("Accel")
      );

      this.ensureRowIsVisible(edge);
    }

    _handleEnter() {
      if (this._editingColumn) {
        this.stopEditing(true);
        this.focus();
        return true;
      }

      return this.changeOpenState(this.currentIndex);
    }

    #verticalScrollbar = null;
    #lastScrollEventTimeStampMap = new Map();

    #canScroll(event) {
      const lastScrollEventTimeStamp = this.#lastScrollEventTimeStampMap.get(
        event.type
      );
      this.#lastScrollEventTimeStampMap.set(event.type, event.timeStamp);

      if (
        window.windowUtils.getWheelScrollTarget() ||
        event.axis == event.HORIZONTAL_AXIS ||
        (this.getAttribute("allowunderflowscroll") == "true" &&
          this.getAttribute("hidevscroll") == "true")
      ) {
        return false;
      }

      if (
        event.timeStamp - (lastScrollEventTimeStamp ?? 0) <
        Services.prefs.getIntPref("mousewheel.scroll_series_timeout")
      ) {
        // If the time difference of previous event does not over the timeout,
        // handle the event in tree as the same seies of events even if the
        // current position is edge.
        return true;
      }

      const curpos = this.scrollbarPosition;
      return (
        (event.detail < 0 && 0 < curpos) ||
        (event.detail > 0 && curpos < this.scrollbarMaxPosition)
      );
    }
  }

  MozXULElement.implementCustomInterface(MozTree, [
    Ci.nsIDOMXULMultiSelectControlElement,
  ]);
  customElements.define("tree", MozTree);
}
