forked from mirrors/gecko-dev
516 lines
16 KiB
JavaScript
516 lines
16 KiB
JavaScript
/* 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/. */
|
|
|
|
"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._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(`
|
|
<image class="tree-columnpicker-icon"></image>
|
|
<menupopup anonid="popup">
|
|
<menuseparator anonid="menuseparator"></menuseparator>
|
|
<menuitem anonid="menuitem" label="&restoreColumnOrder.label;"></menuitem>
|
|
</menupopup>
|
|
`, ["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 observedAttributes() {
|
|
return [
|
|
"label",
|
|
"sortdirection",
|
|
"hideheader",
|
|
"crop",
|
|
];
|
|
}
|
|
|
|
get content() {
|
|
return MozXULElement.parseXULToFragment(`
|
|
<label class="treecol-text" flex="1" crop="right"></label>
|
|
<image class="treecol-sortdirection"></image>
|
|
`);
|
|
}
|
|
|
|
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._updateAttributes();
|
|
}
|
|
|
|
attributeChangedCallback() {
|
|
if (this.isConnectedAndReady) {
|
|
this._updateAttributes();
|
|
}
|
|
}
|
|
|
|
_updateAttributes() {
|
|
let image = this.querySelector(".treecol-sortdirection");
|
|
let label = this.querySelector(".treecol-text");
|
|
|
|
this.inheritAttribute(image, "sortdirection");
|
|
this.inheritAttribute(image, "hidden=hideheader");
|
|
this.inheritAttribute(label, "value=label");
|
|
|
|
// Don't remove the attribute on the child if it's los on the host.
|
|
if (this.hasAttribute("crop")) {
|
|
this.inheritAttribute(label, "crop");
|
|
}
|
|
}
|
|
|
|
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 {
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
if (!this.querySelector("treecolpicker")) {
|
|
this.appendChild(MozXULElement.parseXULToFragment(`
|
|
<treecolpicker class="treecol-image" fixed="true"></treecolpicker>
|
|
`));
|
|
}
|
|
|
|
let treecolpicker = this.querySelector("treecolpicker");
|
|
this.inheritAttribute(treecolpicker, "tooltiptext=pickertooltiptext");
|
|
|
|
// 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);
|
|
}
|