/* 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() {
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 ==
"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 {
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 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() {
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);
}