Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* 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 =
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 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>
`;
}
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 {
".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.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 ==
);
}
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 {
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`;
let scrollbarWidth = window.windowUtils.getBoundsWithoutFlushing(
this.#verticalScrollbar
).width;
// Note: this won't be quite right in RTL for trees using twisties
// of getCoordsForCellItem which we called above so it provides
// better numbers in those cases.
let textPadding = Math.max(0, textRect.x - cellRect.x);
// Since we are absolutely positioning the text input relative to the
// tree stack element, we need to ignore any negative values that are
// returned from getCoordsForCellItem. This will prevent the text
// input overlapping on the vertical scrollbar or overflowing past
// the tree's boundaries.
let left = Math.max(0, cellRect.x) + textPadding;
let width = cellRect.width;
// According to nsTreeBodyFrame::GetCoordsForCellItem,
// consumers need to deal with overflow and scrollbars. In LTR,
// if the cell we are trying to edit is in the last column,
// where a scrollbar could appear, we need to subtract that
// width to prevent overflow.
const isFinalColumn = column.index == column.columns.count - 1;
const isRTL = style.direction == "rtl";
if (isFinalColumn && !isRTL) {
// TODO: probably the '&& isRTL' check can be removed when we
width -= scrollbarWidth;
}
if (isRTL) {
// In RTL, the position is offset by the scrollbar, so we add the
// scrollbarWidth.
left += scrollbarWidth;
if (!isFinalColumn) {
// need an additional offset by scrollbarWidth, to account
// for the fact that the leftmost column fails to shrink by
// that amount when it should.
left += scrollbarWidth;
}
}
input.style.left = `${left}px`;
input.style.width = `${width}px`;
input.style.height = `${
textRect.height +
topadj +
parseInt(style.borderBottomWidth) +
parseInt(style.paddingBottom)
}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 = 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);
}