summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/tree.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/tree.js')
-rw-r--r--toolkit/content/widgets/tree.js1705
1 files changed, 1705 insertions, 0 deletions
diff --git a/toolkit/content/widgets/tree.js b/toolkit/content/widgets/tree.js
new file mode 100644
index 0000000000..322e42586e
--- /dev/null
+++ b/toolkit/content/widgets/tree.js
@@ -0,0 +1,1705 @@
+/* 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.getAttribute("hidden") != "true") {
+ 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(aEvent) {
+ 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">
+ <hbox flex="1" class="tree-bodybox">
+ <html:slot name="treechildren"></html:slot>
+ </hbox>
+ <scrollbar height="0" minwidth="0" minheight="0" orient="vertical"
+ class="hidevscroll-scrollbar scrollbar-topmost"
+ ></scrollbar>
+ </hbox>
+ <html:input class="tree-input" type="text" hidden="true"/>
+ </stack>
+ <hbox class="hidehscroll-box">
+ <scrollbar orient="horizontal" flex="1" increment="16" class="scrollbar-topmost" ></scrollbar>
+ <scrollcorner class="hidevscroll-scrollcorner"></scrollcorner>
+ </hbox>
+ `;
+ }
+
+ 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" });
+ let handledElements = this.constructor.fragment.querySelectorAll(
+ "scrollbar,scrollcorner"
+ );
+ let stopAndPrevent = e => {
+ e.stopPropagation();
+ e.preventDefault();
+ };
+ let stopProp = e => e.stopPropagation();
+ for (let el of handledElements) {
+ el.addEventListener("click", stopAndPrevent);
+ el.addEventListener("contextmenu", stopAndPrevent);
+ el.addEventListener("dblclick", stopProp);
+ el.addEventListener("command", stopProp);
+ }
+ this.shadowRoot.appendChild(this.constructor.fragment);
+
+ this.#verticalScrollbar = this.shadowRoot.querySelector(
+ "scrollbar[orient='vertical']"
+ );
+ }
+
+ static get inheritedAttributes() {
+ return {
+ ".hidehscroll-box": "collapsed=hidehscroll",
+ ".hidevscroll-scrollbar": "collapsed=hidevscroll",
+ ".hidevscroll-scrollcorner": "collapsed=hidevscroll",
+ };
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ if (!this._eventListenersSetup) {
+ this._eventListenersSetup = true;
+ this.setupEventListeners();
+ }
+
+ this.setAttribute("hidevscroll", "true");
+ this.setAttribute("hidehscroll", "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 == 1) {
+ this.setAttribute("hidehscroll", "true");
+ } else if (event.detail == 0) {
+ this.setAttribute("hidevscroll", "true");
+ }
+ event.stopPropagation();
+ });
+
+ this.addEventListener("overflow", event => {
+ if (event.target.tagName != "treechildren") {
+ return;
+ }
+ if (event.detail == 1) {
+ this.removeAttribute("hidehscroll");
+ } else 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", event => {
+ 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 {
+ 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", event => {
+ 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;
+ var adjustedX = aX + this.horizontalPosition;
+ 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 > adjustedX) {
+ 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 leftside 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) - 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(event) {
+ 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 = Number(this.#verticalScrollbar.getAttribute("curpos"));
+ return (
+ (event.detail < 0 && 0 < curpos) ||
+ (event.detail > 0 &&
+ curpos < Number(this.#verticalScrollbar.getAttribute("maxpos")))
+ );
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozTree, [
+ Ci.nsIDOMXULMultiSelectControlElement,
+ ]);
+ customElements.define("tree", MozTree);
+}