2011-09-26 09:59:23 -07:00
|
|
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
2011-10-11 06:11:20 -07:00
|
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
2012-05-21 04:12:37 -07:00
|
|
|
/* 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/. */
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
const Cu = Components.utils;
|
2012-06-01 12:39:00 -07:00
|
|
|
const Ci = Components.interfaces;
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
Cu.import("resource:///modules/domplate.jsm");
|
|
|
|
Cu.import("resource:///modules/InsideOutBox.jsm");
|
2011-11-18 09:36:16 -08:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2012-04-19 11:04:46 -07:00
|
|
|
Cu.import("resource:///modules/inspector.jsm");
|
2012-06-01 12:39:00 -07:00
|
|
|
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
2011-09-26 09:59:23 -07:00
|
|
|
|
2011-10-11 06:11:10 -07:00
|
|
|
var EXPORTED_SYMBOLS = ["TreePanel", "DOMHelpers"];
|
2011-09-26 09:59:23 -07:00
|
|
|
|
2011-09-26 10:05:40 -07:00
|
|
|
const INSPECTOR_URI = "chrome://browser/content/inspector.html";
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
/**
|
|
|
|
* TreePanel
|
|
|
|
* A container for the Inspector's HTML Tree Panel widget constructor function.
|
|
|
|
* @param aContext nsIDOMWindow (xulwindow)
|
|
|
|
* @param aIUI global InspectorUI object
|
|
|
|
*/
|
|
|
|
function TreePanel(aContext, aIUI) {
|
|
|
|
this._init(aContext, aIUI);
|
|
|
|
};
|
|
|
|
|
|
|
|
TreePanel.prototype = {
|
|
|
|
showTextNodesWithWhitespace: false,
|
|
|
|
id: "treepanel", // DO NOT LOCALIZE
|
2012-01-27 08:13:53 -08:00
|
|
|
_open: false,
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
/**
|
2011-09-26 10:05:40 -07:00
|
|
|
* The tree panel container element.
|
|
|
|
* @returns xul:panel|xul:vbox|null
|
|
|
|
* xul:panel is returned when the tree panel is not docked, or
|
|
|
|
* xul:vbox when when the tree panel is docked.
|
|
|
|
* null is returned when no container is available.
|
2011-09-26 09:59:23 -07:00
|
|
|
*/
|
|
|
|
get container()
|
|
|
|
{
|
2012-01-27 08:13:53 -08:00
|
|
|
return this.document.getElementById("inspector-tree-box");
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Main TreePanel boot-strapping method. Initialize the TreePanel with the
|
|
|
|
* originating context and the InspectorUI global.
|
|
|
|
* @param aContext nsIDOMWindow (xulwindow)
|
|
|
|
* @param aIUI global InspectorUI object
|
|
|
|
*/
|
|
|
|
_init: function TP__init(aContext, aIUI)
|
|
|
|
{
|
|
|
|
this.IUI = aIUI;
|
|
|
|
this.window = aContext;
|
|
|
|
this.document = this.window.document;
|
2012-01-27 08:13:53 -08:00
|
|
|
this.button =
|
|
|
|
this.IUI.chromeDoc.getElementById("inspector-treepanel-toolbutton");
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
domplateUtils.setDOM(this.window);
|
|
|
|
|
2011-10-11 06:11:10 -07:00
|
|
|
this.DOMHelpers = new DOMHelpers(this.window);
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
let isOpen = this.isOpen.bind(this);
|
|
|
|
|
|
|
|
this.editingEvents = {};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialization function for the TreePanel.
|
|
|
|
*/
|
|
|
|
initializeIFrame: function TP_initializeIFrame()
|
|
|
|
{
|
|
|
|
if (!this.initializingTreePanel || this.treeLoaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.treeBrowserDocument = this.treeIFrame.contentDocument;
|
|
|
|
this.treePanelDiv = this.treeBrowserDocument.createElement("div");
|
|
|
|
this.treeBrowserDocument.body.appendChild(this.treePanelDiv);
|
|
|
|
this.treePanelDiv.ownerPanel = this;
|
|
|
|
this.ioBox = new InsideOutBox(this, this.treePanelDiv);
|
|
|
|
this.ioBox.createObjectBox(this.IUI.win.document.documentElement);
|
|
|
|
this.treeLoaded = true;
|
2012-06-01 12:39:00 -07:00
|
|
|
this._boundTreeKeyPress = this.onTreeKeyPress.bind(this);
|
|
|
|
this.treeIFrame.addEventListener("keypress", this._boundTreeKeyPress.bind(this), true);
|
2011-09-26 09:59:23 -07:00
|
|
|
this.treeIFrame.addEventListener("click", this.onTreeClick.bind(this), false);
|
|
|
|
this.treeIFrame.addEventListener("dblclick", this.onTreeDblClick.bind(this), false);
|
2012-01-24 10:56:47 -08:00
|
|
|
this.treeIFrame.focus();
|
2011-09-26 09:59:23 -07:00
|
|
|
delete this.initializingTreePanel;
|
|
|
|
Services.obs.notifyObservers(null,
|
2011-09-26 10:11:22 -07:00
|
|
|
this.IUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, null);
|
2012-04-19 11:04:46 -07:00
|
|
|
if (this.pendingSelection) {
|
|
|
|
this.select(this.pendingSelection.node, this.pendingSelection.scroll);
|
|
|
|
delete this.pendingSelection;
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open the inspector's tree panel and initialize it.
|
|
|
|
*/
|
|
|
|
open: function TP_open()
|
|
|
|
{
|
2012-01-27 08:13:53 -08:00
|
|
|
if (this._open) {
|
2011-09-26 09:59:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-01-27 08:13:53 -08:00
|
|
|
this._open = true;
|
|
|
|
|
|
|
|
this.button.setAttribute("checked", true);
|
2011-09-26 09:59:23 -07:00
|
|
|
this.initializingTreePanel = true;
|
|
|
|
|
|
|
|
this.treeIFrame = this.document.getElementById("inspector-tree-iframe");
|
|
|
|
if (!this.treeIFrame) {
|
|
|
|
this.treeIFrame = this.document.createElement("iframe");
|
|
|
|
this.treeIFrame.setAttribute("id", "inspector-tree-iframe");
|
2011-09-26 10:05:40 -07:00
|
|
|
this.treeIFrame.flex = 1;
|
2011-09-26 09:59:23 -07:00
|
|
|
this.treeIFrame.setAttribute("type", "content");
|
2012-02-20 18:02:00 -08:00
|
|
|
this.treeIFrame.setAttribute("context", "inspector-node-popup");
|
2011-09-26 09:59:23 -07:00
|
|
|
}
|
|
|
|
|
2011-09-26 10:05:40 -07:00
|
|
|
let treeBox = null;
|
|
|
|
treeBox = this.document.createElement("vbox");
|
|
|
|
treeBox.id = "inspector-tree-box";
|
2012-02-23 09:41:43 -08:00
|
|
|
treeBox.state = "open";
|
2011-11-07 11:31:45 -08:00
|
|
|
try {
|
|
|
|
treeBox.height =
|
|
|
|
Services.prefs.getIntPref("devtools.inspector.htmlHeight");
|
|
|
|
} catch(e) {
|
|
|
|
treeBox.height = 112;
|
|
|
|
}
|
2012-01-24 10:56:47 -08:00
|
|
|
|
2011-11-07 11:31:45 -08:00
|
|
|
treeBox.minHeight = 64;
|
2011-10-21 03:17:40 -07:00
|
|
|
|
2012-02-23 09:41:43 -08:00
|
|
|
this.splitter = this.document.createElement("splitter");
|
|
|
|
this.splitter.id = "inspector-tree-splitter";
|
2012-05-09 06:50:33 -07:00
|
|
|
this.splitter.className = "devtools-horizontal-splitter";
|
2012-02-23 09:41:43 -08:00
|
|
|
|
|
|
|
let container = this.document.getElementById("appcontent");
|
|
|
|
container.appendChild(this.splitter);
|
|
|
|
container.appendChild(treeBox);
|
2011-10-21 03:17:40 -07:00
|
|
|
|
2011-09-26 10:05:40 -07:00
|
|
|
treeBox.appendChild(this.treeIFrame);
|
|
|
|
|
2012-01-27 08:13:53 -08:00
|
|
|
this._boundLoadedInitializeTreePanel = function loadedInitializeTreePanel()
|
2011-09-26 10:05:40 -07:00
|
|
|
{
|
|
|
|
this.treeIFrame.removeEventListener("load",
|
2012-01-27 08:13:53 -08:00
|
|
|
this._boundLoadedInitializeTreePanel, true);
|
|
|
|
delete this._boundLoadedInitializeTreePanel;
|
2011-09-26 10:05:40 -07:00
|
|
|
this.initializeIFrame();
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
this.treeIFrame.addEventListener("load",
|
2012-01-27 08:13:53 -08:00
|
|
|
this._boundLoadedInitializeTreePanel, true);
|
2011-09-26 10:05:40 -07:00
|
|
|
|
|
|
|
let src = this.treeIFrame.getAttribute("src");
|
|
|
|
if (src != INSPECTOR_URI) {
|
|
|
|
this.treeIFrame.setAttribute("src", INSPECTOR_URI);
|
|
|
|
} else {
|
|
|
|
this.treeIFrame.contentWindow.location.reload();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
/**
|
|
|
|
* Close the TreePanel.
|
|
|
|
*/
|
|
|
|
close: function TP_close()
|
|
|
|
{
|
2012-01-27 08:13:53 -08:00
|
|
|
this._open = false;
|
|
|
|
|
|
|
|
// Stop caring about the tree iframe load if it's in progress.
|
|
|
|
if (this._boundLoadedInitializeTreePanel) {
|
|
|
|
this.treeIFrame.removeEventListener("load",
|
|
|
|
this._boundLoadedInitializeTreePanel, true);
|
|
|
|
delete this._boundLoadedInitializeTreePanel;
|
2011-09-26 10:05:40 -07:00
|
|
|
}
|
|
|
|
|
2012-01-27 08:13:53 -08:00
|
|
|
this.button.removeAttribute("checked");
|
|
|
|
let treeBox = this.container;
|
|
|
|
Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height);
|
|
|
|
let treeBoxParent = treeBox.parentNode;
|
|
|
|
treeBoxParent.removeChild(this.splitter);
|
|
|
|
treeBoxParent.removeChild(treeBox);
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
if (this.treePanelDiv) {
|
|
|
|
this.treePanelDiv.ownerPanel = null;
|
|
|
|
let parent = this.treePanelDiv.parentNode;
|
|
|
|
parent.removeChild(this.treePanelDiv);
|
2012-06-01 12:39:00 -07:00
|
|
|
this.treeIFrame.removeEventListener("keypress", this._boundTreeKeyPress, true);
|
2011-09-26 09:59:23 -07:00
|
|
|
delete this.treePanelDiv;
|
|
|
|
delete this.treeBrowserDocument;
|
|
|
|
}
|
|
|
|
|
2012-04-19 11:04:46 -07:00
|
|
|
if (this.ioBox) {
|
|
|
|
this.ioBox.destroy();
|
|
|
|
delete this.ioBox;
|
|
|
|
}
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
this.treeLoaded = false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is the TreePanel open?
|
|
|
|
* @returns boolean
|
|
|
|
*/
|
|
|
|
isOpen: function TP_isOpen()
|
|
|
|
{
|
2012-01-27 08:13:53 -08:00
|
|
|
return this._open;
|
|
|
|
},
|
2011-09-26 10:05:40 -07:00
|
|
|
|
2012-01-27 08:13:53 -08:00
|
|
|
/**
|
|
|
|
* Toggle the TreePanel.
|
|
|
|
*/
|
|
|
|
toggle: function TP_toggle()
|
|
|
|
{
|
|
|
|
this.isOpen() ? this.close() : this.open();
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the ObjectBox for the given object.
|
|
|
|
* @param object nsIDOMNode
|
|
|
|
* @param isRoot boolean - Is this the root object?
|
|
|
|
* @returns InsideOutBox
|
|
|
|
*/
|
|
|
|
createObjectBox: function TP_createObjectBox(object, isRoot)
|
|
|
|
{
|
|
|
|
let tag = domplateUtils.getNodeTag(object);
|
|
|
|
if (tag)
|
|
|
|
return tag.replace({object: object}, this.treeBrowserDocument);
|
|
|
|
},
|
|
|
|
|
|
|
|
getParentObject: function TP_getParentObject(node)
|
|
|
|
{
|
2011-10-11 06:11:10 -07:00
|
|
|
return this.DOMHelpers.getParentObject(node);
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
getChildObject: function TP_getChildObject(node, index, previousSibling)
|
|
|
|
{
|
2011-10-11 06:11:10 -07:00
|
|
|
return this.DOMHelpers.getChildObject(node, index, previousSibling,
|
|
|
|
this.showTextNodesWithWhitespace);
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
getFirstChild: function TP_getFirstChild(node)
|
|
|
|
{
|
2011-10-11 06:11:10 -07:00
|
|
|
return this.DOMHelpers.getFirstChild(node);
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
getNextSibling: function TP_getNextSibling(node)
|
|
|
|
{
|
2011-10-11 06:11:10 -07:00
|
|
|
return this.DOMHelpers.getNextSibling(node);
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
|
|
// Event Handling
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle click events in the html tree panel.
|
|
|
|
* @param aEvent
|
|
|
|
* The mouse event.
|
|
|
|
*/
|
|
|
|
onTreeClick: function TP_onTreeClick(aEvent)
|
|
|
|
{
|
|
|
|
let node;
|
|
|
|
let target = aEvent.target;
|
|
|
|
let hitTwisty = false;
|
|
|
|
if (this.hasClass(target, "twisty")) {
|
|
|
|
node = this.getRepObject(aEvent.target.nextSibling);
|
|
|
|
hitTwisty = true;
|
|
|
|
} else {
|
|
|
|
node = this.getRepObject(aEvent.target);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node) {
|
|
|
|
if (hitTwisty) {
|
|
|
|
this.ioBox.toggleObject(node);
|
|
|
|
} else {
|
|
|
|
if (this.IUI.inspecting) {
|
|
|
|
this.IUI.stopInspecting(true);
|
|
|
|
} else {
|
2012-06-01 12:39:00 -07:00
|
|
|
this.navigate(node);
|
2011-09-26 09:59:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle double-click events in the html tree panel.
|
2012-03-09 02:49:00 -08:00
|
|
|
* Double-clicking an attribute name or value allows it to be edited.
|
2011-09-26 09:59:23 -07:00
|
|
|
* @param aEvent
|
|
|
|
* The mouse event.
|
|
|
|
*/
|
|
|
|
onTreeDblClick: function TP_onTreeDblClick(aEvent)
|
|
|
|
{
|
|
|
|
// if already editing an attribute value, double-clicking elsewhere
|
|
|
|
// in the tree is the same as a click, which dismisses the editor
|
|
|
|
if (this.editingContext)
|
|
|
|
this.closeEditor();
|
|
|
|
|
|
|
|
let target = aEvent.target;
|
|
|
|
|
2012-03-09 02:49:00 -08:00
|
|
|
if (!this.hasClass(target, "editable")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let repObj = this.getRepObject(target);
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
if (this.hasClass(target, "nodeValue")) {
|
|
|
|
let attrName = target.getAttribute("data-attributeName");
|
|
|
|
let attrVal = target.innerHTML;
|
|
|
|
|
2012-03-09 02:49:00 -08:00
|
|
|
this.editAttribute(target, repObj, attrName, attrVal);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.hasClass(target, "nodeName")) {
|
|
|
|
let attrName = target.innerHTML;
|
|
|
|
let attrValNode = target.nextSibling.nextSibling; // skip 2 (=)
|
|
|
|
|
|
|
|
if (attrValNode)
|
|
|
|
this.editAttribute(target, repObj, attrName, attrValNode.innerHTML);
|
2011-09-26 09:59:23 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-06-01 12:39:00 -07:00
|
|
|
navigate: function TP_navigate(node)
|
|
|
|
{
|
|
|
|
if (!node)
|
|
|
|
return;
|
|
|
|
this.ioBox.select(node, false, false, true);
|
|
|
|
|
|
|
|
if (this.IUI.highlighter.isNodeHighlightable(node)) {
|
|
|
|
this.IUI.select(node, true, false, "treepanel");
|
|
|
|
this.IUI.highlighter.highlight(node);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onTreeKeyPress: function TP_onTreeKeyPress(aEvent)
|
|
|
|
{
|
|
|
|
let handled = true;
|
|
|
|
switch(aEvent.keyCode) {
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
|
|
|
|
this.ioBox.contractObjectBox(this.ioBox.selectedObjectBox);
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
|
|
|
|
this.ioBox.expandObjectBox(this.ioBox.selectedObjectBox);
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_UP:
|
|
|
|
this.navigate(this.ioBox.previousObject());
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
|
|
|
|
this.navigate(this.ioBox.nextObject());
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
|
|
|
|
this.navigate(this.ioBox.farPreviousObject(10));
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
|
|
|
|
this.navigate(this.ioBox.farNextObject(10));
|
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
|
|
|
|
this.navigate(this.ioBox.rootObject);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
handled = false;
|
|
|
|
}
|
|
|
|
if (handled) {
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
aEvent.preventDefault();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
/**
|
2012-03-09 02:49:00 -08:00
|
|
|
* Starts the editor for an attribute name or value.
|
2011-09-26 09:59:23 -07:00
|
|
|
* @param aAttrObj
|
2012-03-09 02:49:00 -08:00
|
|
|
* The DOM object representing the attribute name or value in the HTML
|
|
|
|
* Tree.
|
2011-09-26 09:59:23 -07:00
|
|
|
* @param aRepObj
|
|
|
|
* The original DOM (target) object being inspected/edited
|
|
|
|
* @param aAttrName
|
|
|
|
* The name of the attribute being edited
|
|
|
|
* @param aAttrVal
|
|
|
|
* The current value of the attribute being edited
|
|
|
|
*/
|
2012-03-09 02:49:00 -08:00
|
|
|
editAttribute:
|
|
|
|
function TP_editAttribute(aAttrObj, aRepObj, aAttrName, aAttrVal)
|
2011-09-26 09:59:23 -07:00
|
|
|
{
|
|
|
|
let editor = this.treeBrowserDocument.getElementById("attribute-editor");
|
|
|
|
let editorInput =
|
|
|
|
this.treeBrowserDocument.getElementById("attribute-editor-input");
|
|
|
|
let attrDims = aAttrObj.getBoundingClientRect();
|
|
|
|
// figure out actual viewable viewport dimensions (sans scrollbars)
|
|
|
|
let viewportWidth = this.treeBrowserDocument.documentElement.clientWidth;
|
|
|
|
let viewportHeight = this.treeBrowserDocument.documentElement.clientHeight;
|
|
|
|
|
|
|
|
// saves the editing context for use when the editor is saved/closed
|
|
|
|
this.editingContext = {
|
|
|
|
attrObj: aAttrObj,
|
|
|
|
repObj: aRepObj,
|
2012-03-09 02:49:00 -08:00
|
|
|
attrName: aAttrName,
|
|
|
|
attrValue: aAttrVal
|
2011-09-26 09:59:23 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// highlight attribute-value node in tree while editing
|
|
|
|
this.addClass(aAttrObj, "editingAttributeValue");
|
|
|
|
|
|
|
|
// show the editor
|
|
|
|
this.addClass(editor, "editing");
|
|
|
|
|
|
|
|
// offset the editor below the attribute-value node being edited
|
2012-03-09 02:49:00 -08:00
|
|
|
let editorVerticalOffset = 2;
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
// keep the editor comfortably within the bounds of the viewport
|
|
|
|
let editorViewportBoundary = 5;
|
|
|
|
|
|
|
|
// outer editor is sized based on the <input> box inside it
|
|
|
|
editorInput.style.width = Math.min(attrDims.width, viewportWidth -
|
|
|
|
editorViewportBoundary) + "px";
|
|
|
|
editorInput.style.height = Math.min(attrDims.height, viewportHeight -
|
|
|
|
editorViewportBoundary) + "px";
|
|
|
|
let editorDims = editor.getBoundingClientRect();
|
|
|
|
|
|
|
|
// calculate position for the editor according to the attribute node
|
|
|
|
let editorLeft = attrDims.left + this.treeIFrame.contentWindow.scrollX -
|
|
|
|
// center the editor against the attribute value
|
|
|
|
((editorDims.width - attrDims.width) / 2);
|
|
|
|
let editorTop = attrDims.top + this.treeIFrame.contentWindow.scrollY +
|
2012-03-09 02:49:00 -08:00
|
|
|
attrDims.height + editorVerticalOffset;
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
// but, make sure the editor stays within the visible viewport
|
|
|
|
editorLeft = Math.max(0, Math.min(
|
|
|
|
(this.treeIFrame.contentWindow.scrollX +
|
|
|
|
viewportWidth - editorDims.width),
|
|
|
|
editorLeft)
|
|
|
|
);
|
|
|
|
editorTop = Math.max(0, Math.min(
|
|
|
|
(this.treeIFrame.contentWindow.scrollY +
|
|
|
|
viewportHeight - editorDims.height),
|
|
|
|
editorTop)
|
|
|
|
);
|
|
|
|
|
|
|
|
// position the editor
|
|
|
|
editor.style.left = editorLeft + "px";
|
|
|
|
editor.style.top = editorTop + "px";
|
|
|
|
|
|
|
|
// set and select the text
|
2012-03-09 02:49:00 -08:00
|
|
|
if (this.hasClass(aAttrObj, "nodeValue")) {
|
|
|
|
editorInput.value = aAttrVal;
|
|
|
|
editorInput.select();
|
|
|
|
} else {
|
|
|
|
editorInput.value = aAttrName;
|
|
|
|
editorInput.select();
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
// listen for editor specific events
|
|
|
|
this.bindEditorEvent(editor, "click", function(aEvent) {
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
});
|
|
|
|
this.bindEditorEvent(editor, "dblclick", function(aEvent) {
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
});
|
|
|
|
this.bindEditorEvent(editor, "keypress",
|
|
|
|
this.handleEditorKeypress.bind(this));
|
|
|
|
|
|
|
|
// event notification
|
2011-09-26 10:11:22 -07:00
|
|
|
Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED,
|
2011-09-26 09:59:23 -07:00
|
|
|
null);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle binding an event handler for the editor.
|
|
|
|
* (saves the callback for easier unbinding later)
|
|
|
|
* @param aEditor
|
|
|
|
* The DOM object for the editor
|
|
|
|
* @param aEventName
|
|
|
|
* The name of the event to listen for
|
|
|
|
* @param aEventCallback
|
|
|
|
* The callback to bind to the event (and also to save for later
|
|
|
|
* unbinding)
|
|
|
|
*/
|
|
|
|
bindEditorEvent:
|
|
|
|
function TP_bindEditorEvent(aEditor, aEventName, aEventCallback)
|
|
|
|
{
|
|
|
|
this.editingEvents[aEventName] = aEventCallback;
|
|
|
|
aEditor.addEventListener(aEventName, aEventCallback, false);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle unbinding an event handler from the editor.
|
|
|
|
* (unbinds the previously bound and saved callback)
|
|
|
|
* @param aEditor
|
|
|
|
* The DOM object for the editor
|
|
|
|
* @param aEventName
|
|
|
|
* The name of the event being listened for
|
|
|
|
*/
|
|
|
|
unbindEditorEvent: function TP_unbindEditorEvent(aEditor, aEventName)
|
|
|
|
{
|
|
|
|
aEditor.removeEventListener(aEventName, this.editingEvents[aEventName],
|
|
|
|
false);
|
|
|
|
this.editingEvents[aEventName] = null;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle keypress events in the editor.
|
|
|
|
* @param aEvent
|
|
|
|
* The keyboard event.
|
|
|
|
*/
|
|
|
|
handleEditorKeypress: function TP_handleEditorKeypress(aEvent)
|
|
|
|
{
|
|
|
|
if (aEvent.which == this.window.KeyEvent.DOM_VK_RETURN) {
|
|
|
|
this.saveEditor();
|
2011-11-05 12:38:00 -07:00
|
|
|
aEvent.preventDefault();
|
|
|
|
aEvent.stopPropagation();
|
2011-09-26 09:59:23 -07:00
|
|
|
} else if (aEvent.keyCode == this.window.KeyEvent.DOM_VK_ESCAPE) {
|
|
|
|
this.closeEditor();
|
2011-11-05 12:38:00 -07:00
|
|
|
aEvent.preventDefault();
|
|
|
|
aEvent.stopPropagation();
|
2011-09-26 09:59:23 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close the editor and cleanup.
|
|
|
|
*/
|
|
|
|
closeEditor: function TP_closeEditor()
|
|
|
|
{
|
2012-08-02 03:42:00 -07:00
|
|
|
if (!this.treeBrowserDocument) // already closed, bug 706092
|
|
|
|
return;
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
let editor = this.treeBrowserDocument.getElementById("attribute-editor");
|
2012-08-02 03:42:00 -07:00
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
let editorInput =
|
|
|
|
this.treeBrowserDocument.getElementById("attribute-editor-input");
|
|
|
|
|
|
|
|
// remove highlight from attribute-value node in tree
|
|
|
|
this.removeClass(this.editingContext.attrObj, "editingAttributeValue");
|
|
|
|
|
|
|
|
// hide editor
|
|
|
|
this.removeClass(editor, "editing");
|
|
|
|
|
|
|
|
// stop listening for editor specific events
|
|
|
|
this.unbindEditorEvent(editor, "click");
|
|
|
|
this.unbindEditorEvent(editor, "dblclick");
|
|
|
|
this.unbindEditorEvent(editor, "keypress");
|
|
|
|
|
|
|
|
// clean up after the editor
|
|
|
|
editorInput.value = "";
|
|
|
|
editorInput.blur();
|
|
|
|
this.editingContext = null;
|
|
|
|
this.editingEvents = {};
|
|
|
|
|
|
|
|
// event notification
|
2011-09-26 10:11:22 -07:00
|
|
|
Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED,
|
2011-09-26 09:59:23 -07:00
|
|
|
null);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Commit the edits made in the editor, then close it.
|
|
|
|
*/
|
|
|
|
saveEditor: function TP_saveEditor()
|
|
|
|
{
|
|
|
|
let editorInput =
|
|
|
|
this.treeBrowserDocument.getElementById("attribute-editor-input");
|
2012-03-09 02:49:00 -08:00
|
|
|
let dirty = false;
|
2011-09-26 09:59:23 -07:00
|
|
|
|
2012-03-09 02:49:00 -08:00
|
|
|
if (this.hasClass(this.editingContext.attrObj, "nodeValue")) {
|
|
|
|
// set the new attribute value on the original target DOM element
|
|
|
|
this.editingContext.repObj.setAttribute(this.editingContext.attrName,
|
|
|
|
editorInput.value);
|
|
|
|
|
|
|
|
// update the HTML tree attribute value
|
|
|
|
this.editingContext.attrObj.innerHTML = editorInput.value;
|
|
|
|
dirty = true;
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
|
2012-03-09 02:49:00 -08:00
|
|
|
if (this.hasClass(this.editingContext.attrObj, "nodeName")) {
|
|
|
|
// remove the original attribute from the original target DOM element
|
|
|
|
this.editingContext.repObj.removeAttribute(this.editingContext.attrName);
|
|
|
|
|
|
|
|
// set the new attribute value on the original target DOM element
|
|
|
|
this.editingContext.repObj.setAttribute(editorInput.value,
|
|
|
|
this.editingContext.attrValue);
|
|
|
|
|
|
|
|
// update the HTML tree attribute value
|
|
|
|
this.editingContext.attrObj.innerHTML = editorInput.value;
|
|
|
|
dirty = true;
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
|
2012-03-09 02:49:00 -08:00
|
|
|
this.IUI.isDirty = dirty;
|
2012-05-30 19:49:10 -07:00
|
|
|
this.IUI.nodeChanged("treepanel");
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
// event notification
|
2011-09-26 10:11:22 -07:00
|
|
|
Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED,
|
2011-09-26 09:59:23 -07:00
|
|
|
null);
|
|
|
|
|
|
|
|
this.closeEditor();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple tree select method.
|
|
|
|
* @param aNode the DOM node in the content document to select.
|
|
|
|
* @param aScroll boolean scroll to the visible node?
|
|
|
|
*/
|
2012-06-01 12:39:00 -07:00
|
|
|
select: function TP_select(aNode, aScroll, aFrom)
|
2011-09-26 09:59:23 -07:00
|
|
|
{
|
2012-04-19 11:04:46 -07:00
|
|
|
if (this.ioBox) {
|
2012-06-01 12:39:00 -07:00
|
|
|
this.ioBox.select(aNode, true, aFrom != "treepanel", aScroll);
|
2012-04-19 11:04:46 -07:00
|
|
|
} else {
|
|
|
|
this.pendingSelection = { node: aNode, scroll: aScroll };
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
//// Utility functions
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the given object have a class attribute?
|
|
|
|
* @param aNode
|
|
|
|
* the DOM node.
|
|
|
|
* @param aClass
|
|
|
|
* The class string.
|
|
|
|
* @returns boolean
|
|
|
|
*/
|
|
|
|
hasClass: function TP_hasClass(aNode, aClass)
|
|
|
|
{
|
|
|
|
if (!(aNode instanceof this.window.Element))
|
|
|
|
return false;
|
|
|
|
return aNode.classList.contains(aClass);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add the class name to the given object.
|
|
|
|
* @param aNode
|
|
|
|
* the DOM node.
|
|
|
|
* @param aClass
|
|
|
|
* The class string.
|
|
|
|
*/
|
|
|
|
addClass: function TP_addClass(aNode, aClass)
|
|
|
|
{
|
|
|
|
if (aNode instanceof this.window.Element)
|
|
|
|
aNode.classList.add(aClass);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove the class name from the given object
|
|
|
|
* @param aNode
|
|
|
|
* the DOM node.
|
|
|
|
* @param aClass
|
|
|
|
* The class string.
|
|
|
|
*/
|
|
|
|
removeClass: function TP_removeClass(aNode, aClass)
|
|
|
|
{
|
|
|
|
if (aNode instanceof this.window.Element)
|
|
|
|
aNode.classList.remove(aClass);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the "repObject" from the HTML panel's domplate-constructed DOM node.
|
|
|
|
* In this system, a "repObject" is the Object being Represented by the box
|
|
|
|
* object. It is the "real" object that we're building our facade around.
|
|
|
|
*
|
|
|
|
* @param element
|
|
|
|
* The element in the HTML panel the user clicked.
|
|
|
|
* @returns either a real node or null
|
|
|
|
*/
|
|
|
|
getRepObject: function TP_getRepObject(element)
|
|
|
|
{
|
|
|
|
let target = null;
|
|
|
|
for (let child = element; child; child = child.parentNode) {
|
|
|
|
if (this.hasClass(child, "repTarget"))
|
|
|
|
target = child;
|
|
|
|
|
|
|
|
if (child.repObject) {
|
|
|
|
if (!target && this.hasClass(child.repObject, "repIgnore"))
|
|
|
|
break;
|
|
|
|
else
|
|
|
|
return child.repObject;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2012-02-20 18:02:00 -08:00
|
|
|
/**
|
|
|
|
* Remove a node box from the tree view.
|
|
|
|
* @param aElement
|
|
|
|
* The DOM node to remove from the HTML IOBox.
|
|
|
|
*/
|
|
|
|
deleteChildBox: function TP_deleteChildBox(aElement)
|
|
|
|
{
|
|
|
|
let childBox = this.ioBox.findObjectBox(aElement);
|
|
|
|
if (!childBox) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
childBox.parentNode.removeChild(childBox);
|
|
|
|
},
|
|
|
|
|
2011-09-26 09:59:23 -07:00
|
|
|
/**
|
|
|
|
* Destructor function. Cleanup.
|
|
|
|
*/
|
|
|
|
destroy: function TP_destroy()
|
|
|
|
{
|
|
|
|
if (this.isOpen()) {
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
domplateUtils.setDOM(null);
|
|
|
|
|
2011-10-11 06:11:10 -07:00
|
|
|
if (this.DOMHelpers) {
|
|
|
|
this.DOMHelpers.destroy();
|
|
|
|
delete this.DOMHelpers;
|
|
|
|
}
|
2011-09-26 09:59:23 -07:00
|
|
|
|
|
|
|
if (this.treePanelDiv) {
|
|
|
|
this.treePanelDiv.ownerPanel = null;
|
|
|
|
let parent = this.treePanelDiv.parentNode;
|
|
|
|
parent.removeChild(this.treePanelDiv);
|
|
|
|
delete this.treePanelDiv;
|
|
|
|
delete this.treeBrowserDocument;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.treeIFrame) {
|
|
|
|
this.treeIFrame.removeEventListener("dblclick", this.onTreeDblClick, false);
|
|
|
|
this.treeIFrame.removeEventListener("click", this.onTreeClick, false);
|
|
|
|
let parent = this.treeIFrame.parentNode;
|
|
|
|
parent.removeChild(this.treeIFrame);
|
|
|
|
delete this.treeIFrame;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-10-11 06:11:10 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* DOMHelpers
|
|
|
|
* Makes DOM traversal easier. Goes through iframes.
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param nsIDOMWindow aWindow
|
|
|
|
* The content window, owning the document to traverse.
|
|
|
|
*/
|
|
|
|
function DOMHelpers(aWindow) {
|
|
|
|
this.window = aWindow;
|
|
|
|
};
|
|
|
|
|
|
|
|
DOMHelpers.prototype = {
|
|
|
|
getParentObject: function Helpers_getParentObject(node)
|
|
|
|
{
|
|
|
|
let parentNode = node ? node.parentNode : null;
|
|
|
|
|
|
|
|
if (!parentNode) {
|
|
|
|
// Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
|
|
|
|
// and Notation. top level windows have no parentNode
|
|
|
|
if (node && node == this.window.Node.DOCUMENT_NODE) {
|
|
|
|
// document type
|
|
|
|
if (node.defaultView) {
|
|
|
|
let embeddingFrame = node.defaultView.frameElement;
|
|
|
|
if (embeddingFrame)
|
|
|
|
return embeddingFrame.parentNode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// a Document object without a parentNode or window
|
|
|
|
return null; // top level has no parent
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) {
|
|
|
|
if (parentNode.defaultView) {
|
|
|
|
return parentNode.defaultView.frameElement;
|
|
|
|
}
|
|
|
|
// parent is document element, but no window at defaultView.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!parentNode.localName)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
return parentNode;
|
|
|
|
},
|
|
|
|
|
|
|
|
getChildObject: function Helpers_getChildObject(node, index, previousSibling,
|
|
|
|
showTextNodesWithWhitespace)
|
|
|
|
{
|
|
|
|
if (!node)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if (node.contentDocument) {
|
|
|
|
// then the node is a frame
|
|
|
|
if (index == 0) {
|
|
|
|
return node.contentDocument.documentElement; // the node's HTMLElement
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node instanceof this.window.GetSVGDocument) {
|
|
|
|
let svgDocument = node.getSVGDocument();
|
|
|
|
if (svgDocument) {
|
|
|
|
// then the node is a frame
|
|
|
|
if (index == 0) {
|
|
|
|
return svgDocument.documentElement; // the node's SVGElement
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let child = null;
|
|
|
|
if (previousSibling) // then we are walking
|
|
|
|
child = this.getNextSibling(previousSibling);
|
|
|
|
else
|
|
|
|
child = this.getFirstChild(node);
|
|
|
|
|
|
|
|
if (showTextNodesWithWhitespace)
|
|
|
|
return child;
|
|
|
|
|
|
|
|
for (; child; child = this.getNextSibling(child)) {
|
|
|
|
if (!this.isWhitespaceText(child))
|
|
|
|
return child;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null; // we have no children worth showing.
|
|
|
|
},
|
|
|
|
|
|
|
|
getFirstChild: function Helpers_getFirstChild(node)
|
|
|
|
{
|
|
|
|
let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL;
|
|
|
|
this.treeWalker = node.ownerDocument.createTreeWalker(node,
|
|
|
|
SHOW_ALL, null, false);
|
|
|
|
return this.treeWalker.firstChild();
|
|
|
|
},
|
|
|
|
|
|
|
|
getNextSibling: function Helpers_getNextSibling(node)
|
|
|
|
{
|
|
|
|
let next = this.treeWalker.nextSibling();
|
|
|
|
|
|
|
|
if (!next)
|
|
|
|
delete this.treeWalker;
|
|
|
|
|
|
|
|
return next;
|
|
|
|
},
|
|
|
|
|
|
|
|
isWhitespaceText: function Helpers_isWhitespaceText(node)
|
|
|
|
{
|
|
|
|
return node.nodeType == this.window.Node.TEXT_NODE &&
|
|
|
|
!/[^\s]/.exec(node.nodeValue);
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function Helpers_destroy()
|
|
|
|
{
|
|
|
|
delete this.window;
|
|
|
|
delete this.treeWalker;
|
|
|
|
}
|
|
|
|
};
|