/* 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. { 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 { constructor() { super(); this.addEventListener("command", (event) => { if (event.originalTarget == this) { var popup = this.querySelector("[anonid=\"popup\"]"); this.buildPopup(popup); popup.openPopup(this, "after_end"); } else { var tree = this.parentNode.parentNode; tree.stopEditing(true); var menuitem = this.querySelector("[anonid=\"menuitem\"]"); if (event.originalTarget == menuitem) { tree.columns.restoreNaturalOrder(); this.removeAttribute("ordinal"); tree._ensureColumnOrder(); } else { var colindex = event.originalTarget.getAttribute("colindex"); var column = tree.columns[colindex]; if (column) { var element = column.element; if (element.getAttribute("hidden") == "true") element.setAttribute("hidden", "false"); else element.setAttribute("hidden", "true"); } } } }); } connectedCallback() { if (this.delayConnectedCallback()) { return; } this.textContent = ""; this.appendChild(MozXULElement.parseXULToFragment(` `, ["chrome://global/locale/tree.dtd"])); } 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.createElement("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"); aPopup.insertBefore(popupChild, refChild); } } var hidden = !tree.enableColumnDrag; aPopup.querySelectorAll(":not([colindex])").forEach((e) => { e.hidden = hidden; }); } } customElements.define("treecolpicker", MozTreecolPicker); class MozTreecol extends MozElements.BaseControl { static get inheritedAttributes() { return { ".treecol-sortdirection": "sortdirection,hidden=hideheader", ".treecol-text": "value=label,crop", }; } get content() { return MozXULElement.parseXULToFragment(` `); } constructor() { super(); this.addEventListener("mousedown", (event) => { if (event.button != 0) { return; } if (this.parentNode.parentNode.enableColumnDrag) { var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; var cols = this.parentNode.getElementsByTagNameNS(xulns, "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].boxObject.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 (/Win/.test(navigator.platform) && event.detail % 2 == 0) return; var tree = this.parentNode.parentNode; if (tree.columns) { tree.view.cycleHeader(tree.columns.getColumnFor(this)); } }); } markTreeDirty() { this.parentNode.parentNode._columnsDirty = true; } connectedCallback() { if (this.delayConnectedCallback()) { return; } if (!this.isRunningDelayedConnectedCallback) { this.markTreeDirty(); } this.textContent = ""; this.appendChild(this.content); this.initializeAttributeInheritance(); } set ordinal(val) { this.setAttribute("ordinal", val); return val; } get ordinal() { var val = this.getAttribute("ordinal"); if (val == "") return "1"; return "" + (val == "0" ? 0 : parseInt(val)); } get _previousVisibleColumn() { var sib = this.boxObject.previousSibling; while (sib) { if (sib.localName == "treecol" && sib.boxObject.width > 0 && sib.parentNode == this.parentNode) return sib; sib = sib.boxObject.previousSibling; } 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", }; } connectedCallback() { if (this.delayConnectedCallback()) { return; } this.setAttribute("slot", "treecols"); if (!this.querySelector("treecolpicker")) { this.appendChild(MozXULElement.parseXULToFragment(` `)); this.initializeAttributeInheritance(); } // Set resizeafter="farthest" on the splitters if nothing else has been // specified. Array.forEach(this.getElementsByTagName("splitter"), function(splitter) { if (!splitter.hasAttribute("resizeafter")) splitter.setAttribute("resizeafter", "farthest"); }); } } customElements.define("treecols", MozTreecols); class MozTree extends BaseControlMixin(MozElementMixin(XULTreeElement)) { constructor() { super(); 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.addEventListener("MozMousePixelScroll", (event) => { if (!(this.getAttribute("allowunderflowscroll") == "true" && this.getAttribute("hidevscroll") == "true")) event.preventDefault(); }); this.addEventListener("DOMMouseScroll", (event) => { if (!(this.getAttribute("allowunderflowscroll") == "true" && this.getAttribute("hidevscroll") == "true")) event.preventDefault(); if (this._editingColumn) return; if (event.axis == event.HORIZONTAL_AXIS) 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; }); this.addEventListener("blur", (event) => { if (event.originalTarget == this.inputField.inputField) this.stopEditing(true); }, true); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_RETURN) { return; } if (this._handleEnter(event)) { event.stopPropagation(); event.preventDefault(); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_ESCAPE) { return; } if (this._editingColumn) { this.stopEditing(false); this.focus(); event.stopPropagation(); event.preventDefault(); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_LEFT) { return; } if (this._editingColumn) return; var row = this.currentIndex; if (row < 0) return; if (this.changeOpenState(this.currentIndex, false)) { event.preventDefault(); return; } var parentIndex = this.view.getParentIndex(this.currentIndex); if (parentIndex >= 0) { this.view.selection.select(parentIndex); this.ensureRowIsVisible(parentIndex); event.preventDefault(); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_RIGHT) { return; } if (this._editingColumn) return; var row = this.currentIndex; if (row < 0) return; if (this.changeOpenState(row, true)) { event.preventDefault(); return; } var c = row + 1; var 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(); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_UP) { return; } if (this._editingColumn) return; if (event.getModifierState("Shift")) { this._moveByOffsetShift(-1, 0, event); } else { this._moveByOffset(-1, 0, event); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_DOWN) { return; } 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); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_PAGE_UP) { return; } if (this._editingColumn) return; if (event.getModifierState("Shift")) { this._moveByPageShift(-1, 0, event); } else { this._moveByPage(-1, 0, event); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_PAGE_DOWN) { return; } 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); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_HOME) { return; } if (this._editingColumn) return; if (event.getModifierState("Shift")) { this._moveToEdgeShift(0, event); } else { this._moveToEdge(0, event); } }); this.addEventListener("keydown", (event) => { if (event.keyCode != KeyEvent.DOM_VK_END) { return; } if (this._editingColumn) return; if (event.getModifierState("Shift")) { this._moveToEdgeShift(this.view.rowCount - 1, event); } else { this._moveToEdge(this.view.rowCount - 1, event); } }); 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(); } }); this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(MozXULElement.parseXULToFragment(` `)); } static get observedAttributes() { return [ "hidehscroll", "hidevscroll", ]; } attributeChangedCallback(name, oldValue, newValue) { if (this.isConnectedAndReady && oldValue != newValue) { this._updateAttributes(); } } _updateAttributes() { for (let [ el, attrs ] of this._inheritedAttributeMap.entries()) { for (let attr of attrs) { this.inheritAttribute(el, attr); } } } get _inheritedAttributeMap() { if (!this.__inheritedAttributeMap) { this.__inheritedAttributeMap = new Map(); for (let el of this.shadowRoot.querySelectorAll("[inherits]")) { this.__inheritedAttributeMap.set(el, el.getAttribute("inherits").split(",")); } } return this.__inheritedAttributeMap; } connectedCallback() { if (this.delayConnectedCallback()) { return; } this.setAttribute("hidevscroll", "true"); this.setAttribute("hidehscroll", "true"); this.setAttribute("clickthrough", "never"); this._updateAttributes(); this.pageUpOrDownMovesSelection = !/Mac/.test(navigator.platform); this._inputField = null; this._editingRow = -1; this._editingColumn = null; this._columnsDirty = true; this._lastKeyTime = 0; this._incrementalString = ""; this._touchY = -1; } get body() { return this.treeBody; } set editable(val) { if (val) this.setAttribute("editable", "true"); else this.removeAttribute("editable"); return val; } get editable() { return this.getAttribute("editable") == "true"; } /** * ///////////////// nsIDOMXULSelectControlElement ///////////////// ///////////////// nsIDOMXULMultiSelectControlElement ///////////////// */ set selType(val) { this.setAttribute("seltype", val); return val; } get selType() { return this.getAttribute("seltype"); } set currentIndex(val) { if (this.view) return this.view.selection.currentIndex = val; return val; } get currentIndex() { return this.view ? this.view.selection.currentIndex : -1; } set keepCurrentInView(val) { if (val) this.setAttribute("keepcurrentinview", "true"); else this.removeAttribute("keepcurrentinview"); return val; } get keepCurrentInView() { return (this.getAttribute("keepcurrentinview") == "true"); } set enableColumnDrag(val) { if (val) this.setAttribute("enableColumnDrag", "true"); else this.removeAttribute("enableColumnDrag"); return val; } 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), true); } return this._inputField; } set disableKeyNavigation(val) { if (val) this.setAttribute("disableKeyNavigation", "true"); else this.removeAttribute("disableKeyNavigation"); return val; } 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; } _ensureColumnOrder() { if (!this._columnsDirty) return; 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 = []; var i; for (var col = this.columns.getFirstColumn(); col; col = col.getNext()) cols.push(col.element); for (i = 0; i < cols.length; ++i) cols[i].setAttribute("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 (i = 0; i < splitters.length; ++i) splitters[i].setAttribute("ordinal", (i + 1) * 2); } this._columnsDirty = false; } _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; } } _getColumnAtX(aX, aThresh, aPos) { var isRTL = document.defaultView.getComputedStyle(this) .direction == "rtl"; 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.boxObject.x; var adjustedX = aX + this.horizontalPosition; for (var i = 0; i < columns.length; ++i) { col = columns[i]; var cw = col.element.boxObject.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 && column.type != window.TreeColumn.TYPE_PASSWORD) 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.top = textRect.y - topadj; // 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. var left, widthdiff; if (style.direction == "rtl") { left = cellRect.x; widthdiff = cellRect.x - textRect.x; } else { left = textRect.x; widthdiff = textRect.x - cellRect.x; } input.left = left; input.height = textRect.height + topadj + parseInt(style.borderBottomWidth) + parseInt(style.paddingBottom); input.width = cellRect.width - widthdiff; input.hidden = false; input.value = this.view.getCellText(row, column); input.select(); input.inputField.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; if (accept) { 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); else // Ctrl+Up/Down moves the anchor without selecting 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); } } MozXULElement.implementCustomInterface(MozTree, [Ci.nsIDOMXULMultiSelectControlElement]); customElements.define("tree", MozTree); }