diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/content/widgets/tree.js | 1705 |
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); +} |