mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1533 lines
42 KiB
JavaScript
1533 lines
42 KiB
JavaScript
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
|
|
const Cc = Components.classes;
|
|
const Cu = Components.utils;
|
|
const Ci = Components.interfaces;
|
|
|
|
// Page size for pageup/pagedown
|
|
const PAGE_SIZE = 10;
|
|
|
|
const PREVIEW_AREA = 700;
|
|
const DEFAULT_MAX_CHILDREN = 100;
|
|
|
|
this.EXPORTED_SYMBOLS = ["MarkupView"];
|
|
|
|
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm");
|
|
Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
|
|
Cu.import("resource:///modules/devtools/Templater.jsm");
|
|
Cu.import("resource:///modules/devtools/Undo.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
/**
|
|
* Vocabulary for the purposes of this file:
|
|
*
|
|
* MarkupContainer - the structure that holds an editor and its
|
|
* immediate children in the markup panel.
|
|
* Node - A content node.
|
|
* object.elt - A UI element in the markup panel.
|
|
*/
|
|
|
|
/**
|
|
* The markup tree. Manages the mapping of nodes to MarkupContainers,
|
|
* updating based on mutations, and the undo/redo bindings.
|
|
*
|
|
* @param Inspector aInspector
|
|
* The inspector we're watching.
|
|
* @param iframe aFrame
|
|
* An iframe in which the caller has kindly loaded markup-view.xhtml.
|
|
*/
|
|
this.MarkupView = function MarkupView(aInspector, aFrame, aControllerWindow)
|
|
{
|
|
this._inspector = aInspector;
|
|
this._frame = aFrame;
|
|
this.doc = this._frame.contentDocument;
|
|
this._elt = this.doc.querySelector("#root");
|
|
|
|
try {
|
|
this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
|
|
} catch(ex) {
|
|
this.maxChildren = DEFAULT_MAX_CHILDREN;
|
|
}
|
|
|
|
this.undo = new UndoStack();
|
|
this.undo.installController(aControllerWindow);
|
|
|
|
this._containers = new WeakMap();
|
|
|
|
this._observer = new this.doc.defaultView.MutationObserver(this._mutationObserver.bind(this));
|
|
|
|
this._boundOnNewSelection = this._onNewSelection.bind(this);
|
|
this._inspector.selection.on("new-node", this._boundOnNewSelection);
|
|
this._onNewSelection();
|
|
|
|
this._boundKeyDown = this._onKeyDown.bind(this);
|
|
this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
|
|
|
|
this._boundFocus = this._onFocus.bind(this);
|
|
this._frame.addEventListener("focus", this._boundFocus, false);
|
|
|
|
this._initPreview();
|
|
}
|
|
|
|
MarkupView.prototype = {
|
|
_selectedContainer: null,
|
|
|
|
template: function MT_template(aName, aDest, aOptions={stack: "markup-view.xhtml"})
|
|
{
|
|
let node = this.doc.getElementById("template-" + aName).cloneNode(true);
|
|
node.removeAttribute("id");
|
|
template(node, aDest, aOptions);
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Get the MarkupContainer object for a given node, or undefined if
|
|
* none exists.
|
|
*/
|
|
getContainer: function MT_getContainer(aNode)
|
|
{
|
|
return this._containers.get(aNode);
|
|
},
|
|
|
|
/**
|
|
* Highlight the inspector selected node.
|
|
*/
|
|
_onNewSelection: function MT__onNewSelection()
|
|
{
|
|
if (this._inspector.selection.isNode()) {
|
|
this.showNode(this._inspector.selection.node, true);
|
|
this.markNodeAsSelected(this._inspector.selection.node);
|
|
} else {
|
|
this.unmarkSelectedNode();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a TreeWalker to find the next/previous
|
|
* node for selection.
|
|
*/
|
|
_selectionWalker: function MT__seletionWalker(aStart)
|
|
{
|
|
let walker = this.doc.createTreeWalker(
|
|
aStart || this._elt,
|
|
Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
|
|
function(aElement) {
|
|
if (aElement.container && aElement.container.visible) {
|
|
return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
|
|
}
|
|
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
|
|
}
|
|
);
|
|
walker.currentNode = this._selectedContainer.elt;
|
|
return walker;
|
|
},
|
|
|
|
/**
|
|
* Key handling.
|
|
*/
|
|
_onKeyDown: function MT__KeyDown(aEvent)
|
|
{
|
|
let handled = true;
|
|
|
|
// Ignore keystrokes that originated in editors.
|
|
if (aEvent.target.tagName.toLowerCase() === "input" ||
|
|
aEvent.target.tagName.toLowerCase() === "textarea") {
|
|
return;
|
|
}
|
|
|
|
switch(aEvent.keyCode) {
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
|
|
this.deleteNode(this._selectedContainer.node);
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
|
|
this.navigate(this._containers.get(this._rootNode.firstChild));
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
|
|
if (this._selectedContainer.expanded) {
|
|
this.collapseNode(this._selectedContainer.node);
|
|
} else {
|
|
let parent = this._selectionWalker().parentNode();
|
|
if (parent) {
|
|
this.navigate(parent.container);
|
|
}
|
|
}
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
|
|
if (!this._selectedContainer.expanded) {
|
|
this.expandNode(this._selectedContainer.node);
|
|
} else {
|
|
let next = this._selectionWalker().nextNode();
|
|
if (next) {
|
|
this.navigate(next.container);
|
|
}
|
|
}
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_UP:
|
|
let prev = this._selectionWalker().previousNode();
|
|
if (prev) {
|
|
this.navigate(prev.container);
|
|
}
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
|
|
let next = this._selectionWalker().nextNode();
|
|
if (next) {
|
|
this.navigate(next.container);
|
|
}
|
|
break;
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
|
|
let walker = this._selectionWalker();
|
|
let selection = this._selectedContainer;
|
|
for (let i = 0; i < PAGE_SIZE; i++) {
|
|
let prev = walker.previousNode();
|
|
if (!prev) {
|
|
break;
|
|
}
|
|
selection = prev.container;
|
|
}
|
|
this.navigate(selection);
|
|
break;
|
|
}
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: {
|
|
let walker = this._selectionWalker();
|
|
let selection = this._selectedContainer;
|
|
for (let i = 0; i < PAGE_SIZE; i++) {
|
|
let next = walker.nextNode();
|
|
if (!next) {
|
|
break;
|
|
}
|
|
selection = next.container;
|
|
}
|
|
this.navigate(selection);
|
|
break;
|
|
}
|
|
default:
|
|
handled = false;
|
|
}
|
|
if (handled) {
|
|
aEvent.stopPropagation();
|
|
aEvent.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete a node from the DOM.
|
|
* This is an undoable action.
|
|
*/
|
|
deleteNode: function MC__deleteNode(aNode)
|
|
{
|
|
let doc = nodeDocument(aNode);
|
|
if (aNode === doc ||
|
|
aNode === doc.documentElement ||
|
|
aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
|
|
return;
|
|
}
|
|
|
|
let parentNode = aNode.parentNode;
|
|
let sibling = aNode.nextSibling;
|
|
|
|
this.undo.do(function() {
|
|
if (aNode.selected) {
|
|
this.navigate(this._containers.get(parentNode));
|
|
}
|
|
parentNode.removeChild(aNode);
|
|
}.bind(this), function() {
|
|
parentNode.insertBefore(aNode, sibling);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* If an editable item is focused, select its container.
|
|
*/
|
|
_onFocus: function MC__onFocus(aEvent) {
|
|
let parent = aEvent.target;
|
|
while (!parent.container) {
|
|
parent = parent.parentNode;
|
|
}
|
|
if (parent) {
|
|
this.navigate(parent.container, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle a user-requested navigation to a given MarkupContainer,
|
|
* updating the inspector's currently-selected node.
|
|
*
|
|
* @param MarkupContainer aContainer
|
|
* The container we're navigating to.
|
|
* @param aIgnoreFocus aIgnoreFocus
|
|
* If falsy, keyboard focus will be moved to the container too.
|
|
*/
|
|
navigate: function MT__navigate(aContainer, aIgnoreFocus)
|
|
{
|
|
if (!aContainer) {
|
|
return;
|
|
}
|
|
|
|
let node = aContainer.node;
|
|
this.showNode(node, false);
|
|
|
|
this._inspector.selection.setNode(node, "treepanel");
|
|
// This event won't be fired if the node is the same. But the highlighter
|
|
// need to lock the node if it wasn't.
|
|
this._inspector.selection.emit("new-node");
|
|
|
|
if (!aIgnoreFocus) {
|
|
aContainer.focus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Make sure a node is included in the markup tool.
|
|
*
|
|
* @param DOMNode aNode
|
|
* The node in the content document.
|
|
*
|
|
* @returns MarkupContainer The MarkupContainer object for this element.
|
|
*/
|
|
importNode: function MT_importNode(aNode, aExpand)
|
|
{
|
|
if (!aNode) {
|
|
return null;
|
|
}
|
|
|
|
if (this._containers.has(aNode)) {
|
|
return this._containers.get(aNode);
|
|
}
|
|
|
|
this._observer.observe(aNode, {
|
|
attributes: true,
|
|
childList: true,
|
|
characterData: true,
|
|
});
|
|
|
|
let walker = documentWalker(aNode);
|
|
let parent = walker.parentNode();
|
|
if (parent) {
|
|
var container = new MarkupContainer(this, aNode);
|
|
} else {
|
|
var container = new RootContainer(this, aNode);
|
|
this._elt.appendChild(container.elt);
|
|
this._rootNode = aNode;
|
|
aNode.addEventListener("load", function MP_watch_contentLoaded(aEvent) {
|
|
// Fake a childList mutation here.
|
|
this._mutationObserver([{target: aEvent.target, type: "childList"}]);
|
|
}.bind(this), true);
|
|
}
|
|
|
|
this._containers.set(aNode, container);
|
|
// FIXME: set an expando to prevent the the wrapper from disappearing
|
|
// See bug 819131 for details.
|
|
aNode.__preserveHack = true;
|
|
container.expanded = aExpand;
|
|
|
|
container.childrenDirty = true;
|
|
this._updateChildren(container);
|
|
|
|
if (parent) {
|
|
this.importNode(parent, true);
|
|
}
|
|
return container;
|
|
},
|
|
|
|
/**
|
|
* Mutation observer used for included nodes.
|
|
*/
|
|
_mutationObserver: function MT__mutationObserver(aMutations)
|
|
{
|
|
for (let mutation of aMutations) {
|
|
let container = this._containers.get(mutation.target);
|
|
if (!container) {
|
|
// Container might not exist if this came from a load event for an iframe
|
|
// we're not viewing.
|
|
continue;
|
|
}
|
|
if (mutation.type === "attributes" || mutation.type === "characterData") {
|
|
container.update();
|
|
} else if (mutation.type === "childList") {
|
|
container.childrenDirty = true;
|
|
this._updateChildren(container);
|
|
}
|
|
}
|
|
this._inspector.emit("markupmutation");
|
|
},
|
|
|
|
/**
|
|
* Make sure the given node's parents are expanded and the
|
|
* node is scrolled on to screen.
|
|
*/
|
|
showNode: function MT_showNode(aNode, centered)
|
|
{
|
|
let container = this.importNode(aNode);
|
|
this._updateChildren(container);
|
|
let walker = documentWalker(aNode);
|
|
let parent;
|
|
while (parent = walker.parentNode()) {
|
|
this._updateChildren(this.getContainer(parent));
|
|
this.expandNode(parent);
|
|
}
|
|
LayoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
|
|
},
|
|
|
|
/**
|
|
* Expand the container's children.
|
|
*/
|
|
_expandContainer: function MT__expandContainer(aContainer)
|
|
{
|
|
if (aContainer.hasChildren && !aContainer.expanded) {
|
|
aContainer.expanded = true;
|
|
this._updateChildren(aContainer);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Expand the node's children.
|
|
*/
|
|
expandNode: function MT_expandNode(aNode)
|
|
{
|
|
let container = this._containers.get(aNode);
|
|
this._expandContainer(container);
|
|
},
|
|
|
|
/**
|
|
* Expand the entire tree beneath a container.
|
|
*
|
|
* @param aContainer The container to expand.
|
|
*/
|
|
_expandAll: function MT_expandAll(aContainer)
|
|
{
|
|
this._expandContainer(aContainer);
|
|
let child = aContainer.children.firstChild;
|
|
while (child) {
|
|
this._expandAll(child.container);
|
|
child = child.nextSibling;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Expand the entire tree beneath a node.
|
|
*
|
|
* @param aContainer The node to expand, or null
|
|
* to start from the top.
|
|
*/
|
|
expandAll: function MT_expandAll(aNode)
|
|
{
|
|
aNode = aNode || this._rootNode;
|
|
this._expandAll(this._containers.get(aNode));
|
|
},
|
|
|
|
/**
|
|
* Collapse the node's children.
|
|
*/
|
|
collapseNode: function MT_collapseNode(aNode)
|
|
{
|
|
let container = this._containers.get(aNode);
|
|
container.expanded = false;
|
|
},
|
|
|
|
/**
|
|
* Mark the given node selected.
|
|
*/
|
|
markNodeAsSelected: function MT_markNodeAsSelected(aNode)
|
|
{
|
|
let container = this._containers.get(aNode);
|
|
if (this._selectedContainer === container) {
|
|
return false;
|
|
}
|
|
if (this._selectedContainer) {
|
|
this._selectedContainer.selected = false;
|
|
}
|
|
this._selectedContainer = container;
|
|
if (aNode) {
|
|
this._selectedContainer.selected = true;
|
|
}
|
|
|
|
this._ensureSelectionVisible();
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Make sure that every ancestor of the selection are updated
|
|
* and included in the list of visible children.
|
|
*/
|
|
_ensureSelectionVisible: function MT_ensureSelectionVisible()
|
|
{
|
|
let node = this._selectedContainer.node;
|
|
let walker = documentWalker(node);
|
|
while (node) {
|
|
let container = this._containers.get(node);
|
|
let parent = walker.parentNode();
|
|
if (!container.elt.parentNode) {
|
|
let parentContainer = this._containers.get(parent);
|
|
parentContainer.childrenDirty = true;
|
|
this._updateChildren(parentContainer, node);
|
|
}
|
|
|
|
node = parent;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unmark selected node (no node selected).
|
|
*/
|
|
unmarkSelectedNode: function MT_unmarkSelectedNode()
|
|
{
|
|
if (this._selectedContainer) {
|
|
this._selectedContainer.selected = false;
|
|
this._selectedContainer = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when the markup panel initiates a change on a node.
|
|
*/
|
|
nodeChanged: function MT_nodeChanged(aNode)
|
|
{
|
|
if (aNode === this._inspector.selection) {
|
|
this._inspector.change("markupview");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Make sure all children of the given container's node are
|
|
* imported and attached to the container in the right order.
|
|
* @param aCentered If provided, this child will be included
|
|
* in the visible subset, and will be roughly centered
|
|
* in that list.
|
|
*/
|
|
_updateChildren: function MT__updateChildren(aContainer, aCentered)
|
|
{
|
|
if (!aContainer.childrenDirty) {
|
|
return false;
|
|
}
|
|
|
|
// Get a tree walker pointing at the first child of the node.
|
|
let treeWalker = documentWalker(aContainer.node);
|
|
let child = treeWalker.firstChild();
|
|
aContainer.hasChildren = !!child;
|
|
|
|
if (!aContainer.expanded) {
|
|
return;
|
|
}
|
|
|
|
aContainer.childrenDirty = false;
|
|
|
|
let children = this._getVisibleChildren(aContainer, aCentered);
|
|
let fragment = this.doc.createDocumentFragment();
|
|
|
|
for (child of children.children) {
|
|
let container = this.importNode(child, false);
|
|
fragment.appendChild(container.elt);
|
|
}
|
|
|
|
while (aContainer.children.firstChild) {
|
|
aContainer.children.removeChild(aContainer.children.firstChild);
|
|
}
|
|
|
|
if (!(children.hasFirst && children.hasLast)) {
|
|
let data = {
|
|
showing: this.strings.GetStringFromName("markupView.more.showing"),
|
|
showAll: this.strings.formatStringFromName(
|
|
"markupView.more.showAll",
|
|
[aContainer.node.children.length.toString()], 1),
|
|
allButtonClick: function() {
|
|
aContainer.maxChildren = -1;
|
|
aContainer.childrenDirty = true;
|
|
this._updateChildren(aContainer);
|
|
}.bind(this)
|
|
};
|
|
|
|
if (!children.hasFirst) {
|
|
let span = this.template("more-nodes", data);
|
|
fragment.insertBefore(span, fragment.firstChild);
|
|
}
|
|
if (!children.hasLast) {
|
|
let span = this.template("more-nodes", data);
|
|
fragment.appendChild(span);
|
|
}
|
|
}
|
|
|
|
aContainer.children.appendChild(fragment);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Return a list of the children to display for this container.
|
|
*/
|
|
_getVisibleChildren: function MV__getVisibleChildren(aContainer, aCentered)
|
|
{
|
|
let maxChildren = aContainer.maxChildren || this.maxChildren;
|
|
if (maxChildren == -1) {
|
|
maxChildren = Number.MAX_VALUE;
|
|
}
|
|
let firstChild = documentWalker(aContainer.node).firstChild();
|
|
let lastChild = documentWalker(aContainer.node).lastChild();
|
|
|
|
if (!firstChild) {
|
|
// No children, we're done.
|
|
return { hasFirst: true, hasLast: true, children: [] };
|
|
}
|
|
|
|
// By default try to put the selected child in the middle of the list.
|
|
let start = aCentered || firstChild;
|
|
|
|
// Start by reading backward from the starting point....
|
|
let nodes = [];
|
|
let backwardWalker = documentWalker(start);
|
|
if (backwardWalker.previousSibling()) {
|
|
let backwardCount = Math.floor(maxChildren / 2);
|
|
let backwardNodes = this._readBackward(backwardWalker, backwardCount);
|
|
nodes = backwardNodes;
|
|
}
|
|
|
|
// Then read forward by any slack left in the max children...
|
|
let forwardWalker = documentWalker(start);
|
|
let forwardCount = maxChildren - nodes.length;
|
|
nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
|
|
|
|
// If there's any room left, it means we've run all the way to the end.
|
|
// In that case, there might still be more items at the front.
|
|
let remaining = maxChildren - nodes.length;
|
|
if (remaining > 0 && nodes[0] != firstChild) {
|
|
let firstNodes = this._readBackward(backwardWalker, remaining);
|
|
|
|
// Then put it all back together.
|
|
nodes = firstNodes.concat(nodes);
|
|
}
|
|
|
|
return {
|
|
hasFirst: nodes[0] == firstChild,
|
|
hasLast: nodes[nodes.length - 1] == lastChild,
|
|
children: nodes
|
|
};
|
|
},
|
|
|
|
_readForward: function MV__readForward(aWalker, aCount)
|
|
{
|
|
let ret = [];
|
|
let node = aWalker.currentNode;
|
|
do {
|
|
ret.push(node);
|
|
node = aWalker.nextSibling();
|
|
} while (node && --aCount);
|
|
return ret;
|
|
},
|
|
|
|
_readBackward: function MV__readBackward(aWalker, aCount)
|
|
{
|
|
let ret = [];
|
|
let node = aWalker.currentNode;
|
|
do {
|
|
ret.push(node);
|
|
node = aWalker.previousSibling();
|
|
} while(node && --aCount);
|
|
ret.reverse();
|
|
return ret;
|
|
},
|
|
|
|
/**
|
|
* Tear down the markup panel.
|
|
*/
|
|
destroy: function MT_destroy()
|
|
{
|
|
this.undo.destroy();
|
|
delete this.undo;
|
|
|
|
this._frame.removeEventListener("focus", this._boundFocus, false);
|
|
delete this._boundFocus;
|
|
|
|
this._frame.contentWindow.removeEventListener("scroll", this._boundUpdatePreview, true);
|
|
this._frame.contentWindow.removeEventListener("resize", this._boundResizePreview, true);
|
|
this._frame.contentWindow.removeEventListener("overflow", this._boundResizePreview, true);
|
|
this._frame.contentWindow.removeEventListener("underflow", this._boundResizePreview, true);
|
|
delete this._boundUpdatePreview;
|
|
|
|
this._frame.contentWindow.removeEventListener("keydown", this._boundKeyDown, true);
|
|
delete this._boundKeyDown;
|
|
|
|
this._inspector.selection.off("new-node", this._boundOnNewSelection);
|
|
delete this._boundOnNewSelection;
|
|
|
|
delete this._elt;
|
|
|
|
delete this._containers;
|
|
this._observer.disconnect();
|
|
delete this._observer;
|
|
},
|
|
|
|
/**
|
|
* Initialize the preview panel.
|
|
*/
|
|
_initPreview: function MT_initPreview()
|
|
{
|
|
if (!Services.prefs.getBoolPref("devtools.inspector.markupPreview")) {
|
|
return;
|
|
}
|
|
|
|
this._previewBar = this.doc.querySelector("#previewbar");
|
|
this._preview = this.doc.querySelector("#preview");
|
|
this._viewbox = this.doc.querySelector("#viewbox");
|
|
|
|
this._previewBar.classList.remove("disabled");
|
|
|
|
this._previewWidth = this._preview.getBoundingClientRect().width;
|
|
|
|
this._boundResizePreview = this._resizePreview.bind(this);
|
|
this._frame.contentWindow.addEventListener("resize", this._boundResizePreview, true);
|
|
this._frame.contentWindow.addEventListener("overflow", this._boundResizePreview, true);
|
|
this._frame.contentWindow.addEventListener("underflow", this._boundResizePreview, true);
|
|
|
|
this._boundUpdatePreview = this._updatePreview.bind(this);
|
|
this._frame.contentWindow.addEventListener("scroll", this._boundUpdatePreview, true);
|
|
this._updatePreview();
|
|
},
|
|
|
|
|
|
/**
|
|
* Move the preview viewbox.
|
|
*/
|
|
_updatePreview: function MT_updatePreview()
|
|
{
|
|
let win = this._frame.contentWindow;
|
|
|
|
if (win.scrollMaxY == 0) {
|
|
this._previewBar.classList.add("disabled");
|
|
return;
|
|
}
|
|
|
|
this._previewBar.classList.remove("disabled");
|
|
|
|
let ratio = this._previewWidth / PREVIEW_AREA;
|
|
let width = ratio * win.innerWidth;
|
|
|
|
let height = ratio * (win.scrollMaxY + win.innerHeight);
|
|
let scrollTo
|
|
if (height >= win.innerHeight) {
|
|
scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY);
|
|
this._previewBar.setAttribute("style", "height:" + height + "px;transform:translateY(" + scrollTo + "px)");
|
|
} else {
|
|
this._previewBar.setAttribute("style", "height:100%");
|
|
}
|
|
|
|
let bgSize = ~~width + "px " + ~~height + "px";
|
|
this._preview.setAttribute("style", "background-size:" + bgSize);
|
|
|
|
let height = ~~(win.innerHeight * ratio) + "px";
|
|
let top = ~~(win.scrollY * ratio) + "px";
|
|
this._viewbox.setAttribute("style", "height:" + height + ";transform: translateY(" + top + ")");
|
|
},
|
|
|
|
/**
|
|
* Hide the preview while resizing, to avoid slowness.
|
|
*/
|
|
_resizePreview: function MT_resizePreview()
|
|
{
|
|
let win = this._frame.contentWindow;
|
|
this._previewBar.classList.add("hide");
|
|
win.clearTimeout(this._resizePreviewTimeout);
|
|
|
|
win.setTimeout(function() {
|
|
this._updatePreview();
|
|
this._previewBar.classList.remove("hide");
|
|
}.bind(this), 1000);
|
|
},
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
* The main structure for storing a document node in the markup
|
|
* tree. Manages creation of the editor for the node and
|
|
* a <ul> for placing child elements, and expansion/collapsing
|
|
* of the element.
|
|
*
|
|
* @param MarkupView aMarkupView
|
|
* The markup view that owns this container.
|
|
* @param DOMNode aNode
|
|
* The node to display.
|
|
*/
|
|
function MarkupContainer(aMarkupView, aNode)
|
|
{
|
|
this.markup = aMarkupView;
|
|
this.doc = this.markup.doc;
|
|
this.undo = this.markup.undo;
|
|
this.node = aNode;
|
|
|
|
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
|
|
this.editor = new TextEditor(this, aNode, "text");
|
|
} else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
|
|
this.editor = new TextEditor(this, aNode, "comment");
|
|
} else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
|
|
this.editor = new ElementEditor(this, aNode);
|
|
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
|
|
this.editor = new DoctypeEditor(this, aNode);
|
|
} else {
|
|
this.editor = new GenericEditor(this.markup, aNode);
|
|
}
|
|
|
|
// The template will fill the following properties
|
|
this.elt = null;
|
|
this.expander = null;
|
|
this.codeBox = null;
|
|
this.children = null;
|
|
this.markup.template("container", this);
|
|
this.elt.container = this;
|
|
|
|
this.expander.addEventListener("click", function() {
|
|
this.markup.navigate(this);
|
|
|
|
if (this.expanded) {
|
|
this.markup.collapseNode(this.node);
|
|
} else {
|
|
this.markup.expandNode(this.node);
|
|
}
|
|
}.bind(this));
|
|
|
|
this.codeBox.insertBefore(this.editor.elt, this.children);
|
|
|
|
this.editor.elt.addEventListener("mousedown", function(evt) {
|
|
this.markup.navigate(this);
|
|
}.bind(this), false);
|
|
|
|
if (this.editor.summaryElt) {
|
|
this.editor.summaryElt.addEventListener("click", function(evt) {
|
|
this.markup.navigate(this);
|
|
this.markup.expandNode(this.node);
|
|
}.bind(this), false);
|
|
this.codeBox.appendChild(this.editor.summaryElt);
|
|
}
|
|
|
|
if (this.editor.closeElt) {
|
|
this.editor.closeElt.addEventListener("mousedown", function(evt) {
|
|
this.markup.navigate(this);
|
|
}.bind(this), false);
|
|
this.codeBox.appendChild(this.editor.closeElt);
|
|
}
|
|
|
|
}
|
|
|
|
MarkupContainer.prototype = {
|
|
/**
|
|
* True if the current node has children. The MarkupView
|
|
* will set this attribute for the MarkupContainer.
|
|
*/
|
|
_hasChildren: false,
|
|
|
|
get hasChildren() {
|
|
return this._hasChildren;
|
|
},
|
|
|
|
set hasChildren(aValue) {
|
|
this._hasChildren = aValue;
|
|
if (aValue) {
|
|
this.expander.style.visibility = "visible";
|
|
} else {
|
|
this.expander.style.visibility = "hidden";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* True if the node has been visually expanded in the tree.
|
|
*/
|
|
get expanded() {
|
|
return this.children.hasAttribute("expanded");
|
|
},
|
|
|
|
set expanded(aValue) {
|
|
if (aValue) {
|
|
this.expander.setAttribute("open", "");
|
|
this.children.setAttribute("expanded", "");
|
|
if (this.editor.summaryElt) {
|
|
this.editor.summaryElt.setAttribute("expanded", "");
|
|
}
|
|
} else {
|
|
this.expander.removeAttribute("open");
|
|
this.children.removeAttribute("expanded");
|
|
if (this.editor.summaryElt) {
|
|
this.editor.summaryElt.removeAttribute("expanded");
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* True if the container is visible in the markup tree.
|
|
*/
|
|
get visible()
|
|
{
|
|
return this.elt.getBoundingClientRect().height > 0;
|
|
},
|
|
|
|
/**
|
|
* True if the container is currently selected.
|
|
*/
|
|
_selected: false,
|
|
|
|
get selected() {
|
|
return this._selected;
|
|
},
|
|
|
|
set selected(aValue) {
|
|
this._selected = aValue;
|
|
if (this._selected) {
|
|
this.editor.elt.classList.add("theme-selected");
|
|
if (this.editor.closeElt) {
|
|
this.editor.closeElt.classList.add("theme-selected");
|
|
}
|
|
} else {
|
|
this.editor.elt.classList.remove("theme-selected");
|
|
if (this.editor.closeElt) {
|
|
this.editor.closeElt.classList.remove("theme-selected");
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the container's editor to the current state of the
|
|
* viewed node.
|
|
*/
|
|
update: function MC_update()
|
|
{
|
|
if (this.editor.update) {
|
|
this.editor.update();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Try to put keyboard focus on the current editor.
|
|
*/
|
|
focus: function MC_focus()
|
|
{
|
|
let focusable = this.editor.elt.querySelector("[tabindex]");
|
|
if (focusable) {
|
|
focusable.focus();
|
|
}
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Dummy container node used for the root document element.
|
|
*/
|
|
function RootContainer(aMarkupView, aNode)
|
|
{
|
|
this.doc = aMarkupView.doc;
|
|
this.elt = this.doc.createElement("ul");
|
|
this.children = this.elt;
|
|
this.node = aNode;
|
|
}
|
|
|
|
/**
|
|
* Creates an editor for simple nodes.
|
|
*/
|
|
function GenericEditor(aContainer, aNode)
|
|
{
|
|
this.elt = aContainer.doc.createElement("span");
|
|
this.elt.className = "editor";
|
|
this.elt.textContent = aNode.nodeName;
|
|
}
|
|
|
|
/**
|
|
* Creates an editor for a DOCTYPE node.
|
|
*
|
|
* @param MarkupContainer aContainer The container owning this editor.
|
|
* @param DOMNode aNode The node being edited.
|
|
*/
|
|
function DoctypeEditor(aContainer, aNode)
|
|
{
|
|
this.elt = aContainer.doc.createElement("span");
|
|
this.elt.className = "editor comment";
|
|
this.elt.textContent = '<!DOCTYPE ' + aNode.name +
|
|
(aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
|
|
(aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
|
|
'>';
|
|
}
|
|
|
|
/**
|
|
* Creates a simple text editor node, used for TEXT and COMMENT
|
|
* nodes.
|
|
*
|
|
* @param MarkupContainer aContainer The container owning this editor.
|
|
* @param DOMNode aNode The node being edited.
|
|
* @param string aTemplate The template id to use to build the editor.
|
|
*/
|
|
function TextEditor(aContainer, aNode, aTemplate)
|
|
{
|
|
this.node = aNode;
|
|
|
|
aContainer.markup.template(aTemplate, this);
|
|
|
|
editableField({
|
|
element: this.value,
|
|
stopOnReturn: true,
|
|
trigger: "dblclick",
|
|
multiline: true,
|
|
done: function TE_done(aVal, aCommit) {
|
|
if (!aCommit) {
|
|
return;
|
|
}
|
|
let oldValue = this.node.nodeValue;
|
|
aContainer.undo.do(function() {
|
|
this.node.nodeValue = aVal;
|
|
aContainer.markup.nodeChanged(this.node);
|
|
}.bind(this), function() {
|
|
this.node.nodeValue = oldValue;
|
|
aContainer.markup.nodeChanged(this.node);
|
|
}.bind(this));
|
|
}.bind(this)
|
|
});
|
|
|
|
this.update();
|
|
}
|
|
|
|
TextEditor.prototype = {
|
|
update: function TE_update()
|
|
{
|
|
this.value.textContent = this.node.nodeValue;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates an editor for an Element node.
|
|
*
|
|
* @param MarkupContainer aContainer The container owning this editor.
|
|
* @param Element aNode The node being edited.
|
|
*/
|
|
function ElementEditor(aContainer, aNode)
|
|
{
|
|
this.doc = aContainer.doc;
|
|
this.undo = aContainer.undo;
|
|
this.template = aContainer.markup.template.bind(aContainer.markup);
|
|
this.container = aContainer;
|
|
this.markup = this.container.markup;
|
|
this.node = aNode;
|
|
|
|
this.attrs = [];
|
|
|
|
// The templates will fill the following properties
|
|
this.elt = null;
|
|
this.tag = null;
|
|
this.attrList = null;
|
|
this.newAttr = null;
|
|
this.summaryElt = null;
|
|
this.closeElt = null;
|
|
|
|
// Create the main editor
|
|
this.template("element", this);
|
|
|
|
if (this.node.firstChild || this.node.textContent.length > 0) {
|
|
// Create the summary placeholder
|
|
this.template("elementContentSummary", this);
|
|
}
|
|
|
|
// Create the closing tag
|
|
this.template("elementClose", this);
|
|
|
|
// Make the tag name editable (unless this is a document element)
|
|
if (aNode != aNode.ownerDocument.documentElement) {
|
|
this.tag.setAttribute("tabindex", "0");
|
|
editableField({
|
|
element: this.tag,
|
|
trigger: "dblclick",
|
|
stopOnReturn: true,
|
|
done: this.onTagEdit.bind(this),
|
|
});
|
|
}
|
|
|
|
// Make the new attribute space editable.
|
|
editableField({
|
|
element: this.newAttr,
|
|
trigger: "dblclick",
|
|
stopOnReturn: true,
|
|
done: function EE_onNew(aVal, aCommit) {
|
|
if (!aCommit) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this._applyAttributes(aVal);
|
|
} catch (x) {
|
|
return;
|
|
}
|
|
}.bind(this)
|
|
});
|
|
|
|
let tagName = this.node.nodeName.toLowerCase();
|
|
this.tag.textContent = tagName;
|
|
this.closeTag.textContent = tagName;
|
|
|
|
this.update();
|
|
}
|
|
|
|
ElementEditor.prototype = {
|
|
/**
|
|
* Update the state of the editor from the node.
|
|
*/
|
|
update: function EE_update()
|
|
{
|
|
let attrs = this.node.attributes;
|
|
if (!attrs) {
|
|
return;
|
|
}
|
|
|
|
// Hide all the attribute editors, they'll be re-shown if they're
|
|
// still applicable. Don't update attributes that are being
|
|
// actively edited.
|
|
let attrEditors = this.attrList.querySelectorAll(".attreditor");
|
|
for (let i = 0; i < attrEditors.length; i++) {
|
|
if (!attrEditors[i].inplaceEditor) {
|
|
attrEditors[i].style.display = "none";
|
|
}
|
|
}
|
|
|
|
// Get the attribute editor for each attribute that exists on
|
|
// the node and show it.
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
let attr = this._createAttribute(attrs[i]);
|
|
if (!attr.inplaceEditor) {
|
|
attr.style.removeProperty("display");
|
|
}
|
|
}
|
|
},
|
|
|
|
_createAttribute: function EE_createAttribute(aAttr, aBefore)
|
|
{
|
|
if (this.attrs.indexOf(aAttr.name) !== -1) {
|
|
var attr = this.attrs[aAttr.name];
|
|
var name = attr.querySelector(".attrname");
|
|
var val = attr.querySelector(".attrvalue");
|
|
} else {
|
|
// Create the template editor, which will save some variables here.
|
|
let data = {
|
|
attrName: aAttr.name,
|
|
};
|
|
this.template("attribute", data);
|
|
var {attr, inner, name, val} = data;
|
|
|
|
// Figure out where we should place the attribute.
|
|
let before = aBefore || null;
|
|
if (aAttr.name == "id") {
|
|
before = this.attrList.firstChild;
|
|
} else if (aAttr.name == "class") {
|
|
let idNode = this.attrs["id"];
|
|
before = idNode ? idNode.nextSibling : this.attrList.firstChild;
|
|
}
|
|
this.attrList.insertBefore(attr, before);
|
|
|
|
// Make the attribute editable.
|
|
editableField({
|
|
element: inner,
|
|
trigger: "dblclick",
|
|
stopOnReturn: true,
|
|
selectAll: false,
|
|
start: function EE_editAttribute_start(aEditor, aEvent) {
|
|
// If the editing was started inside the name or value areas,
|
|
// select accordingly.
|
|
if (aEvent && aEvent.target === name) {
|
|
aEditor.input.setSelectionRange(0, name.textContent.length);
|
|
} else if (aEvent && aEvent.target === val) {
|
|
let length = val.textContent.length;
|
|
let editorLength = aEditor.input.value.length;
|
|
let start = editorLength - (length + 1);
|
|
aEditor.input.setSelectionRange(start, start + length);
|
|
} else {
|
|
aEditor.input.select();
|
|
}
|
|
},
|
|
done: function EE_editAttribute_done(aVal, aCommit) {
|
|
if (!aCommit) {
|
|
return;
|
|
}
|
|
|
|
this.undo.startBatch();
|
|
|
|
// Remove the attribute stored in this editor and re-add any attributes
|
|
// parsed out of the input element. Restore original attribute if
|
|
// parsing fails.
|
|
this._removeAttribute(this.node, aAttr.name);
|
|
try {
|
|
this._applyAttributes(aVal, attr);
|
|
this.undo.endBatch();
|
|
} catch (e) {
|
|
this.undo.endBatch();
|
|
this.undo.undo();
|
|
}
|
|
}.bind(this)
|
|
});
|
|
|
|
this.attrs[aAttr.name] = attr;
|
|
}
|
|
|
|
name.textContent = aAttr.name;
|
|
val.textContent = aAttr.value;
|
|
|
|
return attr;
|
|
},
|
|
|
|
/**
|
|
* Parse a user-entered attribute string and apply the resulting
|
|
* attributes to the node. This operation is undoable.
|
|
*
|
|
* @param string aValue the user-entered value.
|
|
* @param Element aAttrNode the attribute editor that created this
|
|
* set of attributes, used to place new attributes where the
|
|
* user put them.
|
|
*/
|
|
_applyAttributes: function EE__applyAttributes(aValue, aAttrNode)
|
|
{
|
|
let attrs = escapeAttributeValues(aValue);
|
|
|
|
this.undo.startBatch();
|
|
|
|
for (let attr of attrs) {
|
|
let attribute = {
|
|
name: attr.name,
|
|
value: attr.value
|
|
};
|
|
// Create an attribute editor next to the current attribute if needed.
|
|
this._createAttribute(attribute, aAttrNode ? aAttrNode.nextSibling : null);
|
|
this._setAttribute(this.node, attr.name, attr.value);
|
|
}
|
|
|
|
this.undo.endBatch();
|
|
},
|
|
|
|
/**
|
|
* Helper function for _setAttribute and _removeAttribute,
|
|
* returns a function that puts an attribute back the way it was.
|
|
*/
|
|
_restoreAttribute: function EE_restoreAttribute(aNode, aName)
|
|
{
|
|
if (aNode.hasAttribute(aName)) {
|
|
let oldValue = aNode.getAttribute(aName);
|
|
return function() {
|
|
aNode.setAttribute(aName, oldValue);
|
|
this.markup.nodeChanged(aNode);
|
|
}.bind(this);
|
|
} else {
|
|
return function() {
|
|
aNode.removeAttribute(aName);
|
|
this.markup.nodeChanged(aNode);
|
|
}.bind(this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets an attribute. This operation is undoable.
|
|
*/
|
|
_setAttribute: function EE_setAttribute(aNode, aName, aValue)
|
|
{
|
|
this.undo.do(function() {
|
|
aNode.setAttribute(aName, aValue);
|
|
this.markup.nodeChanged(aNode);
|
|
}.bind(this), this._restoreAttribute(aNode, aName));
|
|
},
|
|
|
|
/**
|
|
* Removes an attribute. This operation is undoable.
|
|
*/
|
|
_removeAttribute: function EE_removeAttribute(aNode, aName)
|
|
{
|
|
this.undo.do(function() {
|
|
aNode.removeAttribute(aName);
|
|
this.markup.nodeChanged(aNode);
|
|
}.bind(this), this._restoreAttribute(aNode, aName));
|
|
},
|
|
|
|
/**
|
|
* Handler for the new attribute editor.
|
|
*/
|
|
_onNewAttribute: function EE_onNewAttribute(aValue, aCommit)
|
|
{
|
|
if (!aValue || !aCommit) {
|
|
return;
|
|
}
|
|
|
|
this._setAttribute(this.node, aValue, "");
|
|
let attr = this._createAttribute({ name: aValue, value: ""});
|
|
attr.style.removeAttribute("display");
|
|
attr.querySelector("attrvalue").click();
|
|
},
|
|
|
|
|
|
/**
|
|
* Called when the tag name editor has is done editing.
|
|
*/
|
|
onTagEdit: function EE_onTagEdit(aVal, aCommit) {
|
|
if (!aCommit || aVal == this.node.tagName) {
|
|
return;
|
|
}
|
|
|
|
// Create a new element with the same attributes as the
|
|
// current element and prepare to replace the current node
|
|
// with it.
|
|
try {
|
|
var newElt = nodeDocument(this.node).createElement(aVal);
|
|
} catch(x) {
|
|
// Failed to create a new element with that tag name, ignore
|
|
// the change.
|
|
return;
|
|
}
|
|
|
|
let attrs = this.node.attributes;
|
|
|
|
for (let i = 0 ; i < attrs.length; i++) {
|
|
newElt.setAttribute(attrs[i].name, attrs[i].value);
|
|
}
|
|
|
|
function swapNodes(aOld, aNew) {
|
|
while (aOld.firstChild) {
|
|
aNew.appendChild(aOld.firstChild);
|
|
}
|
|
aOld.parentNode.insertBefore(aNew, aOld);
|
|
aOld.parentNode.removeChild(aOld);
|
|
}
|
|
|
|
let markup = this.container.markup;
|
|
|
|
// Queue an action to swap out the element.
|
|
this.undo.do(function() {
|
|
swapNodes(this.node, newElt);
|
|
|
|
// Make sure the new node is imported and is expanded/selected
|
|
// the same as the current node.
|
|
let newContainer = markup.importNode(newElt, this.container.expanded);
|
|
newContainer.expanded = this.container.expanded;
|
|
if (this.container.selected) {
|
|
markup.navigate(newContainer);
|
|
}
|
|
}.bind(this), function() {
|
|
swapNodes(newElt, this.node);
|
|
|
|
let newContainer = markup._containers.get(newElt);
|
|
this.container.expanded = newContainer.expanded;
|
|
if (newContainer.selected) {
|
|
markup.navigate(this.container);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
}
|
|
|
|
|
|
|
|
RootContainer.prototype = {
|
|
hasChildren: true,
|
|
expanded: true,
|
|
update: function RC_update() {}
|
|
};
|
|
|
|
function documentWalker(node) {
|
|
return new DocumentWalker(node, Ci.nsIDOMNodeFilter.SHOW_ALL, whitespaceTextFilter);
|
|
}
|
|
|
|
function nodeDocument(node) {
|
|
return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
|
|
}
|
|
|
|
/**
|
|
* Similar to a TreeWalker, except will dig in to iframes and it doesn't
|
|
* implement the good methods like previousNode and nextNode.
|
|
*
|
|
* See TreeWalker documentation for explanations of the methods.
|
|
*/
|
|
function DocumentWalker(aNode, aShow, aFilter)
|
|
{
|
|
let doc = nodeDocument(aNode);
|
|
this.walker = doc.createTreeWalker(nodeDocument(aNode), aShow, aFilter);
|
|
this.walker.currentNode = aNode;
|
|
this.filter = aFilter;
|
|
}
|
|
|
|
DocumentWalker.prototype = {
|
|
get node() this.walker.node,
|
|
get whatToShow() this.walker.whatToShow,
|
|
get expandEntityReferences() this.walker.expandEntityReferences,
|
|
get currentNode() this.walker.currentNode,
|
|
set currentNode(aVal) this.walker.currentNode = aVal,
|
|
|
|
/**
|
|
* Called when the new node is in a different document than
|
|
* the current node, creates a new treewalker for the document we've
|
|
* run in to.
|
|
*/
|
|
_reparentWalker: function DW_reparentWalker(aNewNode) {
|
|
if (!aNewNode) {
|
|
return null;
|
|
}
|
|
let doc = nodeDocument(aNewNode);
|
|
let walker = doc.createTreeWalker(doc,
|
|
this.whatToShow, this.filter, this.expandEntityReferences);
|
|
walker.currentNode = aNewNode;
|
|
this.walker = walker;
|
|
return aNewNode;
|
|
},
|
|
|
|
parentNode: function DW_parentNode()
|
|
{
|
|
let currentNode = this.walker.currentNode;
|
|
let parentNode = this.walker.parentNode();
|
|
|
|
if (!parentNode) {
|
|
if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
|
|
&& currentNode.defaultView) {
|
|
let embeddingFrame = currentNode.defaultView.frameElement;
|
|
if (embeddingFrame) {
|
|
return this._reparentWalker(embeddingFrame);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return parentNode;
|
|
},
|
|
|
|
firstChild: function DW_firstChild()
|
|
{
|
|
let node = this.walker.currentNode;
|
|
if (!node)
|
|
return;
|
|
if (node.contentDocument) {
|
|
return this._reparentWalker(node.contentDocument);
|
|
} else if (node instanceof nodeDocument(node).defaultView.GetSVGDocument) {
|
|
return this._reparentWalker(node.getSVGDocument());
|
|
}
|
|
return this.walker.firstChild();
|
|
},
|
|
|
|
lastChild: function DW_lastChild()
|
|
{
|
|
let node = this.walker.currentNode;
|
|
if (!node)
|
|
return;
|
|
if (node.contentDocument) {
|
|
return this._reparentWalker(node.contentDocument);
|
|
} else if (node instanceof nodeDocument(node).defaultView.GetSVGDocument) {
|
|
return this._reparentWalker(node.getSVGDocument());
|
|
}
|
|
return this.walker.lastChild();
|
|
},
|
|
|
|
previousSibling: function DW_previousSibling() this.walker.previousSibling(),
|
|
nextSibling: function DW_nextSibling() this.walker.nextSibling(),
|
|
|
|
// XXX bug 785143: not doing previousNode or nextNode, which would sure be useful.
|
|
};
|
|
|
|
/**
|
|
* Properly escape attribute values.
|
|
*
|
|
* @param {String} attr
|
|
* The attributes for which the values are to be escaped.
|
|
* @return {Array}
|
|
* An array of attribute names and their escaped values.
|
|
*/
|
|
function escapeAttributeValues(attr) {
|
|
let name = null;
|
|
let value = null;
|
|
let result = "";
|
|
let attributes = [];
|
|
|
|
while(attr.length > 0) {
|
|
let match;
|
|
let dirty = false;
|
|
|
|
// Trim quotes and spaces from attr start
|
|
match = attr.match(/^["\s]+/);
|
|
if (match && match.length == 1) {
|
|
attr = attr.substr(match[0].length);
|
|
}
|
|
|
|
// Name
|
|
if (!dirty) {
|
|
match = attr.match(/^([\w-]+)="/);
|
|
if (match && match.length == 2) {
|
|
if (name) {
|
|
// We had a name without a value e.g. disabled. Let's set the value to "";
|
|
value = "";
|
|
} else {
|
|
name = match[1];
|
|
attr = attr.substr(match[0].length);
|
|
}
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
// Value (in the case of multiple attributes)
|
|
if (!dirty) {
|
|
match = attr.match(/^(.+?)"\s+[\w-]+="/);
|
|
if (match && match.length > 1) {
|
|
value = typeof match[1] == "undefined" ? match[2] : match[1];
|
|
attr = attr.substr(value.length);
|
|
value = simpleEscape(value);
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
// Final value
|
|
if (!dirty && attr.indexOf("=\"") == -1) {
|
|
// No more attributes, get the remaining value minus it's ending quote.
|
|
if (attr.charAt(attr.length - 1) == '"') {
|
|
attr = attr.substr(0, attr.length - 1);
|
|
}
|
|
|
|
if (!name) {
|
|
name = attr;
|
|
value = "";
|
|
} else {
|
|
value = simpleEscape(attr);
|
|
}
|
|
attr = "";
|
|
dirty = true;
|
|
}
|
|
|
|
if (name !== null && value !== null) {
|
|
attributes.push({name: name, value: value});
|
|
name = value = null;
|
|
}
|
|
|
|
if (!dirty) {
|
|
// This should never happen but we exit here if it does.
|
|
return attributes;
|
|
}
|
|
}
|
|
return attributes;
|
|
}
|
|
|
|
/**
|
|
* Escape basic html entities <, >, " and '.
|
|
* @param {String} value
|
|
* Value to escape.
|
|
* @return {String}
|
|
* Escaped value.
|
|
*/
|
|
function simpleEscape(value) {
|
|
return value.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* A tree walker filter for avoiding empty whitespace text nodes.
|
|
*/
|
|
function whitespaceTextFilter(aNode)
|
|
{
|
|
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
|
|
!/[^\s]/.exec(aNode.nodeValue)) {
|
|
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
|
|
} else {
|
|
return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
|
|
}
|
|
}
|
|
|
|
XPCOMUtils.defineLazyGetter(MarkupView.prototype, "strings", function () {
|
|
return Services.strings.createBundle(
|
|
"chrome://browser/locale/devtools/inspector.properties");
|
|
});
|