mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-14 18:51:28 +00:00
934 lines
31 KiB
JavaScript
934 lines
31 KiB
JavaScript
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is the Firefox Sanitizer.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Ben Goodger.
|
|
* Portions created by the Initial Developer are Copyright (C) 2005
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Ben Goodger <ben@mozilla.org>
|
|
* Giorgio Maone <g.maone@informaction.com>
|
|
* Johnathan Nightingale <johnath@mozilla.com>
|
|
* Drew Willcoxon <adw@mozilla.com>
|
|
* Ehsan Akhgari <ehsan.akhgari@gmail.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
|
|
var gSanitizePromptDialog = {
|
|
|
|
get bundleBrowser()
|
|
{
|
|
if (!this._bundleBrowser)
|
|
this._bundleBrowser = document.getElementById("bundleBrowser");
|
|
return this._bundleBrowser;
|
|
},
|
|
|
|
get selectedTimespan()
|
|
{
|
|
var durList = document.getElementById("sanitizeDurationChoice");
|
|
return parseInt(durList.value);
|
|
},
|
|
|
|
get sanitizePreferences()
|
|
{
|
|
if (!this._sanitizePreferences) {
|
|
this._sanitizePreferences =
|
|
document.getElementById("sanitizePreferences");
|
|
}
|
|
return this._sanitizePreferences;
|
|
},
|
|
|
|
get warningBox()
|
|
{
|
|
return document.getElementById("sanitizeEverythingWarningBox");
|
|
},
|
|
|
|
init: function ()
|
|
{
|
|
// This is used by selectByTimespan() to determine if the window has loaded.
|
|
this._inited = true;
|
|
|
|
var s = new Sanitizer();
|
|
s.prefDomain = "privacy.cpd.";
|
|
|
|
let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
|
|
for (let i = 0; i < sanitizeItemList.length; i++) {
|
|
let prefItem = sanitizeItemList[i];
|
|
let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
|
|
if (!s.canClearItem(name)) {
|
|
prefItem.preference = null;
|
|
prefItem.checked = false;
|
|
prefItem.disabled = true;
|
|
}
|
|
}
|
|
|
|
document.documentElement.getButton("accept").label =
|
|
this.bundleBrowser.getString("sanitizeButtonOK");
|
|
|
|
if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
|
|
this.prepareWarning();
|
|
this.warningBox.hidden = false;
|
|
document.title =
|
|
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
|
|
}
|
|
else
|
|
this.warningBox.hidden = true;
|
|
},
|
|
|
|
selectByTimespan: function ()
|
|
{
|
|
// This method is the onselect handler for the duration dropdown. As a
|
|
// result it's called a couple of times before onload calls init().
|
|
if (!this._inited)
|
|
return;
|
|
|
|
var warningBox = this.warningBox;
|
|
|
|
// If clearing everything
|
|
if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
|
|
this.prepareWarning();
|
|
if (warningBox.hidden) {
|
|
warningBox.hidden = false;
|
|
window.resizeBy(0, warningBox.boxObject.height);
|
|
}
|
|
window.document.title =
|
|
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
|
|
return;
|
|
}
|
|
|
|
// If clearing a specific time range
|
|
if (!warningBox.hidden) {
|
|
window.resizeBy(0, -warningBox.boxObject.height);
|
|
warningBox.hidden = true;
|
|
}
|
|
window.document.title =
|
|
window.document.documentElement.getAttribute("noneverythingtitle");
|
|
},
|
|
|
|
sanitize: function ()
|
|
{
|
|
// Update pref values before handing off to the sanitizer (bug 453440)
|
|
this.updatePrefs();
|
|
var s = new Sanitizer();
|
|
s.prefDomain = "privacy.cpd.";
|
|
|
|
s.range = Sanitizer.getClearRange(this.selectedTimespan);
|
|
s.ignoreTimespan = !s.range;
|
|
|
|
try {
|
|
s.sanitize();
|
|
} catch (er) {
|
|
Components.utils.reportError("Exception during sanitize: " + er);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* If the panel that displays a warning when the duration is "Everything" is
|
|
* not set up, sets it up. Otherwise does nothing.
|
|
*
|
|
* @param aDontShowItemList Whether only the warning message should be updated.
|
|
* True means the item list visibility status should not
|
|
* be changed.
|
|
*/
|
|
prepareWarning: function (aDontShowItemList) {
|
|
// If the date and time-aware locale warning string is ever used again,
|
|
// initialize it here. Currently we use the no-visits warning string,
|
|
// which does not include date and time. See bug 480169 comment 48.
|
|
|
|
var warningStringID;
|
|
if (this.hasNonSelectedItems()) {
|
|
warningStringID = "sanitizeSelectedWarning";
|
|
if (!aDontShowItemList)
|
|
this.showItemList();
|
|
}
|
|
else {
|
|
warningStringID = "sanitizeEverythingWarning2";
|
|
}
|
|
|
|
var warningDesc = document.getElementById("sanitizeEverythingWarning");
|
|
warningDesc.textContent =
|
|
this.bundleBrowser.getString(warningStringID);
|
|
},
|
|
|
|
/**
|
|
* Called when the value of a preference element is synced from the actual
|
|
* pref. Enables or disables the OK button appropriately.
|
|
*/
|
|
onReadGeneric: function ()
|
|
{
|
|
var found = false;
|
|
|
|
// Find any other pref that's checked and enabled.
|
|
var i = 0;
|
|
while (!found && i < this.sanitizePreferences.childNodes.length) {
|
|
var preference = this.sanitizePreferences.childNodes[i];
|
|
|
|
found = !!preference.value &&
|
|
!preference.disabled;
|
|
i++;
|
|
}
|
|
|
|
try {
|
|
document.documentElement.getButton("accept").disabled = !found;
|
|
}
|
|
catch (e) { }
|
|
|
|
// Update the warning prompt if needed
|
|
this.prepareWarning(true);
|
|
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
|
|
* Because the type of this prefwindow is "child" -- and that's needed because
|
|
* without it the dialog has no OK and Cancel buttons -- the prefs are not
|
|
* updated on dialogaccept on platforms that don't support instant-apply
|
|
* (i.e., Windows). We must therefore manually set the prefs from their
|
|
* corresponding preference elements.
|
|
*/
|
|
updatePrefs : function ()
|
|
{
|
|
var tsPref = document.getElementById("privacy.sanitize.timeSpan");
|
|
Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan);
|
|
|
|
// Keep the pref for the download history in sync with the history pref.
|
|
document.getElementById("privacy.cpd.downloads").value =
|
|
document.getElementById("privacy.cpd.history").value;
|
|
|
|
// Now manually set the prefs from their corresponding preference
|
|
// elements.
|
|
var prefs = this.sanitizePreferences.rootBranch;
|
|
for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
|
|
var p = this.sanitizePreferences.childNodes[i];
|
|
prefs.setBoolPref(p.name, p.value);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if all of the history items have been selected like the default status.
|
|
*/
|
|
hasNonSelectedItems: function () {
|
|
let checkboxes = document.querySelectorAll("#itemList > [preference]");
|
|
for (let i = 0; i < checkboxes.length; ++i) {
|
|
let pref = document.getElementById(checkboxes[i].getAttribute("preference"));
|
|
if (!pref.value)
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Show the history items list.
|
|
*/
|
|
showItemList: function () {
|
|
var itemList = document.getElementById("itemList");
|
|
var expanderButton = document.getElementById("detailsExpander");
|
|
|
|
if (itemList.collapsed) {
|
|
expanderButton.className = "expander-up";
|
|
itemList.setAttribute("collapsed", "false");
|
|
if (document.documentElement.boxObject.height)
|
|
window.resizeBy(0, itemList.boxObject.height);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Hide the history items list.
|
|
*/
|
|
hideItemList: function () {
|
|
var itemList = document.getElementById("itemList");
|
|
var expanderButton = document.getElementById("detailsExpander");
|
|
|
|
if (!itemList.collapsed) {
|
|
expanderButton.className = "expander-down";
|
|
window.resizeBy(0, -itemList.boxObject.height);
|
|
itemList.setAttribute("collapsed", "true");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called by the item list expander button to toggle the list's visibility.
|
|
*/
|
|
toggleItemList: function ()
|
|
{
|
|
var itemList = document.getElementById("itemList");
|
|
|
|
if (itemList.collapsed)
|
|
this.showItemList();
|
|
else
|
|
this.hideItemList();
|
|
}
|
|
|
|
#ifdef CRH_DIALOG_TREE_VIEW
|
|
// A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR,
|
|
// Sanitizer.TIMESPAN_2HOURS, et al. This should match the value attribute
|
|
// of the sanitizeDurationCustom menuitem.
|
|
get TIMESPAN_CUSTOM()
|
|
{
|
|
return -1;
|
|
},
|
|
|
|
get placesTree()
|
|
{
|
|
if (!this._placesTree)
|
|
this._placesTree = document.getElementById("placesTree");
|
|
return this._placesTree;
|
|
},
|
|
|
|
init: function ()
|
|
{
|
|
// This is used by selectByTimespan() to determine if the window has loaded.
|
|
this._inited = true;
|
|
|
|
var s = new Sanitizer();
|
|
s.prefDomain = "privacy.cpd.";
|
|
|
|
let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
|
|
for (let i = 0; i < sanitizeItemList.length; i++) {
|
|
let prefItem = sanitizeItemList[i];
|
|
let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
|
|
if (!s.canClearItem(name)) {
|
|
prefItem.preference = null;
|
|
prefItem.checked = false;
|
|
prefItem.disabled = true;
|
|
}
|
|
}
|
|
|
|
document.documentElement.getButton("accept").label =
|
|
this.bundleBrowser.getString("sanitizeButtonOK");
|
|
|
|
this.selectByTimespan();
|
|
},
|
|
|
|
/**
|
|
* Sets up the hashes this.durationValsToRows, which maps duration values
|
|
* to rows in the tree, this.durationRowsToVals, which maps rows in
|
|
* the tree to duration values, and this.durationStartTimes, which maps
|
|
* duration values to their corresponding start times.
|
|
*/
|
|
initDurationDropdown: function ()
|
|
{
|
|
// First, calculate the start times for each duration.
|
|
this.durationStartTimes = {};
|
|
var durVals = [];
|
|
var durPopup = document.getElementById("sanitizeDurationPopup");
|
|
var durMenuitems = durPopup.childNodes;
|
|
for (let i = 0; i < durMenuitems.length; i++) {
|
|
let durMenuitem = durMenuitems[i];
|
|
let durVal = parseInt(durMenuitem.value);
|
|
if (durMenuitem.localName === "menuitem" &&
|
|
durVal !== Sanitizer.TIMESPAN_EVERYTHING &&
|
|
durVal !== this.TIMESPAN_CUSTOM) {
|
|
durVals.push(durVal);
|
|
let durTimes = Sanitizer.getClearRange(durVal);
|
|
this.durationStartTimes[durVal] = durTimes[0];
|
|
}
|
|
}
|
|
|
|
// Sort the duration values ascending. Because one tree index can map to
|
|
// more than one duration, this ensures that this.durationRowsToVals maps
|
|
// a row index to the largest duration possible in the code below.
|
|
durVals.sort();
|
|
|
|
// Now calculate the rows in the tree of the durations' start times. For
|
|
// each duration, we are looking for the node in the tree whose time is the
|
|
// smallest time greater than or equal to the duration's start time.
|
|
this.durationRowsToVals = {};
|
|
this.durationValsToRows = {};
|
|
var view = this.placesTree.view;
|
|
// For all rows in the tree except the grippy row...
|
|
for (let i = 0; i < view.rowCount - 1; i++) {
|
|
let unfoundDurVals = [];
|
|
let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer).
|
|
nodeForTreeIndex(i).time;
|
|
// For all durations whose rows have not yet been found in the tree, see
|
|
// if index i is their index. An index may map to more than one duration,
|
|
// in which case the final duration (the largest) wins.
|
|
for (let j = 0; j < durVals.length; j++) {
|
|
let durVal = durVals[j];
|
|
let durStartTime = this.durationStartTimes[durVal];
|
|
if (nodeTime < durStartTime) {
|
|
this.durationValsToRows[durVal] = i - 1;
|
|
this.durationRowsToVals[i - 1] = durVal;
|
|
}
|
|
else
|
|
unfoundDurVals.push(durVal);
|
|
}
|
|
durVals = unfoundDurVals;
|
|
}
|
|
|
|
// If any durations were not found above, then every node in the tree has a
|
|
// time greater than or equal to the duration. In other words, those
|
|
// durations include the entire tree (except the grippy row).
|
|
for (let i = 0; i < durVals.length; i++) {
|
|
let durVal = durVals[i];
|
|
this.durationValsToRows[durVal] = view.rowCount - 2;
|
|
this.durationRowsToVals[view.rowCount - 2] = durVal;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If the Places tree is not set up, sets it up. Otherwise does nothing.
|
|
*/
|
|
ensurePlacesTreeIsInited: function ()
|
|
{
|
|
if (this._placesTreeIsInited)
|
|
return;
|
|
|
|
this._placesTreeIsInited = true;
|
|
|
|
// Either "Last Four Hours" or "Today" will have the most history. If
|
|
// it's been more than 4 hours since today began, "Today" will. Otherwise
|
|
// "Last Four Hours" will.
|
|
var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY);
|
|
|
|
// If it's been less than 4 hours since today began, use the past 4 hours.
|
|
if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000
|
|
times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS);
|
|
}
|
|
|
|
var histServ = Cc["@mozilla.org/browser/nav-history-service;1"].
|
|
getService(Ci.nsINavHistoryService);
|
|
var query = histServ.getNewQuery();
|
|
query.beginTimeReference = query.TIME_RELATIVE_EPOCH;
|
|
query.beginTime = times[0];
|
|
query.endTimeReference = query.TIME_RELATIVE_EPOCH;
|
|
query.endTime = times[1];
|
|
var opts = histServ.getNewQueryOptions();
|
|
opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
|
|
opts.queryType = opts.QUERY_TYPE_HISTORY;
|
|
var result = histServ.executeQuery(query, opts);
|
|
|
|
var view = gContiguousSelectionTreeHelper.setTree(this.placesTree,
|
|
new PlacesTreeView());
|
|
result.addObserver(view, false);
|
|
this.initDurationDropdown();
|
|
},
|
|
|
|
/**
|
|
* Called on select of the duration dropdown and when grippyMoved() sets a
|
|
* duration based on the location of the grippy row. Selects all the nodes in
|
|
* the tree that are contained in the selected duration. If clearing
|
|
* everything, the warning panel is shown instead.
|
|
*/
|
|
selectByTimespan: function ()
|
|
{
|
|
// This method is the onselect handler for the duration dropdown. As a
|
|
// result it's called a couple of times before onload calls init().
|
|
if (!this._inited)
|
|
return;
|
|
|
|
var durDeck = document.getElementById("durationDeck");
|
|
var durList = document.getElementById("sanitizeDurationChoice");
|
|
var durVal = parseInt(durList.value);
|
|
var durCustom = document.getElementById("sanitizeDurationCustom");
|
|
|
|
// If grippy row is not at a duration boundary, show the custom menuitem;
|
|
// otherwise, hide it. Since the user cannot specify a custom duration by
|
|
// using the dropdown, this conditional is true only when this method is
|
|
// called onselect from grippyMoved(), so no selection need be made.
|
|
if (durVal === this.TIMESPAN_CUSTOM) {
|
|
durCustom.hidden = false;
|
|
return;
|
|
}
|
|
durCustom.hidden = true;
|
|
|
|
// If clearing everything, show the warning and change the dialog's title.
|
|
if (durVal === Sanitizer.TIMESPAN_EVERYTHING) {
|
|
this.prepareWarning();
|
|
durDeck.selectedIndex = 1;
|
|
window.document.title =
|
|
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
|
|
document.documentElement.getButton("accept").disabled = false;
|
|
return;
|
|
}
|
|
|
|
// Otherwise -- if clearing a specific time range -- select that time range
|
|
// in the tree.
|
|
this.ensurePlacesTreeIsInited();
|
|
durDeck.selectedIndex = 0;
|
|
window.document.title =
|
|
window.document.documentElement.getAttribute("noneverythingtitle");
|
|
var durRow = this.durationValsToRows[durVal];
|
|
gContiguousSelectionTreeHelper.rangedSelect(durRow);
|
|
gContiguousSelectionTreeHelper.scrollToGrippy();
|
|
|
|
// If duration is empty (there are no selected rows), disable the dialog's
|
|
// OK button.
|
|
document.documentElement.getButton("accept").disabled = durRow < 0;
|
|
},
|
|
|
|
sanitize: function ()
|
|
{
|
|
// Update pref values before handing off to the sanitizer (bug 453440)
|
|
this.updatePrefs();
|
|
var s = new Sanitizer();
|
|
s.prefDomain = "privacy.cpd.";
|
|
|
|
var durList = document.getElementById("sanitizeDurationChoice");
|
|
var durValue = parseInt(durList.value);
|
|
s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING;
|
|
|
|
// Set the sanitizer's time range if we're not clearing everything.
|
|
if (!s.ignoreTimespan) {
|
|
// If user selected a custom timespan, use that.
|
|
if (durValue === this.TIMESPAN_CUSTOM) {
|
|
var view = this.placesTree.view;
|
|
var now = Date.now() * 1000;
|
|
// We disable the dialog's OK button if there's no selection, but we'll
|
|
// handle that case just in... case.
|
|
if (view.selection.getRangeCount() === 0)
|
|
s.range = [now, now];
|
|
else {
|
|
var startIndexRef = {};
|
|
// Tree sorted by visit date DEscending, so start time time comes last.
|
|
view.selection.getRangeAt(0, {}, startIndexRef);
|
|
view.QueryInterface(Ci.nsINavHistoryResultTreeViewer);
|
|
var startNode = view.nodeForTreeIndex(startIndexRef.value);
|
|
s.range = [startNode.time, now];
|
|
}
|
|
}
|
|
// Otherwise use the predetermined range.
|
|
else
|
|
s.range = [this.durationStartTimes[durValue], Date.now() * 1000];
|
|
}
|
|
|
|
try {
|
|
s.sanitize();
|
|
} catch (er) {
|
|
Components.utils.reportError("Exception during sanitize: " + er);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* In order to mark the custom Places tree view and its nsINavHistoryResult
|
|
* for garbage collection, we need to break the reference cycle between the
|
|
* two.
|
|
*/
|
|
unload: function ()
|
|
{
|
|
let result = this.placesTree.getResult();
|
|
result.removeObserver(this.placesTree.view);
|
|
this.placesTree.view = null;
|
|
},
|
|
|
|
/**
|
|
* Called when the user moves the grippy by dragging it, clicking in the tree,
|
|
* or on keypress. Updates the duration dropdown so that it displays the
|
|
* appropriate specific or custom duration.
|
|
*
|
|
* @param aEventName
|
|
* The name of the event whose handler called this method, e.g.,
|
|
* "ondragstart", "onkeypress", etc.
|
|
* @param aEvent
|
|
* The event captured in the event handler.
|
|
*/
|
|
grippyMoved: function (aEventName, aEvent)
|
|
{
|
|
gContiguousSelectionTreeHelper[aEventName](aEvent);
|
|
var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1;
|
|
var durList = document.getElementById("sanitizeDurationChoice");
|
|
var durValue = parseInt(durList.value);
|
|
|
|
// Multiple durations can map to the same row. Don't update the dropdown
|
|
// if the current duration is valid for lastSelRow.
|
|
if ((durValue !== this.TIMESPAN_CUSTOM ||
|
|
lastSelRow in this.durationRowsToVals) &&
|
|
(durValue === this.TIMESPAN_CUSTOM ||
|
|
this.durationValsToRows[durValue] !== lastSelRow)) {
|
|
// Setting durList.value causes its onselect handler to fire, which calls
|
|
// selectByTimespan().
|
|
if (lastSelRow in this.durationRowsToVals)
|
|
durList.value = this.durationRowsToVals[lastSelRow];
|
|
else
|
|
durList.value = this.TIMESPAN_CUSTOM;
|
|
}
|
|
|
|
// If there are no selected rows, disable the dialog's OK button.
|
|
document.documentElement.getButton("accept").disabled = lastSelRow < 0;
|
|
}
|
|
#endif
|
|
|
|
};
|
|
|
|
|
|
#ifdef CRH_DIALOG_TREE_VIEW
|
|
/**
|
|
* A helper for handling contiguous selection in the tree.
|
|
*/
|
|
var gContiguousSelectionTreeHelper = {
|
|
|
|
/**
|
|
* Gets the tree associated with this helper.
|
|
*/
|
|
get tree()
|
|
{
|
|
return this._tree;
|
|
},
|
|
|
|
/**
|
|
* Sets the tree that this module handles. The tree is assigned a new view
|
|
* that is equipped to handle contiguous selection. You can pass in an
|
|
* object that will be used as the prototype of the new view. Otherwise
|
|
* the tree's current view is used as the prototype.
|
|
*
|
|
* @param aTreeElement
|
|
* The tree element
|
|
* @param aProtoTreeView
|
|
* If defined, this will be used as the prototype of the tree's new
|
|
* view
|
|
* @return The new view
|
|
*/
|
|
setTree: function CSTH_setTree(aTreeElement, aProtoTreeView)
|
|
{
|
|
this._tree = aTreeElement;
|
|
var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view);
|
|
aTreeElement.view = newView;
|
|
return newView;
|
|
},
|
|
|
|
/**
|
|
* The index of the row that the grippy occupies. Note that the index of the
|
|
* last selected row is getGrippyRow() - 1. If getGrippyRow() is 0, then
|
|
* no selection exists.
|
|
*
|
|
* @return The row index of the grippy
|
|
*/
|
|
getGrippyRow: function CSTH_getGrippyRow()
|
|
{
|
|
var sel = this.tree.view.selection;
|
|
var rangeCount = sel.getRangeCount();
|
|
if (rangeCount === 0)
|
|
return 0;
|
|
if (rangeCount !== 1) {
|
|
throw "contiguous selection tree helper: getGrippyRow called with " +
|
|
"multiple selection ranges";
|
|
}
|
|
var max = {};
|
|
sel.getRangeAt(0, {}, max);
|
|
return max.value + 1;
|
|
},
|
|
|
|
/**
|
|
* Helper function for the dragover event. Your dragover listener should
|
|
* call this. It updates the selection in the tree under the mouse.
|
|
*
|
|
* @param aEvent
|
|
* The observed dragover event
|
|
*/
|
|
ondragover: function CSTH_ondragover(aEvent)
|
|
{
|
|
// Without this when dragging on Windows the mouse cursor is a "no" sign.
|
|
// This makes it a drop symbol.
|
|
var ds = Cc["@mozilla.org/widget/dragservice;1"].
|
|
getService(Ci.nsIDragService).
|
|
getCurrentSession();
|
|
ds.canDrop = true;
|
|
ds.dragAction = 0;
|
|
|
|
var tbo = this.tree.treeBoxObject;
|
|
aEvent.QueryInterface(Ci.nsIDOMMouseEvent);
|
|
var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
|
|
|
|
if (hoverRow < 0)
|
|
return;
|
|
|
|
this.rangedSelect(hoverRow - 1);
|
|
},
|
|
|
|
/**
|
|
* Helper function for the dragstart event. Your dragstart listener should
|
|
* call this. It starts a drag session.
|
|
*
|
|
* @param aEvent
|
|
* The observed dragstart event
|
|
*/
|
|
ondragstart: function CSTH_ondragstart(aEvent)
|
|
{
|
|
var tbo = this.tree.treeBoxObject;
|
|
var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
|
|
|
|
if (clickedRow !== this.getGrippyRow())
|
|
return;
|
|
|
|
// This part is a hack. What we really want is a grab and slide, not
|
|
// drag and drop. Start a move drag session with dummy data and a
|
|
// dummy region. Set the region's coordinates to (Infinity, Infinity)
|
|
// so it's drawn offscreen and its size to (1, 1).
|
|
var arr = Cc["@mozilla.org/supports-array;1"].
|
|
createInstance(Ci.nsISupportsArray);
|
|
var trans = Cc["@mozilla.org/widget/transferable;1"].
|
|
createInstance(Ci.nsITransferable);
|
|
trans.setTransferData('dummy-flavor', null, 0);
|
|
arr.AppendElement(trans);
|
|
var reg = Cc["@mozilla.org/gfx/region;1"].
|
|
createInstance(Ci.nsIScriptableRegion);
|
|
reg.setToRect(Infinity, Infinity, 1, 1);
|
|
var ds = Cc["@mozilla.org/widget/dragservice;1"].
|
|
getService(Ci.nsIDragService);
|
|
ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE);
|
|
},
|
|
|
|
/**
|
|
* Helper function for the keypress event. Your keypress listener should
|
|
* call this. Users can use Up, Down, Page Up/Down, Home, and End to move
|
|
* the bottom of the selection window.
|
|
*
|
|
* @param aEvent
|
|
* The observed keypress event
|
|
*/
|
|
onkeypress: function CSTH_onkeypress(aEvent)
|
|
{
|
|
var grippyRow = this.getGrippyRow();
|
|
var tbo = this.tree.treeBoxObject;
|
|
var rangeEnd;
|
|
switch (aEvent.keyCode) {
|
|
case aEvent.DOM_VK_HOME:
|
|
rangeEnd = 0;
|
|
break;
|
|
case aEvent.DOM_VK_PAGE_UP:
|
|
rangeEnd = grippyRow - tbo.getPageLength();
|
|
break;
|
|
case aEvent.DOM_VK_UP:
|
|
rangeEnd = grippyRow - 2;
|
|
break;
|
|
case aEvent.DOM_VK_DOWN:
|
|
rangeEnd = grippyRow;
|
|
break;
|
|
case aEvent.DOM_VK_PAGE_DOWN:
|
|
rangeEnd = grippyRow + tbo.getPageLength();
|
|
break;
|
|
case aEvent.DOM_VK_END:
|
|
rangeEnd = this.tree.view.rowCount - 2;
|
|
break;
|
|
default:
|
|
return;
|
|
break;
|
|
}
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
// First, clip rangeEnd. this.rangedSelect() doesn't clip the range if we
|
|
// select past the ends of the tree.
|
|
if (rangeEnd < 0)
|
|
rangeEnd = -1;
|
|
else if (this.tree.view.rowCount - 2 < rangeEnd)
|
|
rangeEnd = this.tree.view.rowCount - 2;
|
|
|
|
// Next, (de)select.
|
|
this.rangedSelect(rangeEnd);
|
|
|
|
// Finally, scroll the tree. We always want one row above and below the
|
|
// grippy row to be visible if possible.
|
|
if (rangeEnd < grippyRow) // moved up
|
|
tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd);
|
|
else { // moved down
|
|
if (rangeEnd + 2 < this.tree.view.rowCount)
|
|
tbo.ensureRowIsVisible(rangeEnd + 2);
|
|
else if (rangeEnd + 1 < this.tree.view.rowCount)
|
|
tbo.ensureRowIsVisible(rangeEnd + 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper function for the mousedown event. Your mousedown listener should
|
|
* call this. Users can click on individual rows to make the selection
|
|
* jump to them immediately.
|
|
*
|
|
* @param aEvent
|
|
* The observed mousedown event
|
|
*/
|
|
onmousedown: function CSTH_onmousedown(aEvent)
|
|
{
|
|
var tbo = this.tree.treeBoxObject;
|
|
var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
|
|
|
|
if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount)
|
|
return;
|
|
|
|
if (clickedRow < this.getGrippyRow())
|
|
this.rangedSelect(clickedRow);
|
|
else if (clickedRow > this.getGrippyRow())
|
|
this.rangedSelect(clickedRow - 1);
|
|
},
|
|
|
|
/**
|
|
* Selects range [0, aEndRow] in the tree. The grippy row will then be at
|
|
* index aEndRow + 1. aEndRow may be -1, in which case the selection is
|
|
* cleared and the grippy row will be at index 0.
|
|
*
|
|
* @param aEndRow
|
|
* The range [0, aEndRow] will be selected.
|
|
*/
|
|
rangedSelect: function CSTH_rangedSelect(aEndRow)
|
|
{
|
|
var tbo = this.tree.treeBoxObject;
|
|
if (aEndRow < 0)
|
|
this.tree.view.selection.clearSelection();
|
|
else
|
|
this.tree.view.selection.rangedSelect(0, aEndRow, false);
|
|
tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow());
|
|
},
|
|
|
|
/**
|
|
* Scrolls the tree so that the grippy row is in the center of the view.
|
|
*/
|
|
scrollToGrippy: function CSTH_scrollToGrippy()
|
|
{
|
|
var rowCount = this.tree.view.rowCount;
|
|
var tbo = this.tree.treeBoxObject;
|
|
var pageLen = tbo.getPageLength() ||
|
|
parseInt(this.tree.getAttribute("rows")) ||
|
|
10;
|
|
|
|
// All rows fit on a single page.
|
|
if (rowCount <= pageLen)
|
|
return;
|
|
|
|
var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0);
|
|
|
|
// Grippy row is in first half of first page.
|
|
if (scrollToRow < 0)
|
|
scrollToRow = 0;
|
|
|
|
// Grippy row is in last half of last page.
|
|
else if (rowCount < scrollToRow + pageLen)
|
|
scrollToRow = rowCount - pageLen;
|
|
|
|
tbo.scrollToRow(scrollToRow);
|
|
},
|
|
|
|
/**
|
|
* Creates a new tree view suitable for contiguous selection. If
|
|
* aProtoTreeView is specified, it's used as the new view's prototype.
|
|
* Otherwise the tree's current view is used as the prototype.
|
|
*
|
|
* @param aProtoTreeView
|
|
* Used as the new view's prototype if specified
|
|
*/
|
|
_makeTreeView: function CSTH__makeTreeView(aProtoTreeView)
|
|
{
|
|
var atomServ = Cc["@mozilla.org/atom-service;1"].
|
|
getService(Ci.nsIAtomService);
|
|
|
|
var view = aProtoTreeView;
|
|
var that = this;
|
|
|
|
//XXXadw: When Alex gets the grippy icon done, this may or may not change,
|
|
// depending on how we style it.
|
|
view.isSeparator = function CSTH_View_isSeparator(aRow)
|
|
{
|
|
return aRow === that.getGrippyRow();
|
|
};
|
|
|
|
// rowCount includes the grippy row.
|
|
view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount"));
|
|
view.__defineGetter__("rowCount",
|
|
function CSTH_View_rowCount()
|
|
{
|
|
return this._rowCount + 1;
|
|
});
|
|
|
|
// This has to do with visual feedback in the view itself, e.g., drawing
|
|
// a small line underneath the dropzone. Not what we want.
|
|
view.canDrop = function CSTH_View_canDrop() { return false; };
|
|
|
|
// No clicking headers to sort the tree or sort feedback on columns.
|
|
view.cycleHeader = function CSTH_View_cycleHeader() {};
|
|
view.sortingChanged = function CSTH_View_sortingChanged() {};
|
|
|
|
// Override a bunch of methods to account for the grippy row.
|
|
|
|
view._getCellProperties = view.getCellProperties;
|
|
view.getCellProperties =
|
|
function CSTH_View_getCellProperties(aRow, aCol, aProps)
|
|
{
|
|
var grippyRow = that.getGrippyRow();
|
|
if (aRow === grippyRow)
|
|
aProps.AppendElement(atomServ.getAtom("grippyRow"));
|
|
else if (aRow < grippyRow)
|
|
this._getCellProperties(aRow, aCol, aProps);
|
|
else
|
|
this._getCellProperties(aRow - 1, aCol, aProps);
|
|
};
|
|
|
|
view._getRowProperties = view.getRowProperties;
|
|
view.getRowProperties =
|
|
function CSTH_View_getRowProperties(aRow, aProps)
|
|
{
|
|
var grippyRow = that.getGrippyRow();
|
|
if (aRow === grippyRow)
|
|
aProps.AppendElement(atomServ.getAtom("grippyRow"));
|
|
else if (aRow < grippyRow)
|
|
this._getRowProperties(aRow, aProps);
|
|
else
|
|
this._getRowProperties(aRow - 1, aProps);
|
|
};
|
|
|
|
view._getCellText = view.getCellText;
|
|
view.getCellText =
|
|
function CSTH_View_getCellText(aRow, aCol)
|
|
{
|
|
var grippyRow = that.getGrippyRow();
|
|
if (aRow === grippyRow)
|
|
return "";
|
|
aRow = aRow < grippyRow ? aRow : aRow - 1;
|
|
return this._getCellText(aRow, aCol);
|
|
};
|
|
|
|
view._getImageSrc = view.getImageSrc;
|
|
view.getImageSrc =
|
|
function CSTH_View_getImageSrc(aRow, aCol)
|
|
{
|
|
var grippyRow = that.getGrippyRow();
|
|
if (aRow === grippyRow)
|
|
return "";
|
|
aRow = aRow < grippyRow ? aRow : aRow - 1;
|
|
return this._getImageSrc(aRow, aCol);
|
|
};
|
|
|
|
view.isContainer = function CSTH_View_isContainer(aRow) { return false; };
|
|
view.getParentIndex = function CSTH_View_getParentIndex(aRow) { return -1; };
|
|
view.getLevel = function CSTH_View_getLevel(aRow) { return 0; };
|
|
view.hasNextSibling = function CSTH_View_hasNextSibling(aRow, aAfterIndex)
|
|
{
|
|
return aRow < this.rowCount - 1;
|
|
};
|
|
|
|
return view;
|
|
}
|
|
};
|
|
#endif
|