2014-06-25 05:12:07 +00:00
|
|
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
2012-08-23 18:00:43 +00:00
|
|
|
/* 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/. */
|
2015-10-27 09:55:00 +00:00
|
|
|
"use strict";
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-04-11 20:59:08 +00:00
|
|
|
const {Cc, Cu, Ci} = require("chrome");
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
// Page size for pageup/pagedown
|
|
|
|
const PAGE_SIZE = 10;
|
2012-12-20 00:16:45 +00:00
|
|
|
const DEFAULT_MAX_CHILDREN = 100;
|
2013-09-12 22:25:08 +00:00
|
|
|
const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
|
|
|
|
const COLLAPSE_DATA_URL_LENGTH = 60;
|
2014-01-09 11:36:01 +00:00
|
|
|
const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
|
2014-12-26 10:53:00 +00:00
|
|
|
const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
|
|
|
|
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
|
|
|
|
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
|
2015-10-27 09:55:00 +00:00
|
|
|
const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
|
2015-08-05 21:36:05 +00:00
|
|
|
const AUTOCOMPLETE_POPUP_PANEL_ID = "markupview_autoCompletePopup";
|
2012-09-25 16:33:46 +00:00
|
|
|
|
2015-09-21 17:04:18 +00:00
|
|
|
const {UndoStack} = require("devtools/client/shared/undo");
|
|
|
|
const {editableField, InplaceEditor} = require("devtools/client/shared/inplace-editor");
|
|
|
|
const {HTMLEditor} = require("devtools/client/markupview/html-editor");
|
2015-08-26 13:05:13 +00:00
|
|
|
const promise = require("promise");
|
2015-09-21 17:04:18 +00:00
|
|
|
const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
|
|
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
2014-09-29 07:29:00 +00:00
|
|
|
const Heritage = require("sdk/core/heritage");
|
2015-03-24 21:57:54 +00:00
|
|
|
const {setTimeout, clearTimeout, setInterval, clearInterval} = require("sdk/timers");
|
2015-09-21 17:04:18 +00:00
|
|
|
const {parseAttribute} = require("devtools/client/shared/node-attribute-parser");
|
2015-02-03 18:30:45 +00:00
|
|
|
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
|
2015-06-03 22:56:00 +00:00
|
|
|
const {Task} = require("resource://gre/modules/Task.jsm");
|
2015-09-21 17:04:18 +00:00
|
|
|
const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-10-13 23:18:43 +00:00
|
|
|
Cu.import("resource://devtools/shared/gcli/Templater.jsm");
|
2012-09-25 16:33:46 +00:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2014-01-30 16:33:53 +00:00
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
2013-08-08 13:55:39 +00:00
|
|
|
|
|
|
|
loader.lazyGetter(this, "DOMParser", function() {
|
2015-10-27 09:55:00 +00:00
|
|
|
return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
|
2013-08-08 13:55:39 +00:00
|
|
|
});
|
2013-10-02 00:14:00 +00:00
|
|
|
loader.lazyGetter(this, "AutocompletePopup", () => {
|
2015-09-21 17:04:18 +00:00
|
|
|
return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
|
2013-10-02 00:14:00 +00:00
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Vocabulary for the purposes of this file:
|
|
|
|
*
|
|
|
|
* MarkupContainer - the structure that holds an editor and its
|
|
|
|
* immediate children in the markup panel.
|
2014-09-29 07:29:00 +00:00
|
|
|
* - MarkupElementContainer: markup container for element nodes
|
|
|
|
* - MarkupTextContainer: markup container for text / comment nodes
|
|
|
|
* - MarkupReadonlyContainer: markup container for other nodes
|
2012-08-23 18:00:43 +00:00
|
|
|
* 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.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
function MarkupView(aInspector, aFrame, aControllerWindow) {
|
2012-08-23 18:00:43 +00:00
|
|
|
this._inspector = aInspector;
|
2013-06-11 04:18:46 +00:00
|
|
|
this.walker = this._inspector.walker;
|
2012-08-23 18:00:43 +00:00
|
|
|
this._frame = aFrame;
|
2014-12-26 10:53:00 +00:00
|
|
|
this.win = this._frame.contentWindow;
|
2012-08-23 18:00:43 +00:00
|
|
|
this.doc = this._frame.contentDocument;
|
|
|
|
this._elt = this.doc.querySelector("#root");
|
2013-10-24 13:41:03 +00:00
|
|
|
this.htmlEditor = new HTMLEditor(this.doc);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2012-12-20 00:16:45 +00:00
|
|
|
try {
|
|
|
|
this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
|
2015-10-27 09:55:00 +00:00
|
|
|
} catch (ex) {
|
2012-12-20 00:16:45 +00:00
|
|
|
this.maxChildren = DEFAULT_MAX_CHILDREN;
|
|
|
|
}
|
|
|
|
|
2015-11-20 17:24:00 +00:00
|
|
|
this.collapseAttributeLength =
|
|
|
|
Services.prefs.getIntPref("devtools.markup.collapseAttributeLength");
|
|
|
|
|
2013-08-02 10:35:50 +00:00
|
|
|
// Creating the popup to be used to show CSS suggestions.
|
|
|
|
let options = {
|
|
|
|
autoSelect: true,
|
2015-08-05 21:36:05 +00:00
|
|
|
theme: "auto",
|
|
|
|
// panelId option prevents the markupView autocomplete popup from
|
|
|
|
// sharing XUL elements with other views, such as ruleView (see Bug 1191093)
|
|
|
|
panelId: AUTOCOMPLETE_POPUP_PANEL_ID
|
2013-08-02 10:35:50 +00:00
|
|
|
};
|
|
|
|
this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
this.undo = new UndoStack();
|
2012-11-30 08:07:59 +00:00
|
|
|
this.undo.installController(aControllerWindow);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-10-11 15:50:33 +00:00
|
|
|
this._containers = new Map();
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// Binding functions that need to be called in scope.
|
|
|
|
this._mutationObserver = this._mutationObserver.bind(this);
|
|
|
|
this._onDisplayChange = this._onDisplayChange.bind(this);
|
2014-07-20 11:03:59 +00:00
|
|
|
this._onMouseClick = this._onMouseClick.bind(this);
|
2014-12-26 10:53:00 +00:00
|
|
|
this._onMouseUp = this._onMouseUp.bind(this);
|
2015-10-27 09:55:00 +00:00
|
|
|
this._onNewSelection = this._onNewSelection.bind(this);
|
|
|
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
|
|
this._onCopy = this._onCopy.bind(this);
|
|
|
|
this._onFocus = this._onFocus.bind(this);
|
|
|
|
this._onMouseMove = this._onMouseMove.bind(this);
|
|
|
|
this._onMouseLeave = this._onMouseLeave.bind(this);
|
|
|
|
this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
|
|
|
|
|
|
|
|
// Listening to various events.
|
|
|
|
this._elt.addEventListener("click", this._onMouseClick, false);
|
|
|
|
this._elt.addEventListener("mousemove", this._onMouseMove, false);
|
|
|
|
this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
|
2014-12-26 10:53:00 +00:00
|
|
|
this.doc.body.addEventListener("mouseup", this._onMouseUp);
|
2015-10-27 09:55:00 +00:00
|
|
|
this.win.addEventListener("keydown", this._onKeyDown, false);
|
|
|
|
this.win.addEventListener("copy", this._onCopy);
|
|
|
|
this._frame.addEventListener("focus", this._onFocus, false);
|
|
|
|
this.walker.on("mutations", this._mutationObserver);
|
|
|
|
this.walker.on("display-change", this._onDisplayChange);
|
|
|
|
this._inspector.selection.on("new-node-front", this._onNewSelection);
|
|
|
|
this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
this._onNewSelection();
|
2014-01-09 11:36:01 +00:00
|
|
|
this._initTooltips();
|
2013-11-05 16:19:29 +00:00
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
EventEmitter.decorate(this);
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2013-04-11 20:59:08 +00:00
|
|
|
exports.MarkupView = MarkupView;
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
MarkupView.prototype = {
|
2014-08-11 08:43:00 +00:00
|
|
|
/**
|
|
|
|
* How long does a node flash when it mutates (in ms).
|
|
|
|
*/
|
|
|
|
CONTAINER_FLASHING_DURATION: 500,
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
_selectedContainer: null,
|
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
_initTooltips: function() {
|
|
|
|
this.tooltip = new Tooltip(this._inspector.panelDoc);
|
2014-07-20 11:03:59 +00:00
|
|
|
this._makeTooltipPersistent(false);
|
2014-07-18 13:25:03 +00:00
|
|
|
},
|
|
|
|
|
2014-07-20 11:03:59 +00:00
|
|
|
_makeTooltipPersistent: function(state) {
|
|
|
|
if (state) {
|
|
|
|
this.tooltip.stopTogglingOnHover();
|
|
|
|
} else {
|
|
|
|
this.tooltip.startTogglingOnHover(this._elt,
|
|
|
|
this._isImagePreviewTarget.bind(this));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
_onToolboxPickerHover: function(event, nodeFront) {
|
|
|
|
this.showNode(nodeFront).then(() => {
|
|
|
|
this._showContainerAsHovered(nodeFront);
|
|
|
|
}, e => console.error(e));
|
|
|
|
},
|
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
isDragging: false,
|
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
_onMouseMove: function(event) {
|
2015-10-27 09:55:00 +00:00
|
|
|
let target = event.target;
|
|
|
|
|
|
|
|
// Auto-scroll if we're dragging.
|
2014-12-26 10:53:00 +00:00
|
|
|
if (this.isDragging) {
|
|
|
|
event.preventDefault();
|
2015-10-27 09:55:00 +00:00
|
|
|
this._autoScroll(event);
|
2014-12-26 10:53:00 +00:00
|
|
|
return;
|
2015-10-27 09:55:00 +00:00
|
|
|
}
|
2014-01-09 11:36:01 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// Show the current container as hovered and highlight it.
|
|
|
|
// This requires finding the current MarkupContainer (walking up the DOM).
|
2014-01-09 11:36:01 +00:00
|
|
|
while (!target.container) {
|
|
|
|
if (target.tagName.toLowerCase() === "body") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
target = target.parentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
let container = target.container;
|
|
|
|
if (this._hoveredNode !== container.node) {
|
|
|
|
if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
|
|
|
|
this._showBoxModel(container.node);
|
|
|
|
} else {
|
|
|
|
this._hideBoxModel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._showContainerAsHovered(container.node);
|
|
|
|
},
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
/**
|
|
|
|
* Executed on each mouse-move while a node is being dragged in the view.
|
|
|
|
* Auto-scrolls the view to reveal nodes below the fold to drop the dragged
|
|
|
|
* node in.
|
|
|
|
*/
|
|
|
|
_autoScroll: function(event) {
|
|
|
|
let docEl = this.doc.documentElement;
|
|
|
|
|
|
|
|
if (this._autoScrollInterval) {
|
|
|
|
clearInterval(this._autoScrollInterval);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Auto-scroll when the mouse approaches top/bottom edge.
|
|
|
|
let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
|
|
|
|
let fromTop = event.pageY - this.win.scrollY;
|
|
|
|
|
|
|
|
if (fromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
|
|
|
// Map our distance from 0-50 to 5-15 range so the speed is kept in a
|
|
|
|
// range not too fast, not too slow.
|
|
|
|
let speed = map(
|
|
|
|
fromBottom,
|
|
|
|
0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
|
|
|
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
|
|
|
|
|
|
|
this._autoScrollInterval = setInterval(() => {
|
|
|
|
docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
|
|
|
let speed = map(
|
|
|
|
fromTop,
|
|
|
|
0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
|
|
|
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
|
|
|
|
|
|
|
this._autoScrollInterval = setInterval(() => {
|
|
|
|
docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-07-20 11:03:59 +00:00
|
|
|
_onMouseClick: function(event) {
|
|
|
|
// From the target passed here, let's find the parent MarkupContainer
|
|
|
|
// and ask it if the tooltip should be shown
|
|
|
|
let parentNode = event.target;
|
|
|
|
let container;
|
|
|
|
while (parentNode !== this.doc.body) {
|
|
|
|
if (parentNode.container) {
|
|
|
|
container = parentNode.container;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
parentNode = parentNode.parentNode;
|
|
|
|
}
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
if (container instanceof MarkupElementContainer) {
|
2014-07-20 11:03:59 +00:00
|
|
|
// With the newly found container, delegate the tooltip content creation
|
|
|
|
// and decision to show or not the tooltip
|
|
|
|
container._buildEventTooltipContent(event.target, this.tooltip);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
_onMouseUp: function() {
|
2015-08-26 14:30:00 +00:00
|
|
|
this.indicateDropTarget(null);
|
|
|
|
this.indicateDragTarget(null);
|
2015-10-27 09:55:00 +00:00
|
|
|
if (this._autoScrollInterval) {
|
|
|
|
clearInterval(this._autoScrollInterval);
|
2014-12-26 10:53:00 +00:00
|
|
|
}
|
2015-08-26 14:30:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
cancelDragging: function() {
|
|
|
|
if (!this.isDragging) {
|
|
|
|
return;
|
2014-12-26 10:53:00 +00:00
|
|
|
}
|
2015-08-26 14:30:00 +00:00
|
|
|
|
|
|
|
for (let [, container] of this._containers) {
|
|
|
|
if (container.isDragging) {
|
|
|
|
container.cancelDragging();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.indicateDropTarget(null);
|
|
|
|
this.indicateDragTarget(null);
|
2015-10-27 09:55:00 +00:00
|
|
|
if (this._autoScrollInterval) {
|
|
|
|
clearInterval(this._autoScrollInterval);
|
2014-12-26 10:53:00 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
_hoveredNode: null,
|
2014-06-23 15:20:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Show a NodeFront's container as being hovered
|
|
|
|
* @param {NodeFront} nodeFront The node to show as hovered
|
|
|
|
*/
|
2014-01-09 11:36:01 +00:00
|
|
|
_showContainerAsHovered: function(nodeFront) {
|
2014-06-23 15:20:01 +00:00
|
|
|
if (this._hoveredNode === nodeFront) {
|
|
|
|
return;
|
|
|
|
}
|
2014-01-09 11:36:01 +00:00
|
|
|
|
2014-06-23 15:20:01 +00:00
|
|
|
if (this._hoveredNode) {
|
|
|
|
this.getContainer(this._hoveredNode).hovered = false;
|
2014-01-09 11:36:01 +00:00
|
|
|
}
|
2014-06-23 15:20:01 +00:00
|
|
|
|
|
|
|
this.getContainer(nodeFront).hovered = true;
|
|
|
|
this._hoveredNode = nodeFront;
|
2014-01-09 11:36:01 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_onMouseLeave: function() {
|
2015-10-27 09:55:00 +00:00
|
|
|
if (this._autoScrollInterval) {
|
|
|
|
clearInterval(this._autoScrollInterval);
|
|
|
|
}
|
|
|
|
if (this.isDragging) {
|
|
|
|
return;
|
2014-12-26 10:53:00 +00:00
|
|
|
}
|
|
|
|
|
2014-03-13 21:36:48 +00:00
|
|
|
this._hideBoxModel(true);
|
2014-02-25 15:33:57 +00:00
|
|
|
if (this._hoveredNode) {
|
2014-06-23 15:20:01 +00:00
|
|
|
this.getContainer(this._hoveredNode).hovered = false;
|
2014-02-25 15:33:57 +00:00
|
|
|
}
|
2014-02-06 18:45:40 +00:00
|
|
|
this._hoveredNode = null;
|
2014-01-09 11:36:01 +00:00
|
|
|
},
|
|
|
|
|
2014-06-13 14:27:10 +00:00
|
|
|
/**
|
|
|
|
* Show the box model highlighter on a given node front
|
|
|
|
* @param {NodeFront} nodeFront The node to show the highlighter for
|
|
|
|
* @return a promise that resolves when the highlighter for this nodeFront is
|
|
|
|
* shown, taking into account that there could already be highlighter requests
|
|
|
|
* queued up
|
|
|
|
*/
|
2014-06-25 14:40:34 +00:00
|
|
|
_showBoxModel: function(nodeFront) {
|
|
|
|
return this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront);
|
2014-01-09 11:36:01 +00:00
|
|
|
},
|
|
|
|
|
2014-06-13 14:27:10 +00:00
|
|
|
/**
|
|
|
|
* Hide the box model highlighter on a given node front
|
|
|
|
* @param {NodeFront} nodeFront The node to hide the highlighter for
|
|
|
|
* @param {Boolean} forceHide See toolbox-highlighter-utils/unhighlight
|
|
|
|
* @return a promise that resolves when the highlighter for this nodeFront is
|
|
|
|
* hidden, taking into account that there could already be highlighter requests
|
|
|
|
* queued up
|
|
|
|
*/
|
2014-03-13 21:36:48 +00:00
|
|
|
_hideBoxModel: function(forceHide) {
|
2014-03-17 18:11:00 +00:00
|
|
|
return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
|
2014-01-09 11:36:01 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_briefBoxModelTimer: null,
|
|
|
|
|
2015-03-25 16:19:00 +00:00
|
|
|
_clearBriefBoxModelTimer: function() {
|
2014-01-09 11:36:01 +00:00
|
|
|
if (this._briefBoxModelTimer) {
|
2015-03-24 21:57:54 +00:00
|
|
|
clearTimeout(this._briefBoxModelTimer);
|
2015-08-26 13:05:14 +00:00
|
|
|
this._briefBoxModelPromise.resolve();
|
|
|
|
this._briefBoxModelPromise = null;
|
2014-01-09 11:36:01 +00:00
|
|
|
this._briefBoxModelTimer = null;
|
|
|
|
}
|
2015-03-25 16:19:00 +00:00
|
|
|
},
|
2014-01-09 11:36:01 +00:00
|
|
|
|
2015-03-25 16:19:00 +00:00
|
|
|
_brieflyShowBoxModel: function(nodeFront) {
|
|
|
|
this._clearBriefBoxModelTimer();
|
2015-08-26 13:05:14 +00:00
|
|
|
let onShown = this._showBoxModel(nodeFront);
|
|
|
|
this._briefBoxModelPromise = promise.defer();
|
2014-01-09 11:36:01 +00:00
|
|
|
|
2015-03-24 21:57:54 +00:00
|
|
|
this._briefBoxModelTimer = setTimeout(() => {
|
2015-08-26 13:05:14 +00:00
|
|
|
this._hideBoxModel()
|
|
|
|
.then(this._briefBoxModelPromise.resolve,
|
|
|
|
this._briefBoxModelPromise.resolve);
|
2014-01-09 11:36:01 +00:00
|
|
|
}, NEW_SELECTION_HIGHLIGHTER_TIMER);
|
2015-08-26 13:05:14 +00:00
|
|
|
|
|
|
|
return promise.all([onShown, this._briefBoxModelPromise.promise]);
|
2014-01-09 11:36:01 +00:00
|
|
|
},
|
|
|
|
|
2013-10-02 00:14:00 +00:00
|
|
|
template: function(aName, aDest, aOptions={stack: "markup-view.xhtml"}) {
|
2012-08-23 18:00:43 +00:00
|
|
|
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.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
getContainer: function(aNode) {
|
2012-08-23 18:00:43 +00:00
|
|
|
return this._containers.get(aNode);
|
|
|
|
},
|
|
|
|
|
2013-09-16 10:01:25 +00:00
|
|
|
update: function() {
|
2014-06-14 10:49:00 +00:00
|
|
|
let updateChildren = (node) => {
|
2013-09-16 10:01:25 +00:00
|
|
|
this.getContainer(node).update();
|
|
|
|
for (let child of node.treeChildren()) {
|
|
|
|
updateChildren(child);
|
|
|
|
}
|
2014-06-14 10:49:00 +00:00
|
|
|
};
|
2013-09-16 10:01:25 +00:00
|
|
|
|
|
|
|
// Start with the documentElement
|
|
|
|
let documentElement;
|
|
|
|
for (let node of this._rootNode.treeChildren()) {
|
|
|
|
if (node.isDocumentElement === true) {
|
|
|
|
documentElement = node;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recursively update each node starting with documentElement.
|
|
|
|
updateChildren(documentElement);
|
|
|
|
},
|
|
|
|
|
2014-03-19 09:11:34 +00:00
|
|
|
/**
|
|
|
|
* Executed when the mouse hovers over a target in the markup-view and is used
|
|
|
|
* to decide whether this target should be used to display an image preview
|
|
|
|
* tooltip.
|
|
|
|
* Delegates the actual decision to the corresponding MarkupContainer instance
|
|
|
|
* if one is found.
|
2014-09-29 07:29:00 +00:00
|
|
|
* @return the promise returned by MarkupElementContainer._isImagePreviewTarget
|
2014-03-19 09:11:34 +00:00
|
|
|
*/
|
|
|
|
_isImagePreviewTarget: function(target) {
|
2013-11-05 16:19:29 +00:00
|
|
|
// From the target passed here, let's find the parent MarkupContainer
|
|
|
|
// and ask it if the tooltip should be shown
|
2015-11-17 11:36:10 +00:00
|
|
|
if (this.isDragging) {
|
|
|
|
return promise.reject(false);
|
|
|
|
}
|
|
|
|
|
2013-11-05 16:19:29 +00:00
|
|
|
let parent = target, container;
|
|
|
|
while (parent !== this.doc.body) {
|
|
|
|
if (parent.container) {
|
|
|
|
container = parent.container;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
parent = parent.parentNode;
|
|
|
|
}
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
if (container instanceof MarkupElementContainer) {
|
2013-11-05 16:19:29 +00:00
|
|
|
// With the newly found container, delegate the tooltip content creation
|
|
|
|
// and decision to show or not the tooltip
|
2014-09-29 07:29:00 +00:00
|
|
|
return container.isImagePreviewTarget(target, this.tooltip);
|
2013-11-05 16:19:29 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-01-17 22:07:48 +00:00
|
|
|
/**
|
|
|
|
* Given the known reason, should the current selection be briefly highlighted
|
|
|
|
* In a few cases, we don't want to highlight the node:
|
|
|
|
* - If the reason is null (used to reset the selection),
|
|
|
|
* - if it's "inspector-open" (when the inspector opens up, let's not highlight
|
|
|
|
* the default node)
|
|
|
|
* - if it's "navigateaway" (since the page is being navigated away from)
|
|
|
|
* - if it's "test" (this is a special case for mochitest. In tests, we often
|
|
|
|
* need to select elements but don't necessarily want the highlighter to come
|
|
|
|
* and go after a delay as this might break test scenarios)
|
2014-02-06 18:45:40 +00:00
|
|
|
* We also do not want to start a brief highlight timeout if the node is already
|
|
|
|
* being hovered over, since in that case it will already be highlighted.
|
2014-01-17 22:07:48 +00:00
|
|
|
*/
|
|
|
|
_shouldNewSelectionBeHighlighted: function() {
|
|
|
|
let reason = this._inspector.selection.reason;
|
2014-06-17 11:50:41 +00:00
|
|
|
let unwantedReasons = ["inspector-open", "navigateaway", "nodeselected", "test"];
|
2014-02-06 18:45:40 +00:00
|
|
|
let isHighlitNode = this._hoveredNode === this._inspector.selection.nodeFront;
|
|
|
|
return !isHighlitNode && reason && unwantedReasons.indexOf(reason) === -1;
|
2014-01-17 22:07:48 +00:00
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
2015-05-21 13:41:53 +00:00
|
|
|
* React to new-node-front selection events.
|
|
|
|
* Highlights the node if needed, and make sure it is shown and selected in
|
|
|
|
* the view.
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_onNewSelection: function() {
|
2014-01-09 11:36:01 +00:00
|
|
|
let selection = this._inspector.selection;
|
|
|
|
|
2013-10-24 13:41:03 +00:00
|
|
|
this.htmlEditor.hide();
|
2014-06-23 15:20:01 +00:00
|
|
|
if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
|
|
|
|
this.getContainer(this._hoveredNode).hovered = false;
|
|
|
|
this._hoveredNode = null;
|
|
|
|
}
|
|
|
|
|
2015-05-21 13:41:53 +00:00
|
|
|
if (!selection.isNode()) {
|
|
|
|
this.unmarkSelectedNode();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
let done = this._inspector.updating("markup-view");
|
2015-08-26 13:05:14 +00:00
|
|
|
let onShowBoxModel, onShow;
|
2015-05-21 13:41:53 +00:00
|
|
|
|
|
|
|
// Highlight the element briefly if needed.
|
|
|
|
if (this._shouldNewSelectionBeHighlighted()) {
|
2015-08-26 13:05:14 +00:00
|
|
|
onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
|
2015-05-21 13:41:53 +00:00
|
|
|
}
|
|
|
|
|
2015-08-26 13:05:14 +00:00
|
|
|
onShow = this.showNode(selection.nodeFront).then(() => {
|
2015-05-21 13:41:53 +00:00
|
|
|
// We could be destroyed by now.
|
|
|
|
if (this._destroyer) {
|
|
|
|
return promise.reject("markupview destroyed");
|
2014-01-09 11:36:01 +00:00
|
|
|
}
|
|
|
|
|
2015-05-21 13:41:53 +00:00
|
|
|
// Mark the node as selected.
|
|
|
|
this.markNodeAsSelected(selection.nodeFront);
|
|
|
|
|
|
|
|
// Make sure the new selection receives focus so the keyboard can be used.
|
|
|
|
this.maybeFocusNewSelection();
|
|
|
|
}).catch(e => {
|
|
|
|
if (!this._destroyer) {
|
|
|
|
console.error(e);
|
|
|
|
} else {
|
|
|
|
console.warn("Could not mark node as selected, the markup-view was " +
|
|
|
|
"destroyed while showing the node.");
|
|
|
|
}
|
|
|
|
});
|
2015-08-26 13:05:14 +00:00
|
|
|
|
|
|
|
promise.all([onShowBoxModel, onShow]).then(done);
|
2015-05-21 13:41:53 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Focus the current node selection's MarkupContainer if the selection
|
|
|
|
* happened because the user picked an element using the element picker or
|
|
|
|
* browser context menu.
|
|
|
|
*/
|
|
|
|
maybeFocusNewSelection: function() {
|
|
|
|
let {reason, nodeFront} = this._inspector.selection;
|
|
|
|
|
|
|
|
if (reason !== "browser-context-menu" &&
|
|
|
|
reason !== "picker-node-picked") {
|
|
|
|
return;
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
2015-05-21 13:41:53 +00:00
|
|
|
|
|
|
|
this.getContainer(nodeFront).focus();
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a TreeWalker to find the next/previous
|
|
|
|
* node for selection.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_selectionWalker: function(aStart) {
|
2012-08-23 18:00:43 +00:00
|
|
|
let walker = this.doc.createTreeWalker(
|
|
|
|
aStart || this._elt,
|
|
|
|
Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
|
|
|
|
function(aElement) {
|
2013-06-11 04:18:46 +00:00
|
|
|
if (aElement.container &&
|
|
|
|
aElement.container.elt === aElement &&
|
|
|
|
aElement.container.visible) {
|
2012-08-23 18:00:43 +00:00
|
|
|
return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
|
|
|
|
}
|
|
|
|
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
|
2013-02-06 14:22:33 +00:00
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
);
|
|
|
|
walker.currentNode = this._selectedContainer.elt;
|
|
|
|
return walker;
|
|
|
|
},
|
|
|
|
|
2015-07-02 20:43:19 +00:00
|
|
|
_onCopy: function (evt) {
|
|
|
|
// Ignore copy events from editors
|
|
|
|
if (this._isInputOrTextarea(evt.target)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let selection = this._inspector.selection;
|
|
|
|
if (selection.isNode()) {
|
|
|
|
this._inspector.copyOuterHTML();
|
|
|
|
}
|
|
|
|
evt.stopPropagation();
|
|
|
|
evt.preventDefault();
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Key handling.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_onKeyDown: function(aEvent) {
|
2012-08-23 18:00:43 +00:00
|
|
|
let handled = true;
|
|
|
|
|
|
|
|
// Ignore keystrokes that originated in editors.
|
2015-07-02 20:43:19 +00:00
|
|
|
if (this._isInputOrTextarea(aEvent.target)) {
|
2012-08-23 18:00:43 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(aEvent.keyCode) {
|
2013-07-31 18:33:56 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_H:
|
2014-06-16 11:43:00 +00:00
|
|
|
if (aEvent.metaKey || aEvent.shiftKey) {
|
|
|
|
handled = false;
|
2013-07-31 18:33:56 +00:00
|
|
|
} else {
|
2014-06-16 11:43:00 +00:00
|
|
|
let node = this._selectedContainer.node;
|
|
|
|
if (node.hidden) {
|
2015-05-11 14:47:24 +00:00
|
|
|
this.walker.unhideNode(node);
|
2014-06-16 11:43:00 +00:00
|
|
|
} else {
|
2015-05-11 14:47:24 +00:00
|
|
|
this.walker.hideNode(node);
|
2014-06-16 11:43:00 +00:00
|
|
|
}
|
2013-07-31 18:33:56 +00:00
|
|
|
}
|
|
|
|
break;
|
2012-08-23 18:00:43 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
|
2015-10-22 13:49:14 +00:00
|
|
|
this.deleteNodeOrAttribute();
|
2012-08-23 18:00:43 +00:00
|
|
|
break;
|
2014-12-07 19:54:00 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
|
2015-10-22 13:49:14 +00:00
|
|
|
this.deleteNodeOrAttribute(true);
|
2014-12-07 19:54:00 +00:00
|
|
|
break;
|
2012-08-23 18:00:43 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
|
2014-06-23 15:20:01 +00:00
|
|
|
let rootContainer = this.getContainer(this._rootNode);
|
2013-06-11 04:18:46 +00:00
|
|
|
this.navigate(rootContainer.children.firstChild.container);
|
2012-08-23 18:00:43 +00:00
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
|
2013-03-01 02:50:24 +00:00
|
|
|
if (this._selectedContainer.expanded) {
|
|
|
|
this.collapseNode(this._selectedContainer.node);
|
|
|
|
} else {
|
|
|
|
let parent = this._selectionWalker().parentNode();
|
|
|
|
if (parent) {
|
|
|
|
this.navigate(parent.container);
|
|
|
|
}
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
break;
|
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
|
2013-09-12 14:48:13 +00:00
|
|
|
if (!this._selectedContainer.expanded &&
|
|
|
|
this._selectedContainer.hasChildren) {
|
2013-06-11 04:18:46 +00:00
|
|
|
this._expandContainer(this._selectedContainer);
|
2013-03-01 02:50:24 +00:00
|
|
|
} else {
|
|
|
|
let next = this._selectionWalker().nextNode();
|
|
|
|
if (next) {
|
|
|
|
this.navigate(next.container);
|
|
|
|
}
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
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;
|
|
|
|
}
|
2013-11-07 16:23:25 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_F2: {
|
|
|
|
this.beginEditingOuterHTML(this._selectedContainer.node);
|
|
|
|
break;
|
|
|
|
}
|
2015-09-19 03:22:00 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_S: {
|
|
|
|
let selection = this._selectedContainer.node;
|
|
|
|
this._inspector.scrollNodeIntoView(selection);
|
|
|
|
break;
|
|
|
|
}
|
2015-08-26 14:30:00 +00:00
|
|
|
case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: {
|
|
|
|
if (this.isDragging) {
|
|
|
|
this.cancelDragging();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
default:
|
|
|
|
handled = false;
|
|
|
|
}
|
|
|
|
if (handled) {
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
aEvent.preventDefault();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-07-02 20:43:19 +00:00
|
|
|
/**
|
|
|
|
* Check if a node is an input or textarea
|
|
|
|
*/
|
|
|
|
_isInputOrTextarea : function (element) {
|
|
|
|
let name = element.tagName.toLowerCase();
|
|
|
|
return name === "input" || name === "textarea";
|
|
|
|
},
|
|
|
|
|
2015-10-22 13:49:14 +00:00
|
|
|
/**
|
|
|
|
* If there's an attribute on the current node that's currently focused, then
|
|
|
|
* delete this attribute, otherwise delete the node itself.
|
|
|
|
* @param {boolean} moveBackward If set to true and if we're deleting the
|
|
|
|
* node, focus the previous sibling after deletion, otherwise the next one.
|
|
|
|
*/
|
|
|
|
deleteNodeOrAttribute: function(moveBackward) {
|
|
|
|
let focusedAttribute = this.doc.activeElement
|
|
|
|
? this.doc.activeElement.closest(".attreditor")
|
|
|
|
: null;
|
|
|
|
if (focusedAttribute) {
|
|
|
|
// The focused attribute might not be in the current selected container.
|
|
|
|
let container = focusedAttribute.closest("li.child").container;
|
|
|
|
container.removeAttribute(focusedAttribute.dataset.attr);
|
|
|
|
} else {
|
|
|
|
this.deleteNode(this._selectedContainer.node, moveBackward);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Delete a node from the DOM.
|
|
|
|
* This is an undoable action.
|
2014-12-07 19:54:00 +00:00
|
|
|
*
|
|
|
|
* @param {NodeFront} aNode The node to remove.
|
|
|
|
* @param {boolean} moveBackward If set to true, focus the previous sibling,
|
2015-10-22 13:49:14 +00:00
|
|
|
* otherwise the next one.
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2014-12-07 19:54:00 +00:00
|
|
|
deleteNode: function(aNode, moveBackward) {
|
2013-06-17 13:52:55 +00:00
|
|
|
if (aNode.isDocumentElement ||
|
2014-09-29 07:29:00 +00:00
|
|
|
aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE ||
|
|
|
|
aNode.isAnonymous) {
|
2012-08-25 00:59:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(aNode);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-06-17 13:52:55 +00:00
|
|
|
// Retain the node so we can undo this...
|
|
|
|
this.walker.retainNode(aNode).then(() => {
|
|
|
|
let parent = aNode.parentNode();
|
2014-11-21 20:48:04 +00:00
|
|
|
let nextSibling = null;
|
2013-06-17 13:52:55 +00:00
|
|
|
this.undo.do(() => {
|
2014-11-21 20:48:04 +00:00
|
|
|
this.walker.removeNode(aNode).then(siblings => {
|
|
|
|
nextSibling = siblings.nextSibling;
|
2014-12-07 19:54:00 +00:00
|
|
|
let focusNode = moveBackward ? siblings.previousSibling : nextSibling;
|
|
|
|
|
|
|
|
// If we can't move as the user wants, we move to the other direction.
|
|
|
|
// If there is no sibling elements anymore, move to the parent node.
|
|
|
|
if (!focusNode) {
|
|
|
|
focusNode = nextSibling || siblings.previousSibling || parent;
|
|
|
|
}
|
|
|
|
|
2014-11-21 20:48:04 +00:00
|
|
|
if (container.selected) {
|
|
|
|
this.navigate(this.getContainer(focusNode));
|
|
|
|
}
|
2013-06-17 13:52:55 +00:00
|
|
|
});
|
|
|
|
}, () => {
|
2014-11-21 20:48:04 +00:00
|
|
|
this.walker.insertBefore(aNode, parent, nextSibling);
|
2013-06-17 13:52:55 +00:00
|
|
|
});
|
|
|
|
}).then(null, console.error);
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If an editable item is focused, select its container.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_onFocus: function(aEvent) {
|
2012-08-23 18:00:43 +00:00
|
|
|
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.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
navigate: function(aContainer, aIgnoreFocus) {
|
2012-08-23 18:00:43 +00:00
|
|
|
if (!aContainer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let node = aContainer.node;
|
2013-10-24 13:41:03 +00:00
|
|
|
this.markNodeAsSelected(node, "treepanel");
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
if (!aIgnoreFocus) {
|
|
|
|
aContainer.focus();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Make sure a node is included in the markup tool.
|
|
|
|
*
|
2014-09-29 07:29:00 +00:00
|
|
|
* @param NodeFront aNode
|
2012-08-23 18:00:43 +00:00
|
|
|
* The node in the content document.
|
2013-09-23 06:46:12 +00:00
|
|
|
* @param boolean aFlashNode
|
|
|
|
* Whether the newly imported node should be flashed
|
2012-08-23 18:00:43 +00:00
|
|
|
* @returns MarkupContainer The MarkupContainer object for this element.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
importNode: function(aNode, aFlashNode) {
|
2012-08-23 18:00:43 +00:00
|
|
|
if (!aNode) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._containers.has(aNode)) {
|
2014-06-23 15:20:01 +00:00
|
|
|
return this.getContainer(aNode);
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
let container;
|
|
|
|
let {nodeType, isPseudoElement} = aNode;
|
2013-06-11 04:18:46 +00:00
|
|
|
if (aNode === this.walker.rootNode) {
|
2014-09-29 07:29:00 +00:00
|
|
|
container = new RootContainer(this, aNode);
|
2012-08-23 18:00:43 +00:00
|
|
|
this._elt.appendChild(container.elt);
|
|
|
|
this._rootNode = aNode;
|
2014-09-29 07:29:00 +00:00
|
|
|
} else if (nodeType == Ci.nsIDOMNode.ELEMENT_NODE && !isPseudoElement) {
|
|
|
|
container = new MarkupElementContainer(this, aNode, this._inspector);
|
|
|
|
} else if (nodeType == Ci.nsIDOMNode.COMMENT_NODE ||
|
|
|
|
nodeType == Ci.nsIDOMNode.TEXT_NODE) {
|
|
|
|
container = new MarkupTextContainer(this, aNode, this._inspector);
|
2013-06-11 04:18:46 +00:00
|
|
|
} else {
|
2014-09-29 07:29:00 +00:00
|
|
|
container = new MarkupReadOnlyContainer(this, aNode, this._inspector);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aFlashNode) {
|
|
|
|
container.flashMutation();
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this._containers.set(aNode, container);
|
2012-12-20 00:16:45 +00:00
|
|
|
container.childrenDirty = true;
|
2013-06-11 04:18:46 +00:00
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
this._updateChildren(container);
|
|
|
|
|
2015-05-21 11:03:54 +00:00
|
|
|
this._inspector.emit("container-created", container);
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
return container;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mutation observer used for included nodes.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_mutationObserver: function(aMutations) {
|
2012-08-23 18:00:43 +00:00
|
|
|
for (let mutation of aMutations) {
|
2013-06-11 04:18:46 +00:00
|
|
|
let type = mutation.type;
|
|
|
|
let target = mutation.target;
|
|
|
|
|
|
|
|
if (mutation.type === "documentUnload") {
|
|
|
|
// Treat this as a childList change of the child (maybe the protocol
|
|
|
|
// should do this).
|
2013-09-12 14:48:13 +00:00
|
|
|
type = "childList";
|
2013-06-11 04:18:46 +00:00
|
|
|
target = mutation.targetParent;
|
|
|
|
if (!target) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(target);
|
2012-08-25 18:04:46 +00:00
|
|
|
if (!container) {
|
2013-06-11 04:18:46 +00:00
|
|
|
// Container might not exist if this came from a load event for a node
|
2012-08-25 18:04:46 +00:00
|
|
|
// we're not viewing.
|
|
|
|
continue;
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
if (type === "attributes" || type === "characterData") {
|
2013-10-08 12:53:19 +00:00
|
|
|
container.update();
|
2015-09-24 15:23:34 +00:00
|
|
|
} else if (type === "childList" || type === "nativeAnonymousChildList") {
|
2012-12-20 00:16:45 +00:00
|
|
|
container.childrenDirty = true;
|
2013-10-24 13:41:03 +00:00
|
|
|
// Update the children to take care of changes in the markup view DOM.
|
2014-10-08 22:46:16 +00:00
|
|
|
this._updateChildren(container, {flash: true});
|
2015-09-01 18:45:53 +00:00
|
|
|
} else if (type === "pseudoClassLock") {
|
|
|
|
container.update();
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
}
|
2013-10-15 12:09:49 +00:00
|
|
|
|
Bug 1222409 - Listen to window resize events on server and use this to refresh style-inspector; r=bgrins
1 - Make the LayoutChangesObserver also send "resize" events; r=bgrins
The LayoutChangesObserver was originally made to observe all kinds of
layout-related events. So far, it was only observing reflows though.
This adds the capability to also observe resize events on the content
window.
2 - Removed the non-e10s rule/computed-views refreshing mechanism; r=bgrins
When the window is resized, the styles shown in the rule-view and
computed-view need to be updated (media-queries may be at play).
This was done before using a local-only, non-e10s solution. The
inspector-panel would listen to the resize event on the linkedBrowser
in the current tab.
This, obviously, did not work with e10s or across a remote connection.
This change just removes all of the code involved with this.
This won't cause any regression or backwards-compatibility problems as
a new server-driven resize observer is being put in place in this bug.
Even if you connected to an older server, you wouldn't see a difference
because the refresh-on-resize didn't work over remote connections already.
3 - Refresh the style-inspector when the LayoutChangesObserver detects resize
The implementation is simple, the inspector actor uses the
LayoutChangesObserver to detect window resize, and when it does, it
forwards the event to its front.
This is similar to how we deal with reflow events, except that for
reflows, the inspector actor (walker in this case), first filters on
the server to see if the reflow would indeed impact known nodes.
For resize events, it seemed more complex to do this kind of server
side filtering as this would involve remembering which node is currently
selected and which style were applied, and then compare that with the
new styles.
4 - Tests for the style-inspector refresh on window resize
--HG--
extra : commitid : 4AAhw4VBYII
extra : rebase_source : 412159e2a189541758613dd2fae954d973096f72
2015-11-26 11:18:17 +00:00
|
|
|
this._waitForChildren().then(() => {
|
2015-04-17 10:09:58 +00:00
|
|
|
if (this._destroyer) {
|
|
|
|
console.warn("Could not fully update after markup mutations, " +
|
|
|
|
"the markup-view was destroyed while waiting for children.");
|
|
|
|
return;
|
|
|
|
}
|
2013-09-23 06:46:12 +00:00
|
|
|
this._flashMutatedNodes(aMutations);
|
2013-10-24 13:41:03 +00:00
|
|
|
this._inspector.emit("markupmutation", aMutations);
|
|
|
|
|
|
|
|
// Since the htmlEditor is absolutely positioned, a mutation may change
|
|
|
|
// the location in which it should be shown.
|
|
|
|
this.htmlEditor.refresh();
|
2013-06-11 04:18:46 +00:00
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
2014-06-05 12:50:03 +00:00
|
|
|
/**
|
|
|
|
* React to display-change events from the walker
|
|
|
|
* @param {Array} nodes An array of nodeFronts
|
|
|
|
*/
|
|
|
|
_onDisplayChange: function(nodes) {
|
|
|
|
for (let node of nodes) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(node);
|
2014-06-05 12:50:03 +00:00
|
|
|
if (container) {
|
|
|
|
container.isDisplayed = node.isDisplayed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-09-23 06:46:12 +00:00
|
|
|
/**
|
|
|
|
* Given a list of mutations returned by the mutation observer, flash the
|
|
|
|
* corresponding containers to attract attention.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_flashMutatedNodes: function(aMutations) {
|
2013-09-23 06:46:12 +00:00
|
|
|
let addedOrEditedContainers = new Set();
|
|
|
|
let removedContainers = new Set();
|
|
|
|
|
2015-03-24 21:57:57 +00:00
|
|
|
for (let {type, target, added, removed, newValue} of aMutations) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(target);
|
2013-09-23 06:46:12 +00:00
|
|
|
|
|
|
|
if (container) {
|
2015-03-24 21:57:57 +00:00
|
|
|
if (type === "characterData") {
|
|
|
|
addedOrEditedContainers.add(container);
|
|
|
|
} else if (type === "attributes" && newValue === null) {
|
|
|
|
// Removed attributes should flash the entire node.
|
|
|
|
// New or changed attributes will flash the attribute itself
|
|
|
|
// in ElementEditor.flashAttribute.
|
2013-09-23 06:46:12 +00:00
|
|
|
addedOrEditedContainers.add(container);
|
|
|
|
} else if (type === "childList") {
|
|
|
|
// If there has been removals, flash the parent
|
|
|
|
if (removed.length) {
|
|
|
|
removedContainers.add(container);
|
|
|
|
}
|
|
|
|
|
2014-11-22 07:48:00 +00:00
|
|
|
// If there has been additions, flash the nodes if their associated
|
|
|
|
// container exist (so if their parent is expanded in the inspector).
|
2013-09-23 06:46:12 +00:00
|
|
|
added.forEach(added => {
|
2014-06-23 15:20:01 +00:00
|
|
|
let addedContainer = this.getContainer(added);
|
2014-11-22 07:48:00 +00:00
|
|
|
if (addedContainer) {
|
|
|
|
addedOrEditedContainers.add(addedContainer);
|
|
|
|
|
|
|
|
// The node may be added as a result of an append, in which case
|
|
|
|
// it will have been removed from another container first, but in
|
|
|
|
// these cases we don't want to flash both the removal and the
|
|
|
|
// addition
|
|
|
|
removedContainers.delete(container);
|
|
|
|
}
|
2013-09-23 06:46:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let container of removedContainers) {
|
|
|
|
container.flashMutation();
|
|
|
|
}
|
|
|
|
for (let container of addedOrEditedContainers) {
|
|
|
|
container.flashMutation();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Make sure the given node's parents are expanded and the
|
|
|
|
* node is scrolled on to screen.
|
|
|
|
*/
|
2015-05-09 03:39:23 +00:00
|
|
|
showNode: function(aNode, centered=true) {
|
2013-06-11 04:18:46 +00:00
|
|
|
let parent = aNode;
|
2014-01-10 17:55:58 +00:00
|
|
|
|
|
|
|
this.importNode(aNode);
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
while ((parent = parent.parentNode())) {
|
|
|
|
this.importNode(parent);
|
2012-08-23 18:00:43 +00:00
|
|
|
this.expandNode(parent);
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
|
|
|
|
return this._waitForChildren().then(() => {
|
2014-12-15 07:52:00 +00:00
|
|
|
if (this._destroyer) {
|
|
|
|
return promise.reject("markupview destroyed");
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
return this._ensureVisible(aNode);
|
|
|
|
}).then(() => {
|
2015-09-15 04:32:00 +00:00
|
|
|
scrollIntoViewIfNeeded(this.getContainer(aNode).editor.elt, centered);
|
2014-12-10 08:09:19 +00:00
|
|
|
}, e => {
|
|
|
|
// Only report this rejection as an error if the panel hasn't been
|
|
|
|
// destroyed in the meantime.
|
|
|
|
if (!this._destroyer) {
|
|
|
|
console.error(e);
|
|
|
|
} else {
|
|
|
|
console.warn("Could not show the node, the markup-view was destroyed " +
|
|
|
|
"while waiting for children");
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Expand the container's children.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_expandContainer: function(aContainer) {
|
2013-09-23 06:46:12 +00:00
|
|
|
return this._updateChildren(aContainer, {expand: true}).then(() => {
|
2014-12-26 10:53:00 +00:00
|
|
|
if (this._destroyer) {
|
|
|
|
console.warn("Could not expand the node, the markup-view was destroyed");
|
|
|
|
return;
|
2015-04-17 10:09:58 +00:00
|
|
|
}
|
2015-05-13 21:55:09 +00:00
|
|
|
aContainer.setExpanded(true);
|
2013-09-12 14:48:13 +00:00
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Expand the node's children.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
expandNode: function(aNode) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(aNode);
|
2012-08-23 18:00:43 +00:00
|
|
|
this._expandContainer(container);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Expand the entire tree beneath a container.
|
|
|
|
*
|
|
|
|
* @param aContainer The container to expand.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_expandAll: function(aContainer) {
|
2013-06-11 04:18:46 +00:00
|
|
|
return this._expandContainer(aContainer).then(() => {
|
|
|
|
let child = aContainer.children.firstChild;
|
|
|
|
let promises = [];
|
|
|
|
while (child) {
|
|
|
|
promises.push(this._expandAll(child.container));
|
|
|
|
child = child.nextSibling;
|
|
|
|
}
|
|
|
|
return promise.all(promises);
|
|
|
|
}).then(null, console.error);
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Expand the entire tree beneath a node.
|
|
|
|
*
|
|
|
|
* @param aContainer The node to expand, or null
|
|
|
|
* to start from the top.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
expandAll: function(aNode) {
|
2012-08-23 18:00:43 +00:00
|
|
|
aNode = aNode || this._rootNode;
|
2014-06-23 15:20:01 +00:00
|
|
|
return this._expandAll(this.getContainer(aNode));
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Collapse the node's children.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
collapseNode: function(aNode) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(aNode);
|
2015-05-13 21:55:09 +00:00
|
|
|
container.setExpanded(false);
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
2014-11-22 07:48:00 +00:00
|
|
|
/**
|
|
|
|
* Returns either the innerHTML or the outerHTML for a remote node.
|
|
|
|
* @param aNode The NodeFront to get the outerHTML / innerHTML for.
|
|
|
|
* @param isOuter A boolean that, if true, makes the function return the
|
|
|
|
* outerHTML, otherwise the innerHTML.
|
|
|
|
* @returns A promise that will be resolved with the outerHTML / innerHTML.
|
|
|
|
*/
|
|
|
|
_getNodeHTML: function(aNode, isOuter) {
|
|
|
|
let walkerPromise = null;
|
|
|
|
|
|
|
|
if (isOuter) {
|
|
|
|
walkerPromise = this.walker.outerHTML(aNode);
|
|
|
|
} else {
|
|
|
|
walkerPromise = this.walker.innerHTML(aNode);
|
|
|
|
}
|
|
|
|
|
|
|
|
return walkerPromise.then(longstr => {
|
|
|
|
return longstr.string().then(html => {
|
|
|
|
longstr.release().then(null, console.error);
|
|
|
|
return html;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-10-24 13:41:03 +00:00
|
|
|
/**
|
|
|
|
* Retrieve the outerHTML for a remote node.
|
|
|
|
* @param aNode The NodeFront to get the outerHTML for.
|
|
|
|
* @returns A promise that will be resolved with the outerHTML.
|
|
|
|
*/
|
|
|
|
getNodeOuterHTML: function(aNode) {
|
2014-11-22 07:48:00 +00:00
|
|
|
return this._getNodeHTML(aNode, true);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the innerHTML for a remote node.
|
|
|
|
* @param aNode The NodeFront to get the innerHTML for.
|
|
|
|
* @returns A promise that will be resolved with the innerHTML.
|
|
|
|
*/
|
|
|
|
getNodeInnerHTML: function(aNode) {
|
|
|
|
return this._getNodeHTML(aNode);
|
2013-10-24 13:41:03 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2014-10-08 22:46:16 +00:00
|
|
|
* Listen to mutations, expect a given node to be removed and try and select
|
|
|
|
* the node that sits at the same place instead.
|
|
|
|
* This is useful when changing the outerHTML or the tag name so that the
|
|
|
|
* newly inserted node gets selected instead of the one that just got removed.
|
2013-10-24 13:41:03 +00:00
|
|
|
*/
|
2014-10-08 22:46:16 +00:00
|
|
|
reselectOnRemoved: function(removedNode, reason) {
|
|
|
|
// Only allow one removed node reselection at a time, so that when there are
|
|
|
|
// more than 1 request in parallel, the last one wins.
|
|
|
|
this.cancelReselectOnRemoved();
|
|
|
|
|
|
|
|
// Get the removedNode index in its parent node to reselect the right node.
|
|
|
|
let isHTMLTag = removedNode.tagName.toLowerCase() === "html";
|
|
|
|
let oldContainer = this.getContainer(removedNode);
|
|
|
|
let parentContainer = this.getContainer(removedNode.parentNode());
|
|
|
|
let childIndex = parentContainer.getChildContainers().indexOf(oldContainer);
|
|
|
|
|
|
|
|
let onMutations = this._removedNodeObserver = (e, mutations) => {
|
|
|
|
let isNodeRemovalMutation = false;
|
|
|
|
for (let mutation of mutations) {
|
|
|
|
let containsRemovedNode = mutation.removed &&
|
|
|
|
mutation.removed.some(n => n === removedNode);
|
|
|
|
if (mutation.type === "childList" && (containsRemovedNode || isHTMLTag)) {
|
|
|
|
isNodeRemovalMutation = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!isNodeRemovalMutation) {
|
|
|
|
return;
|
|
|
|
}
|
2013-10-24 13:41:03 +00:00
|
|
|
|
2014-10-08 22:46:16 +00:00
|
|
|
this._inspector.off("markupmutation", onMutations);
|
|
|
|
this._removedNodeObserver = null;
|
|
|
|
|
|
|
|
// Don't select the new node if the user has already changed the current
|
|
|
|
// selection.
|
|
|
|
if (this._inspector.selection.nodeFront === parentContainer.node ||
|
|
|
|
(this._inspector.selection.nodeFront === removedNode && isHTMLTag)) {
|
|
|
|
let childContainers = parentContainer.getChildContainers();
|
|
|
|
if (childContainers && childContainers[childIndex]) {
|
|
|
|
this.markNodeAsSelected(childContainers[childIndex].node, reason);
|
|
|
|
if (childContainers[childIndex].hasChildren) {
|
|
|
|
this.expandNode(childContainers[childIndex].node);
|
|
|
|
}
|
|
|
|
this.emit("reselectedonremoved");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2013-10-24 13:41:03 +00:00
|
|
|
|
2014-10-08 22:46:16 +00:00
|
|
|
// Start listening for mutations until we find a childList change that has
|
|
|
|
// removedNode removed.
|
|
|
|
this._inspector.on("markupmutation", onMutations);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Make sure to stop listening for node removal markupmutations and not
|
|
|
|
* reselect the corresponding node when that happens.
|
|
|
|
* Useful when the outerHTML/tagname edition failed.
|
|
|
|
*/
|
|
|
|
cancelReselectOnRemoved: function() {
|
|
|
|
if (this._removedNodeObserver) {
|
|
|
|
this._inspector.off("markupmutation", this._removedNodeObserver);
|
|
|
|
this._removedNodeObserver = null;
|
|
|
|
this.emit("canceledreselectonremoved");
|
|
|
|
}
|
2013-10-24 13:41:03 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2014-08-21 16:59:53 +00:00
|
|
|
* Replace the outerHTML of any node displayed in the inspector with
|
|
|
|
* some other HTML code
|
2014-11-22 07:48:00 +00:00
|
|
|
* @param {NodeFront} node node which outerHTML will be replaced.
|
|
|
|
* @param {string} newValue The new outerHTML to set on the node.
|
|
|
|
* @param {string} oldValue The old outerHTML that will be used if the
|
|
|
|
* user undoes the update.
|
2014-08-21 16:59:53 +00:00
|
|
|
* @returns A promise that will resolve when the outer HTML has been updated.
|
2013-10-24 13:41:03 +00:00
|
|
|
*/
|
2014-11-22 07:48:00 +00:00
|
|
|
updateNodeOuterHTML: function(node, newValue, oldValue) {
|
|
|
|
let container = this.getContainer(node);
|
2013-10-24 13:41:03 +00:00
|
|
|
if (!container) {
|
2014-08-21 16:59:53 +00:00
|
|
|
return promise.reject();
|
2013-10-24 13:41:03 +00:00
|
|
|
}
|
|
|
|
|
2014-10-08 22:46:16 +00:00
|
|
|
// Changing the outerHTML removes the node which outerHTML was changed.
|
|
|
|
// Listen to this removal to reselect the right node afterwards.
|
2014-11-22 07:48:00 +00:00
|
|
|
this.reselectOnRemoved(node, "outerhtml");
|
|
|
|
return this.walker.setOuterHTML(node, newValue).then(null, () => {
|
2014-10-08 22:46:16 +00:00
|
|
|
this.cancelReselectOnRemoved();
|
2013-10-24 13:41:03 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2014-11-22 07:48:00 +00:00
|
|
|
/**
|
|
|
|
* Replace the innerHTML of any node displayed in the inspector with
|
|
|
|
* some other HTML code
|
|
|
|
* @param {Node} node node which innerHTML will be replaced.
|
|
|
|
* @param {string} newValue The new innerHTML to set on the node.
|
|
|
|
* @param {string} oldValue The old innerHTML that will be used if the user
|
|
|
|
* undoes the update.
|
|
|
|
* @returns A promise that will resolve when the inner HTML has been updated.
|
|
|
|
*/
|
|
|
|
updateNodeInnerHTML: function(node, newValue, oldValue) {
|
|
|
|
let container = this.getContainer(node);
|
|
|
|
if (!container) {
|
|
|
|
return promise.reject();
|
|
|
|
}
|
|
|
|
|
|
|
|
let def = promise.defer();
|
|
|
|
|
|
|
|
container.undo.do(() => {
|
|
|
|
this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject);
|
|
|
|
}, () => {
|
|
|
|
this.walker.setInnerHTML(node, oldValue);
|
|
|
|
});
|
|
|
|
|
|
|
|
return def.promise;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Insert adjacent HTML to any node displayed in the inspector.
|
|
|
|
*
|
|
|
|
* @param {NodeFront} node The reference node.
|
|
|
|
* @param {string} position The position as specified for Element.insertAdjacentHTML
|
|
|
|
* (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
|
|
|
|
* @param {string} newValue The adjacent HTML.
|
|
|
|
* @returns A promise that will resolve when the adjacent HTML has
|
|
|
|
* been inserted.
|
|
|
|
*/
|
|
|
|
insertAdjacentHTMLToNode: function(node, position, value) {
|
|
|
|
let container = this.getContainer(node);
|
|
|
|
if (!container) {
|
|
|
|
return promise.reject();
|
|
|
|
}
|
|
|
|
|
|
|
|
let def = promise.defer();
|
|
|
|
|
|
|
|
let injectedNodes = [];
|
|
|
|
container.undo.do(() => {
|
|
|
|
this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => {
|
|
|
|
injectedNodes = nodeArray.nodes;
|
|
|
|
return nodeArray;
|
|
|
|
}).then(def.resolve, def.reject);
|
|
|
|
}, () => {
|
|
|
|
this.walker.removeNodes(injectedNodes);
|
|
|
|
});
|
|
|
|
|
|
|
|
return def.promise;
|
|
|
|
},
|
|
|
|
|
2013-10-24 13:41:03 +00:00
|
|
|
/**
|
|
|
|
* Open an editor in the UI to allow editing of a node's outerHTML.
|
|
|
|
* @param aNode The NodeFront to edit.
|
|
|
|
*/
|
|
|
|
beginEditingOuterHTML: function(aNode) {
|
2014-11-22 07:48:00 +00:00
|
|
|
this.getNodeOuterHTML(aNode).then(oldValue => {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(aNode);
|
2013-10-24 13:41:03 +00:00
|
|
|
if (!container) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.htmlEditor.show(container.tagLine, oldValue);
|
2013-11-07 16:23:25 +00:00
|
|
|
this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
|
|
|
|
// Need to focus the <html> element instead of the frame / window
|
|
|
|
// in order to give keyboard focus back to doc (from editor).
|
2015-10-27 09:55:00 +00:00
|
|
|
this.doc.documentElement.focus();
|
2013-11-07 16:23:25 +00:00
|
|
|
|
2013-10-24 13:41:03 +00:00
|
|
|
if (aCommit) {
|
|
|
|
this.updateNodeOuterHTML(aNode, aValue, oldValue);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mark the given node expanded.
|
2014-04-17 21:39:29 +00:00
|
|
|
* @param {NodeFront} aNode The NodeFront to mark as expanded.
|
|
|
|
* @param {Boolean} aExpanded Whether the expand or collapse.
|
|
|
|
* @param {Boolean} aExpandDescendants Whether to expand all descendants too
|
2013-10-24 13:41:03 +00:00
|
|
|
*/
|
2014-04-17 21:39:29 +00:00
|
|
|
setNodeExpanded: function(aNode, aExpanded, aExpandDescendants) {
|
2013-06-11 04:18:46 +00:00
|
|
|
if (aExpanded) {
|
2014-04-17 21:39:29 +00:00
|
|
|
if (aExpandDescendants) {
|
|
|
|
this.expandAll(aNode);
|
|
|
|
} else {
|
|
|
|
this.expandNode(aNode);
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
} else {
|
|
|
|
this.collapseNode(aNode);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
2013-10-24 13:41:03 +00:00
|
|
|
* Mark the given node selected, and update the inspector.selection
|
|
|
|
* object's NodeFront to keep consistent state between UI and selection.
|
2015-05-21 13:41:53 +00:00
|
|
|
* @param {NodeFront} aNode The NodeFront to mark as selected.
|
|
|
|
* @param {String} reason The reason for marking the node as selected.
|
|
|
|
* @return {Boolean} False if the node is already marked as selected, true
|
|
|
|
* otherwise.
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2015-05-21 13:41:53 +00:00
|
|
|
markNodeAsSelected: function(node, reason) {
|
|
|
|
let container = this.getContainer(node);
|
2012-08-23 18:00:43 +00:00
|
|
|
if (this._selectedContainer === container) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-05-21 13:41:53 +00:00
|
|
|
|
|
|
|
// Un-select the previous container.
|
2012-08-23 18:00:43 +00:00
|
|
|
if (this._selectedContainer) {
|
|
|
|
this._selectedContainer.selected = false;
|
|
|
|
}
|
2015-05-21 13:41:53 +00:00
|
|
|
|
|
|
|
// Select the new container.
|
2012-08-23 18:00:43 +00:00
|
|
|
this._selectedContainer = container;
|
2015-05-21 13:41:53 +00:00
|
|
|
if (node) {
|
2012-08-23 18:00:43 +00:00
|
|
|
this._selectedContainer.selected = true;
|
|
|
|
}
|
|
|
|
|
2015-05-21 13:41:53 +00:00
|
|
|
// Change the current selection if needed.
|
|
|
|
if (this._inspector.selection.nodeFront !== node) {
|
|
|
|
this._inspector.selection.setNodeFront(node, reason || "nodeselected");
|
|
|
|
}
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2012-12-20 00:16:45 +00:00
|
|
|
/**
|
|
|
|
* Make sure that every ancestor of the selection are updated
|
|
|
|
* and included in the list of visible children.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_ensureVisible: function(node) {
|
2012-12-20 00:16:45 +00:00
|
|
|
while (node) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let container = this.getContainer(node);
|
2013-06-11 04:18:46 +00:00
|
|
|
let parent = node.parentNode();
|
2012-12-20 00:16:45 +00:00
|
|
|
if (!container.elt.parentNode) {
|
2014-06-23 15:20:01 +00:00
|
|
|
let parentContainer = this.getContainer(parent);
|
2014-03-22 08:02:14 +00:00
|
|
|
if (parentContainer) {
|
|
|
|
parentContainer.childrenDirty = true;
|
2014-09-29 07:29:00 +00:00
|
|
|
this._updateChildren(parentContainer, {expand: true});
|
2014-03-22 08:02:14 +00:00
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
node = parent;
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
return this._waitForChildren();
|
2012-12-20 00:16:45 +00:00
|
|
|
},
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
/**
|
|
|
|
* Unmark selected node (no node selected).
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
unmarkSelectedNode: function() {
|
2012-11-30 08:07:59 +00:00
|
|
|
if (this._selectedContainer) {
|
|
|
|
this._selectedContainer.selected = false;
|
|
|
|
this._selectedContainer = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
/**
|
|
|
|
* Check if the current selection is a descendent of the container.
|
|
|
|
* if so, make sure it's among the visible set for the container,
|
|
|
|
* and set the dirty flag if needed.
|
|
|
|
* @returns The node that should be made visible, if any.
|
|
|
|
*/
|
|
|
|
_checkSelectionVisible: function(aContainer) {
|
|
|
|
let centered = null;
|
|
|
|
let node = this._inspector.selection.nodeFront;
|
|
|
|
while (node) {
|
|
|
|
if (node.parentNode() === aContainer.node) {
|
|
|
|
centered = node;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
node = node.parentNode();
|
|
|
|
}
|
|
|
|
|
|
|
|
return centered;
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Make sure all children of the given container's node are
|
|
|
|
* imported and attached to the container in the right order.
|
2013-06-11 04:18:46 +00:00
|
|
|
*
|
|
|
|
* Children need to be updated only in the following circumstances:
|
|
|
|
* a) We just imported this node and have never seen its children.
|
|
|
|
* container.childrenDirty will be set by importNode in this case.
|
|
|
|
* b) We received a childList mutation on the node.
|
|
|
|
* container.childrenDirty will be set in that case too.
|
|
|
|
* c) We have changed the selection, and the path to that selection
|
|
|
|
* wasn't loaded in a previous children request (because we only
|
|
|
|
* grab a subset).
|
|
|
|
* container.childrenDirty should be set in that case too!
|
|
|
|
*
|
2013-09-23 06:46:12 +00:00
|
|
|
* @param MarkupContainer aContainer
|
|
|
|
* The markup container whose children need updating
|
|
|
|
* @param Object options
|
|
|
|
* Options are {expand:boolean,flash:boolean}
|
|
|
|
* @return a promise that will be resolved when the children are ready
|
|
|
|
* (which may be immediately).
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_updateChildren: function(aContainer, options) {
|
2013-09-23 06:46:12 +00:00
|
|
|
let expand = options && options.expand;
|
|
|
|
let flash = options && options.flash;
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
aContainer.hasChildren = aContainer.node.hasChildren;
|
|
|
|
|
|
|
|
if (!this._queuedChildUpdates) {
|
|
|
|
this._queuedChildUpdates = new Map();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._queuedChildUpdates.has(aContainer)) {
|
|
|
|
return this._queuedChildUpdates.get(aContainer);
|
|
|
|
}
|
|
|
|
|
2012-12-20 00:16:45 +00:00
|
|
|
if (!aContainer.childrenDirty) {
|
2013-06-11 04:18:46 +00:00
|
|
|
return promise.resolve(aContainer);
|
2012-12-20 00:16:45 +00:00
|
|
|
}
|
|
|
|
|
2015-05-13 21:55:09 +00:00
|
|
|
if (aContainer.singleTextChild
|
|
|
|
&& aContainer.singleTextChild != aContainer.node.singleTextChild) {
|
|
|
|
|
|
|
|
// This container was doing double duty as a container for a single
|
|
|
|
// text child, back that out.
|
|
|
|
this._containers.delete(aContainer.singleTextChild);
|
|
|
|
aContainer.clearSingleTextChild();
|
|
|
|
|
|
|
|
if (aContainer.hasChildren && aContainer.selected) {
|
|
|
|
aContainer.setExpanded(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aContainer.node.singleTextChild) {
|
|
|
|
aContainer.setExpanded(false);
|
|
|
|
// this container will do double duty as the container for the single
|
|
|
|
// text child.
|
|
|
|
while (aContainer.children.firstChild) {
|
|
|
|
aContainer.children.removeChild(aContainer.children.firstChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
aContainer.setSingleTextChild(aContainer.node.singleTextChild);
|
|
|
|
|
|
|
|
this._containers.set(aContainer.node.singleTextChild, aContainer);
|
|
|
|
aContainer.childrenDirty = false;
|
|
|
|
return promise.resolve(aContainer);
|
|
|
|
}
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
if (!aContainer.hasChildren) {
|
|
|
|
while (aContainer.children.firstChild) {
|
|
|
|
aContainer.children.removeChild(aContainer.children.firstChild);
|
|
|
|
}
|
|
|
|
aContainer.childrenDirty = false;
|
2015-05-13 21:55:09 +00:00
|
|
|
aContainer.setExpanded(false);
|
2013-06-11 04:18:46 +00:00
|
|
|
return promise.resolve(aContainer);
|
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
// If we're not expanded (or asked to update anyway), we're done for
|
|
|
|
// now. Note that this will leave the childrenDirty flag set, so when
|
|
|
|
// expanded we'll refresh the child list.
|
2013-09-23 06:46:12 +00:00
|
|
|
if (!(aContainer.expanded || expand)) {
|
2013-06-11 04:18:46 +00:00
|
|
|
return promise.resolve(aContainer);
|
2012-12-20 00:16:45 +00:00
|
|
|
}
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
// We're going to issue a children request, make sure it includes the
|
|
|
|
// centered node.
|
|
|
|
let centered = this._checkSelectionVisible(aContainer);
|
|
|
|
|
|
|
|
// Children aren't updated yet, but clear the childrenDirty flag anyway.
|
|
|
|
// If the dirty flag is re-set while we're fetching we'll need to fetch
|
|
|
|
// again.
|
2012-12-20 00:16:45 +00:00
|
|
|
aContainer.childrenDirty = false;
|
2013-06-11 04:18:46 +00:00
|
|
|
let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => {
|
2013-06-17 13:52:55 +00:00
|
|
|
if (!this._containers) {
|
|
|
|
return promise.reject("markup view destroyed");
|
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
this._queuedChildUpdates.delete(aContainer);
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
// If children are dirty, we got a change notification for this node
|
|
|
|
// while the request was in progress, we need to do it again.
|
|
|
|
if (aContainer.childrenDirty) {
|
2013-09-23 06:46:12 +00:00
|
|
|
return this._updateChildren(aContainer, {expand: centered});
|
2013-06-11 04:18:46 +00:00
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
let fragment = this.doc.createDocumentFragment();
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
for (let child of children.nodes) {
|
2013-09-23 06:46:12 +00:00
|
|
|
let container = this.importNode(child, flash);
|
2013-06-11 04:18:46 +00:00
|
|
|
fragment.appendChild(container.elt);
|
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
while (aContainer.children.firstChild) {
|
|
|
|
aContainer.children.removeChild(aContainer.children.firstChild);
|
2012-12-20 08:37:22 +00:00
|
|
|
}
|
2013-06-11 04:18:46 +00:00
|
|
|
|
|
|
|
if (!(children.hasFirst && children.hasLast)) {
|
|
|
|
let data = {
|
|
|
|
showing: this.strings.GetStringFromName("markupView.more.showing"),
|
|
|
|
showAll: this.strings.formatStringFromName(
|
|
|
|
"markupView.more.showAll",
|
|
|
|
[aContainer.node.numChildren.toString()], 1),
|
|
|
|
allButtonClick: () => {
|
|
|
|
aContainer.maxChildren = -1;
|
|
|
|
aContainer.childrenDirty = true;
|
|
|
|
this._updateChildren(aContainer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
}
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
aContainer.children.appendChild(fragment);
|
|
|
|
return aContainer;
|
|
|
|
}).then(null, console.error);
|
|
|
|
this._queuedChildUpdates.set(aContainer, updatePromise);
|
|
|
|
return updatePromise;
|
|
|
|
},
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
_waitForChildren: function() {
|
|
|
|
if (!this._queuedChildUpdates) {
|
|
|
|
return promise.resolve(undefined);
|
|
|
|
}
|
2015-05-22 18:50:01 +00:00
|
|
|
|
|
|
|
return promise.all([...this._queuedChildUpdates.values()]);
|
2012-12-20 00:16:45 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a list of the children to display for this container.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_getVisibleChildren: function(aContainer, aCentered) {
|
2012-12-20 00:16:45 +00:00
|
|
|
let maxChildren = aContainer.maxChildren || this.maxChildren;
|
|
|
|
if (maxChildren == -1) {
|
2013-06-11 04:18:46 +00:00
|
|
|
maxChildren = undefined;
|
2012-12-20 00:16:45 +00:00
|
|
|
}
|
2012-12-20 00:16:45 +00:00
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
return this.walker.children(aContainer.node, {
|
|
|
|
maxNodes: maxChildren,
|
|
|
|
center: aCentered
|
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tear down the markup panel.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
destroy: function() {
|
2014-03-17 18:11:00 +00:00
|
|
|
if (this._destroyer) {
|
|
|
|
return this._destroyer;
|
|
|
|
}
|
|
|
|
|
2015-01-26 13:38:00 +00:00
|
|
|
this._destroyer = promise.resolve();
|
|
|
|
|
2015-03-25 16:19:00 +00:00
|
|
|
this._clearBriefBoxModelTimer();
|
2014-01-09 11:36:01 +00:00
|
|
|
|
|
|
|
this._hoveredNode = null;
|
|
|
|
|
2013-10-24 13:41:03 +00:00
|
|
|
this.htmlEditor.destroy();
|
2014-01-09 11:36:01 +00:00
|
|
|
this.htmlEditor = null;
|
2013-10-24 13:41:03 +00:00
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
this.undo.destroy();
|
2014-01-09 11:36:01 +00:00
|
|
|
this.undo = null;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-08-02 10:35:50 +00:00
|
|
|
this.popup.destroy();
|
2014-01-09 11:36:01 +00:00
|
|
|
this.popup = null;
|
2013-08-02 10:35:50 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
this._elt.removeEventListener("click", this._onMouseClick, false);
|
2014-01-09 11:36:01 +00:00
|
|
|
this._elt.removeEventListener("mousemove", this._onMouseMove, false);
|
2015-07-02 03:18:09 +00:00
|
|
|
this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
|
2015-10-27 09:55:00 +00:00
|
|
|
this.doc.body.removeEventListener("mouseup", this._onMouseUp);
|
|
|
|
this.win.removeEventListener("keydown", this._onKeyDown, false);
|
|
|
|
this.win.removeEventListener("copy", this._onCopy);
|
|
|
|
this._frame.removeEventListener("focus", this._onFocus, false);
|
|
|
|
this.walker.off("mutations", this._mutationObserver);
|
|
|
|
this.walker.off("display-change", this._onDisplayChange);
|
|
|
|
this._inspector.selection.off("new-node-front", this._onNewSelection);
|
|
|
|
this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
|
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
this._elt = null;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
for (let [, container] of this._containers) {
|
2013-10-11 15:50:33 +00:00
|
|
|
container.destroy();
|
|
|
|
}
|
2014-01-09 11:36:01 +00:00
|
|
|
this._containers = null;
|
2013-11-05 16:19:29 +00:00
|
|
|
|
|
|
|
this.tooltip.destroy();
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tooltip = null;
|
2014-03-17 18:11:00 +00:00
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
this.win = null;
|
|
|
|
this.doc = null;
|
|
|
|
|
|
|
|
this._lastDropTarget = null;
|
|
|
|
this._lastDragTarget = null;
|
|
|
|
|
2014-03-17 18:11:00 +00:00
|
|
|
return this._destroyer;
|
2012-09-25 16:33:46 +00:00
|
|
|
},
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
/**
|
|
|
|
* Find the closest element with class tag-line. These are used to indicate
|
|
|
|
* drag and drop targets.
|
|
|
|
* @param {DOMNode} el
|
|
|
|
* @return {DOMNode}
|
|
|
|
*/
|
|
|
|
findClosestDragDropTarget: function(el) {
|
|
|
|
return el.classList.contains("tag-line")
|
|
|
|
? el
|
|
|
|
: el.querySelector(".tag-line") || el.closest(".tag-line");
|
|
|
|
},
|
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
/**
|
|
|
|
* Takes an element as it's only argument and marks the element
|
|
|
|
* as the drop target
|
|
|
|
*/
|
|
|
|
indicateDropTarget: function(el) {
|
|
|
|
if (this._lastDropTarget) {
|
|
|
|
this._lastDropTarget.classList.remove("drop-target");
|
|
|
|
}
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
let target = this.findClosestDragDropTarget(el);
|
|
|
|
if (target) {
|
|
|
|
target.classList.add("drop-target");
|
|
|
|
this._lastDropTarget = target;
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Takes an element to mark it as indicator of dragging target's initial place
|
|
|
|
*/
|
|
|
|
indicateDragTarget: function(el) {
|
|
|
|
if (this._lastDragTarget) {
|
|
|
|
this._lastDragTarget.classList.remove("drag-target");
|
|
|
|
}
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
let target = this.findClosestDragDropTarget(el);
|
|
|
|
if (target) {
|
|
|
|
target.classList.add("drag-target");
|
|
|
|
this._lastDragTarget = target;
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-10-27 09:55:00 +00:00
|
|
|
* Used to get the nodes required to modify the markup after dragging the
|
|
|
|
* element (parent/nextSibling).
|
2014-12-26 10:53:00 +00:00
|
|
|
*/
|
|
|
|
get dropTargetNodes() {
|
|
|
|
let target = this._lastDropTarget;
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let parent, nextSibling;
|
|
|
|
|
|
|
|
if (this._lastDropTarget.previousElementSibling &&
|
|
|
|
this._lastDropTarget.previousElementSibling.nodeName.toLowerCase() === "ul") {
|
|
|
|
parent = target.parentNode.container.node;
|
|
|
|
nextSibling = null;
|
|
|
|
} else {
|
|
|
|
parent = target.parentNode.container.node.parentNode();
|
|
|
|
nextSibling = target.parentNode.container.node;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nextSibling && nextSibling.isBeforePseudoElement) {
|
|
|
|
nextSibling = target.parentNode.parentNode.children[1].container.node;
|
|
|
|
}
|
|
|
|
if (nextSibling && nextSibling.isAfterPseudoElement) {
|
|
|
|
parent = target.parentNode.container.node.parentNode();
|
|
|
|
nextSibling = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parent.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {parent, nextSibling};
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2013-09-23 06:46:12 +00:00
|
|
|
*
|
2014-09-29 07:29:00 +00:00
|
|
|
* This should not be instantiated directly, instead use one of:
|
|
|
|
* MarkupReadOnlyContainer
|
|
|
|
* MarkupTextContainer
|
|
|
|
* MarkupElementContainer
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2014-09-29 07:29:00 +00:00
|
|
|
function MarkupContainer() { }
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
MarkupContainer.prototype = {
|
|
|
|
/*
|
|
|
|
* Initialize the MarkupContainer. Should be called while one
|
|
|
|
* of the other contain classes is instantiated.
|
|
|
|
*
|
|
|
|
* @param MarkupView markupView
|
|
|
|
* The markup view that owns this container.
|
|
|
|
* @param NodeFront node
|
|
|
|
* The node to display.
|
|
|
|
* @param string templateID
|
|
|
|
* Which template to render for this container
|
|
|
|
*/
|
|
|
|
initialize: function(markupView, node, templateID) {
|
|
|
|
this.markup = markupView;
|
|
|
|
this.node = node;
|
|
|
|
this.undo = this.markup.undo;
|
2014-12-26 10:53:00 +00:00
|
|
|
this.win = this.markup._frame.contentWindow;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
// The template will fill the following properties
|
|
|
|
this.elt = null;
|
|
|
|
this.expander = null;
|
|
|
|
this.tagState = null;
|
|
|
|
this.tagLine = null;
|
|
|
|
this.children = null;
|
|
|
|
this.markup.template(templateID, this);
|
|
|
|
this.elt.container = this;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
this._onMouseDown = this._onMouseDown.bind(this);
|
|
|
|
this._onToggle = this._onToggle.bind(this);
|
2014-12-26 10:53:00 +00:00
|
|
|
this._onMouseUp = this._onMouseUp.bind(this);
|
|
|
|
this._onMouseMove = this._onMouseMove.bind(this);
|
2013-10-25 19:21:01 +00:00
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
// Binding event listeners
|
|
|
|
this.elt.addEventListener("mousedown", this._onMouseDown, false);
|
|
|
|
this.markup.doc.body.addEventListener("mouseup", this._onMouseUp, true);
|
|
|
|
this.markup.doc.body.addEventListener("mousemove", this._onMouseMove, true);
|
2014-09-29 07:29:00 +00:00
|
|
|
this.elt.addEventListener("dblclick", this._onToggle, false);
|
|
|
|
if (this.expander) {
|
|
|
|
this.expander.addEventListener("click", this._onToggle, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Marking the node as shown or hidden
|
|
|
|
this.isDisplayed = this.node.isDisplayed;
|
|
|
|
},
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-09-12 14:48:13 +00:00
|
|
|
toString: function() {
|
|
|
|
return "[MarkupContainer for " + this.node + "]";
|
|
|
|
},
|
2013-06-11 04:18:46 +00:00
|
|
|
|
2014-01-30 16:33:53 +00:00
|
|
|
isPreviewable: function() {
|
2014-09-29 07:29:00 +00:00
|
|
|
if (this.node.tagName && !this.node.isPseudoElement) {
|
2013-10-25 19:21:01 +00:00
|
|
|
let tagName = this.node.tagName.toLowerCase();
|
2013-11-05 16:19:29 +00:00
|
|
|
let srcAttr = this.editor.getAttributeElement("src");
|
|
|
|
let isImage = tagName === "img" && srcAttr;
|
|
|
|
let isCanvas = tagName === "canvas";
|
2013-10-25 19:21:01 +00:00
|
|
|
|
2014-01-30 16:33:53 +00:00
|
|
|
return isImage || isCanvas;
|
|
|
|
}
|
2015-10-27 09:55:00 +00:00
|
|
|
|
|
|
|
return false;
|
2014-01-30 16:33:53 +00:00
|
|
|
},
|
|
|
|
|
2014-06-05 12:50:03 +00:00
|
|
|
/**
|
|
|
|
* Show the element has displayed or not
|
|
|
|
*/
|
|
|
|
set isDisplayed(isDisplayed) {
|
|
|
|
this.elt.classList.remove("not-displayed");
|
|
|
|
if (!isDisplayed) {
|
|
|
|
this.elt.classList.add("not-displayed");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2015-05-13 21:55:09 +00:00
|
|
|
this.updateExpander();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* True if the current node can be expanded.
|
|
|
|
*/
|
|
|
|
get canExpand() {
|
|
|
|
return this._hasChildren && !this.node.singleTextChild;
|
|
|
|
},
|
|
|
|
|
2015-09-01 18:45:53 +00:00
|
|
|
/**
|
|
|
|
* True if this is the root <html> element and can't be collapsed
|
|
|
|
*/
|
|
|
|
get mustExpand() {
|
|
|
|
return this.node._parent === this.markup.walker.rootNode;
|
|
|
|
},
|
|
|
|
|
2015-05-13 21:55:09 +00:00
|
|
|
updateExpander: function() {
|
2014-09-29 07:29:00 +00:00
|
|
|
if (!this.expander) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-09-01 18:45:53 +00:00
|
|
|
if (this.canExpand && !this.mustExpand) {
|
2012-08-23 18:00:43 +00:00
|
|
|
this.expander.style.visibility = "visible";
|
|
|
|
} else {
|
|
|
|
this.expander.style.visibility = "hidden";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-10-08 22:46:16 +00:00
|
|
|
/**
|
|
|
|
* If the node has children, return the list of containers for all these
|
|
|
|
* children.
|
|
|
|
*/
|
|
|
|
getChildContainers: function() {
|
|
|
|
if (!this.hasChildren) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return [...this.children.children].map(node => node.container);
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* True if the node has been visually expanded in the tree.
|
|
|
|
*/
|
|
|
|
get expanded() {
|
2013-09-12 14:48:13 +00:00
|
|
|
return !this.elt.classList.contains("collapsed");
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
2015-05-13 21:55:09 +00:00
|
|
|
setExpanded: function(aValue) {
|
2014-09-29 07:29:00 +00:00
|
|
|
if (!this.expander) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-05-13 21:55:09 +00:00
|
|
|
if (!this.canExpand) {
|
|
|
|
aValue = false;
|
|
|
|
}
|
2015-09-01 18:45:53 +00:00
|
|
|
if (this.mustExpand) {
|
|
|
|
aValue = true;
|
|
|
|
}
|
2015-05-13 21:55:09 +00:00
|
|
|
|
2013-09-12 14:48:13 +00:00
|
|
|
if (aValue && this.elt.classList.contains("collapsed")) {
|
|
|
|
// Expanding a node means cloning its "inline" closing tag into a new
|
|
|
|
// tag-line that the user can interact with and showing the children.
|
2014-09-29 07:29:00 +00:00
|
|
|
let closingTag = this.elt.querySelector(".close");
|
|
|
|
if (closingTag) {
|
|
|
|
if (!this.closeTagLine) {
|
|
|
|
let line = this.markup.doc.createElement("div");
|
|
|
|
line.classList.add("tag-line");
|
2013-09-12 14:48:13 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
let tagState = this.markup.doc.createElement("div");
|
|
|
|
tagState.classList.add("tag-state");
|
|
|
|
line.appendChild(tagState);
|
2013-09-12 14:48:13 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
line.appendChild(closingTag.cloneNode(true));
|
2013-09-12 14:48:13 +00:00
|
|
|
|
2015-10-13 14:19:26 +00:00
|
|
|
flashElementOff(line);
|
2014-09-29 07:29:00 +00:00
|
|
|
this.closeTagLine = line;
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
2014-09-29 07:29:00 +00:00
|
|
|
this.elt.appendChild(this.closeTagLine);
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
2014-09-29 07:29:00 +00:00
|
|
|
|
2013-09-12 14:48:13 +00:00
|
|
|
this.elt.classList.remove("collapsed");
|
2013-03-27 22:20:38 +00:00
|
|
|
this.expander.setAttribute("open", "");
|
2014-01-09 11:36:01 +00:00
|
|
|
this.hovered = false;
|
2013-09-12 14:48:13 +00:00
|
|
|
} else if (!aValue) {
|
2014-09-29 07:29:00 +00:00
|
|
|
if (this.closeTagLine) {
|
2013-09-12 14:48:13 +00:00
|
|
|
this.elt.removeChild(this.closeTagLine);
|
2015-05-13 21:55:09 +00:00
|
|
|
this.closeTagLine = undefined;
|
2013-01-23 17:01:55 +00:00
|
|
|
}
|
2013-09-12 14:48:13 +00:00
|
|
|
this.elt.classList.add("collapsed");
|
2013-03-27 22:20:38 +00:00
|
|
|
this.expander.removeAttribute("open");
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
parentContainer: function() {
|
|
|
|
return this.elt.parentNode ? this.elt.parentNode.container : null;
|
2013-09-12 14:48:13 +00:00
|
|
|
},
|
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
_isDragging: false,
|
|
|
|
_dragStartY: 0,
|
|
|
|
|
|
|
|
set isDragging(isDragging) {
|
|
|
|
this._isDragging = isDragging;
|
|
|
|
this.markup.isDragging = isDragging;
|
|
|
|
|
|
|
|
if (isDragging) {
|
|
|
|
this.elt.classList.add("dragging");
|
|
|
|
this.markup.doc.body.classList.add("dragging");
|
|
|
|
} else {
|
|
|
|
this.elt.classList.remove("dragging");
|
|
|
|
this.markup.doc.body.classList.remove("dragging");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
get isDragging() {
|
|
|
|
return this._isDragging;
|
|
|
|
},
|
|
|
|
|
2015-08-26 14:30:00 +00:00
|
|
|
/**
|
|
|
|
* Check if element is draggable
|
|
|
|
*/
|
2015-10-27 09:55:00 +00:00
|
|
|
isDraggable: function() {
|
|
|
|
let tagName = this.node.tagName.toLowerCase();
|
|
|
|
|
|
|
|
return !this.node.isPseudoElement &&
|
2015-08-26 14:30:00 +00:00
|
|
|
!this.node.isAnonymous &&
|
2015-10-27 09:55:00 +00:00
|
|
|
!this.node.isDocumentElement &&
|
|
|
|
tagName !== "body" &&
|
|
|
|
tagName !== "head" &&
|
2015-08-26 14:30:00 +00:00
|
|
|
this.win.getSelection().isCollapsed &&
|
|
|
|
this.node.parentNode().tagName !== null;
|
|
|
|
},
|
|
|
|
|
2013-09-12 14:48:13 +00:00
|
|
|
_onMouseDown: function(event) {
|
2015-10-27 09:55:00 +00:00
|
|
|
let {target, button, metaKey, ctrlKey} = event;
|
|
|
|
let isLeftClick = button === 0;
|
|
|
|
let isMiddleClick = button === 1;
|
|
|
|
let isMetaClick = isLeftClick && (metaKey || ctrlKey);
|
2013-11-11 15:17:41 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// The "show more nodes" button already has its onclick, so early return.
|
2014-12-26 10:53:00 +00:00
|
|
|
if (target.nodeName === "button") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// target is the MarkupContainer itself.
|
|
|
|
this.hovered = false;
|
|
|
|
this.markup.navigate(this);
|
|
|
|
event.stopPropagation();
|
2015-04-29 15:05:37 +00:00
|
|
|
|
|
|
|
// Preventing the default behavior will avoid the body to gain focus on
|
|
|
|
// mouseup (through bubbling) when clicking on a non focusable node in the
|
|
|
|
// line. So, if the click happened outside of a focusable element, do
|
|
|
|
// prevent the default behavior, so that the tagname or textcontent gains
|
|
|
|
// focus.
|
2015-05-13 21:55:09 +00:00
|
|
|
if (!target.closest(".editor [tabindex]")) {
|
2015-04-29 15:05:37 +00:00
|
|
|
event.preventDefault();
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// Follow attribute links if middle or meta click.
|
2015-07-07 11:40:00 +00:00
|
|
|
if (isMiddleClick || isMetaClick) {
|
|
|
|
let link = target.dataset.link;
|
|
|
|
let type = target.dataset.type;
|
|
|
|
this.markup._inspector.followAttributeLink(type, link);
|
2015-07-29 21:39:59 +00:00
|
|
|
return;
|
2015-07-07 11:40:00 +00:00
|
|
|
}
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// Start node drag & drop (if the mouse moved, see _onMouseMove).
|
|
|
|
if (isLeftClick && this.isDraggable()) {
|
|
|
|
this._isPreDragging = true;
|
2014-12-26 10:53:00 +00:00
|
|
|
this._dragStartY = event.pageY;
|
2015-10-27 09:55:00 +00:00
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On mouse up, stop dragging.
|
|
|
|
*/
|
2015-06-03 22:56:00 +00:00
|
|
|
_onMouseUp: Task.async(function*() {
|
2015-10-27 09:55:00 +00:00
|
|
|
this._isPreDragging = false;
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
if (this.isDragging) {
|
|
|
|
this.cancelDragging();
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
let dropTargetNodes = this.markup.dropTargetNodes;
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
if (!dropTargetNodes) {
|
|
|
|
return;
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
|
|
|
|
dropTargetNodes.nextSibling);
|
|
|
|
this.markup.emit("drop-completed");
|
2014-12-26 10:53:00 +00:00
|
|
|
}
|
2015-06-03 22:56:00 +00:00
|
|
|
}),
|
2014-12-26 10:53:00 +00:00
|
|
|
|
|
|
|
/**
|
2015-10-27 09:55:00 +00:00
|
|
|
* On mouse move, move the dragged element and indicate the drop target.
|
2014-12-26 10:53:00 +00:00
|
|
|
*/
|
|
|
|
_onMouseMove: function(event) {
|
2015-10-27 09:55:00 +00:00
|
|
|
// If this is the first move after mousedown, only start dragging after the
|
|
|
|
// mouse has travelled a few pixels and then indicate the start position.
|
|
|
|
let initialDiff = Math.abs(event.pageY - this._dragStartY);
|
|
|
|
if (this._isPreDragging && initialDiff >= DRAG_DROP_MIN_INITIAL_DISTANCE) {
|
|
|
|
this._isPreDragging = false;
|
|
|
|
this.isDragging = true;
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
// If this is the last child, use the closing <div.tag-line> of parent as
|
|
|
|
// indicator.
|
|
|
|
let position = this.elt.nextElementSibling ||
|
|
|
|
this.markup.getContainer(this.node.parentNode())
|
|
|
|
.closeTagLine;
|
|
|
|
this.markup.indicateDragTarget(position);
|
|
|
|
}
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
if (this.isDragging) {
|
|
|
|
let diff = event.pageY - this._dragStartY;
|
|
|
|
this.elt.style.top = diff + "px";
|
2014-12-26 10:53:00 +00:00
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
|
|
|
|
event.pageY - this.win.scrollY);
|
|
|
|
this.markup.indicateDropTarget(el);
|
|
|
|
}
|
2013-09-12 14:48:13 +00:00
|
|
|
},
|
|
|
|
|
2015-08-26 14:30:00 +00:00
|
|
|
cancelDragging: function() {
|
|
|
|
if (!this.isDragging) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-27 09:55:00 +00:00
|
|
|
this._isPreDragging = false;
|
2015-08-26 14:30:00 +00:00
|
|
|
this.isDragging = false;
|
|
|
|
this.elt.style.removeProperty("top");
|
|
|
|
},
|
|
|
|
|
2013-09-23 06:46:12 +00:00
|
|
|
/**
|
|
|
|
* Temporarily flash the container to attract attention.
|
|
|
|
* Used for markup mutations.
|
|
|
|
*/
|
|
|
|
flashMutation: function() {
|
|
|
|
if (!this.selected) {
|
2014-12-26 10:53:00 +00:00
|
|
|
let contentWin = this.win;
|
2015-03-24 21:57:57 +00:00
|
|
|
flashElementOn(this.tagState, this.editor.elt);
|
2013-09-23 06:46:12 +00:00
|
|
|
if (this._flashMutationTimer) {
|
2015-03-24 21:57:54 +00:00
|
|
|
clearTimeout(this._flashMutationTimer);
|
2013-09-23 06:46:12 +00:00
|
|
|
this._flashMutationTimer = null;
|
|
|
|
}
|
2015-03-24 21:57:54 +00:00
|
|
|
this._flashMutationTimer = setTimeout(() => {
|
2015-03-24 21:57:57 +00:00
|
|
|
flashElementOff(this.tagState, this.editor.elt);
|
2014-08-11 08:43:00 +00:00
|
|
|
}, this.markup.CONTAINER_FLASHING_DURATION);
|
2013-09-23 06:46:12 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-01-09 11:36:01 +00:00
|
|
|
_hovered: false,
|
2013-09-12 14:48:13 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Highlight the currently hovered tag + its closing tag if necessary
|
|
|
|
* (that is if the tag is expanded)
|
|
|
|
*/
|
2014-01-09 11:36:01 +00:00
|
|
|
set hovered(aValue) {
|
|
|
|
this.tagState.classList.remove("flash-out");
|
|
|
|
this._hovered = aValue;
|
2013-09-12 14:48:13 +00:00
|
|
|
if (aValue) {
|
|
|
|
if (!this.selected) {
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tagState.classList.add("theme-bg-darker");
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
|
|
|
if (this.closeTagLine) {
|
2014-01-09 11:36:01 +00:00
|
|
|
this.closeTagLine.querySelector(".tag-state").classList.add(
|
2013-10-02 00:14:00 +00:00
|
|
|
"theme-bg-darker");
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
|
|
|
} else {
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tagState.classList.remove("theme-bg-darker");
|
2013-09-12 14:48:13 +00:00
|
|
|
if (this.closeTagLine) {
|
2014-01-09 11:36:01 +00:00
|
|
|
this.closeTagLine.querySelector(".tag-state").classList.remove(
|
2013-10-02 00:14:00 +00:00
|
|
|
"theme-bg-darker");
|
2013-01-23 17:01:55 +00:00
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* True if the container is visible in the markup tree.
|
|
|
|
*/
|
2013-09-12 14:48:13 +00:00
|
|
|
get visible() {
|
2012-08-23 18:00:43 +00:00
|
|
|
return this.elt.getBoundingClientRect().height > 0;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* True if the container is currently selected.
|
|
|
|
*/
|
|
|
|
_selected: false,
|
|
|
|
|
|
|
|
get selected() {
|
|
|
|
return this._selected;
|
|
|
|
},
|
|
|
|
|
|
|
|
set selected(aValue) {
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tagState.classList.remove("flash-out");
|
2012-08-23 18:00:43 +00:00
|
|
|
this._selected = aValue;
|
2013-06-11 04:18:46 +00:00
|
|
|
this.editor.selected = aValue;
|
2012-08-23 18:00:43 +00:00
|
|
|
if (this._selected) {
|
2013-09-12 14:48:13 +00:00
|
|
|
this.tagLine.setAttribute("selected", "");
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tagState.classList.add("theme-selected");
|
2012-08-23 18:00:43 +00:00
|
|
|
} else {
|
2013-09-12 14:48:13 +00:00
|
|
|
this.tagLine.removeAttribute("selected");
|
2014-01-09 11:36:01 +00:00
|
|
|
this.tagState.classList.remove("theme-selected");
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the container's editor to the current state of the
|
|
|
|
* viewed node.
|
|
|
|
*/
|
2013-10-08 12:53:19 +00:00
|
|
|
update: function() {
|
2015-09-01 18:45:53 +00:00
|
|
|
if (this.node.pseudoClassLocks.length) {
|
|
|
|
this.elt.classList.add("pseudoclass-locked");
|
|
|
|
} else {
|
|
|
|
this.elt.classList.remove("pseudoclass-locked");
|
|
|
|
}
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
if (this.editor.update) {
|
2013-10-08 12:53:19 +00:00
|
|
|
this.editor.update();
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Try to put keyboard focus on the current editor.
|
|
|
|
*/
|
2013-09-12 14:48:13 +00:00
|
|
|
focus: function() {
|
2012-08-23 18:00:43 +00:00
|
|
|
let focusable = this.editor.elt.querySelector("[tabindex]");
|
|
|
|
if (focusable) {
|
|
|
|
focusable.focus();
|
|
|
|
}
|
2013-10-11 15:50:33 +00:00
|
|
|
},
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
_onToggle: function(event) {
|
|
|
|
this.markup.navigate(this);
|
|
|
|
if (this.hasChildren) {
|
|
|
|
this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
|
|
|
|
}
|
|
|
|
event.stopPropagation();
|
|
|
|
},
|
|
|
|
|
2013-10-11 15:50:33 +00:00
|
|
|
/**
|
|
|
|
* Get rid of event listeners and references, when the container is no longer
|
|
|
|
* needed
|
|
|
|
*/
|
|
|
|
destroy: function() {
|
2014-09-29 07:29:00 +00:00
|
|
|
// Remove event listeners
|
|
|
|
this.elt.removeEventListener("mousedown", this._onMouseDown, false);
|
|
|
|
this.elt.removeEventListener("dblclick", this._onToggle, false);
|
2014-12-26 10:53:00 +00:00
|
|
|
this.markup.doc.body.removeEventListener("mouseup", this._onMouseUp, true);
|
|
|
|
this.markup.doc.body.removeEventListener("mousemove", this._onMouseMove, true);
|
|
|
|
|
|
|
|
this.win = null;
|
2014-09-29 07:29:00 +00:00
|
|
|
|
|
|
|
if (this.expander) {
|
|
|
|
this.expander.removeEventListener("click", this._onToggle, false);
|
|
|
|
}
|
|
|
|
|
2013-10-11 15:50:33 +00:00
|
|
|
// Recursively destroy children containers
|
|
|
|
let firstChild;
|
|
|
|
while (firstChild = this.children.firstChild) {
|
2014-01-13 15:32:19 +00:00
|
|
|
// Not all children of a container are containers themselves
|
|
|
|
// ("show more nodes" button is one example)
|
|
|
|
if (firstChild.container) {
|
|
|
|
firstChild.container.destroy();
|
|
|
|
}
|
2013-10-11 15:50:33 +00:00
|
|
|
this.children.removeChild(firstChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.editor.destroy();
|
2013-09-12 14:48:13 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
/**
|
|
|
|
* An implementation of MarkupContainer for Pseudo Elements,
|
|
|
|
* Doctype nodes, or any other type generic node that doesn't
|
|
|
|
* fit for other editors.
|
|
|
|
* Does not allow any editing, just viewing / selecting.
|
|
|
|
*
|
|
|
|
* @param MarkupView markupView
|
|
|
|
* The markup view that owns this container.
|
|
|
|
* @param NodeFront node
|
|
|
|
* The node to display.
|
|
|
|
*/
|
|
|
|
function MarkupReadOnlyContainer(markupView, node) {
|
|
|
|
MarkupContainer.prototype.initialize.call(this, markupView, node, "readonlycontainer");
|
|
|
|
|
|
|
|
this.editor = new GenericEditor(this, node);
|
|
|
|
this.tagLine.appendChild(this.editor.elt);
|
|
|
|
}
|
|
|
|
|
|
|
|
MarkupReadOnlyContainer.prototype = Heritage.extend(MarkupContainer.prototype, {});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An implementation of MarkupContainer for text node and comment nodes.
|
|
|
|
* Allows basic text editing in a textarea.
|
|
|
|
*
|
|
|
|
* @param MarkupView aMarkupView
|
|
|
|
* The markup view that owns this container.
|
|
|
|
* @param NodeFront aNode
|
|
|
|
* The node to display.
|
|
|
|
* @param Inspector aInspector
|
|
|
|
* The inspector tool container the markup-view
|
|
|
|
*/
|
|
|
|
function MarkupTextContainer(markupView, node) {
|
|
|
|
MarkupContainer.prototype.initialize.call(this, markupView, node, "textcontainer");
|
|
|
|
|
|
|
|
if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
|
|
|
|
this.editor = new TextEditor(this, node, "text");
|
|
|
|
} else if (node.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
|
|
|
|
this.editor = new TextEditor(this, node, "comment");
|
|
|
|
} else {
|
|
|
|
throw "Invalid node for MarkupTextContainer";
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tagLine.appendChild(this.editor.elt);
|
|
|
|
}
|
|
|
|
|
|
|
|
MarkupTextContainer.prototype = Heritage.extend(MarkupContainer.prototype, {});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An implementation of MarkupContainer for Elements that can contain
|
|
|
|
* child nodes.
|
|
|
|
* Allows editing of tag name, attributes, expanding / collapsing.
|
|
|
|
*
|
|
|
|
* @param MarkupView markupView
|
|
|
|
* The markup view that owns this container.
|
|
|
|
* @param NodeFront node
|
|
|
|
* The node to display.
|
|
|
|
*/
|
|
|
|
function MarkupElementContainer(markupView, node) {
|
|
|
|
MarkupContainer.prototype.initialize.call(this, markupView, node, "elementcontainer");
|
|
|
|
|
|
|
|
if (node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE) {
|
|
|
|
this.editor = new ElementEditor(this, node);
|
|
|
|
} else {
|
|
|
|
throw "Invalid node for MarkupElementContainer";
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tagLine.appendChild(this.editor.elt);
|
|
|
|
}
|
|
|
|
|
|
|
|
MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
|
|
|
|
|
|
|
|
_buildEventTooltipContent: function(target, tooltip) {
|
|
|
|
if (target.hasAttribute("data-event")) {
|
|
|
|
tooltip.hide(target);
|
|
|
|
|
|
|
|
this.node.getEventListenerInfo().then(listenerInfo => {
|
|
|
|
tooltip.setEventContent({
|
|
|
|
eventListenerInfos: listenerInfo,
|
|
|
|
toolbox: this.markup._inspector.toolbox
|
|
|
|
});
|
|
|
|
|
|
|
|
this.markup._makeTooltipPersistent(true);
|
|
|
|
tooltip.once("hidden", () => {
|
|
|
|
this.markup._makeTooltipPersistent(false);
|
|
|
|
});
|
|
|
|
tooltip.show(target);
|
|
|
|
});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-08-22 03:52:56 +00:00
|
|
|
* Generates the an image preview for this Element. The element must be an
|
|
|
|
* image or canvas (@see isPreviewable).
|
|
|
|
*
|
|
|
|
* @return A Promise that is resolved with an object of form
|
|
|
|
* { data, size: { naturalWidth, naturalHeight, resizeRatio } } where
|
|
|
|
* - data is the data-uri for the image preview.
|
|
|
|
* - size contains information about the original image size and if the
|
|
|
|
* preview has been resized.
|
|
|
|
*
|
|
|
|
* If this element is not previewable or the preview cannot be generated for
|
|
|
|
* some reason, the Promise is rejected.
|
2014-09-29 07:29:00 +00:00
|
|
|
*/
|
2015-08-22 03:52:56 +00:00
|
|
|
_getPreview: function() {
|
|
|
|
if (!this.isPreviewable()) {
|
|
|
|
return promise.reject("_getPreview called on a non-previewable element.");
|
|
|
|
}
|
2014-09-29 07:29:00 +00:00
|
|
|
|
2015-08-22 03:52:56 +00:00
|
|
|
if (this.tooltipDataPromise) {
|
|
|
|
// A preview request is already pending. Re-use that request.
|
|
|
|
return this.tooltipDataPromise;
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
2015-08-22 03:52:56 +00:00
|
|
|
|
|
|
|
let maxDim =
|
|
|
|
Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
|
|
|
|
|
|
|
|
// Fetch the preview from the server.
|
|
|
|
this.tooltipDataPromise = Task.spawn(function*() {
|
|
|
|
let preview = yield this.node.getImageData(maxDim);
|
|
|
|
let data = yield preview.data.string();
|
|
|
|
|
|
|
|
// Clear the pending preview request. We can't reuse the results later as
|
|
|
|
// the preview contents might have changed.
|
|
|
|
this.tooltipDataPromise = null;
|
|
|
|
|
|
|
|
return { data, size: preview.size };
|
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
return this.tooltipDataPromise;
|
2014-09-29 07:29:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executed by MarkupView._isImagePreviewTarget which is itself called when the
|
|
|
|
* mouse hovers over a target in the markup-view.
|
|
|
|
* Checks if the target is indeed something we want to have an image tooltip
|
|
|
|
* preview over and, if so, inserts content into the tooltip.
|
|
|
|
* @return a promise that resolves when the content has been inserted or
|
|
|
|
* rejects if no preview is required. This promise is then used by Tooltip.js
|
|
|
|
* to decide if/when to show the tooltip
|
|
|
|
*/
|
|
|
|
isImagePreviewTarget: function(target, tooltip) {
|
2015-08-22 03:52:56 +00:00
|
|
|
// Is this Element previewable.
|
|
|
|
if (!this.isPreviewable()) {
|
2015-04-21 20:01:40 +00:00
|
|
|
return promise.reject(false);
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
|
|
|
|
2015-08-22 03:52:56 +00:00
|
|
|
// If the Element has an src attribute, the tooltip is shown when hovering
|
|
|
|
// over the src url. If not, the tooltip is shown when hovering over the tag
|
|
|
|
// name.
|
|
|
|
let src = this.editor.getAttributeElement("src");
|
|
|
|
let expectedTarget = src ? src.querySelector(".link") : this.editor.tag;
|
|
|
|
if (target !== expectedTarget) {
|
|
|
|
return promise.reject(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._getPreview().then(({data, size}) => {
|
|
|
|
// The preview is ready.
|
|
|
|
tooltip.setImageContent(data, size);
|
|
|
|
}, () => {
|
|
|
|
// Indicate the failure but show the tooltip anyway.
|
|
|
|
tooltip.setBrokenImageContent();
|
2014-09-29 07:29:00 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
copyImageDataUri: function() {
|
|
|
|
// We need to send again a request to gettooltipData even if one was sent for
|
|
|
|
// the tooltip, because we want the full-size image
|
|
|
|
this.node.getImageData().then(data => {
|
|
|
|
data.data.string().then(str => {
|
2015-05-21 20:49:30 +00:00
|
|
|
clipboardHelper.copyString(str);
|
2014-09-29 07:29:00 +00:00
|
|
|
});
|
|
|
|
});
|
2015-05-13 21:55:09 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
setSingleTextChild: function(singleTextChild) {
|
|
|
|
this.singleTextChild = singleTextChild;
|
|
|
|
this.editor.updateTextEditor();
|
|
|
|
},
|
|
|
|
|
|
|
|
clearSingleTextChild: function() {
|
|
|
|
this.singleTextChild = undefined;
|
|
|
|
this.editor.updateTextEditor();
|
2015-10-20 14:47:02 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger new attribute field for input.
|
|
|
|
*/
|
|
|
|
addAttribute: function() {
|
|
|
|
this.editor.newAttr.editMode();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger attribute field for editing.
|
|
|
|
*/
|
|
|
|
editAttribute: function(attrName) {
|
|
|
|
this.editor.attrElements.get(attrName).editMode();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove attribute from container.
|
|
|
|
* This is an undoable action.
|
|
|
|
*/
|
|
|
|
removeAttribute: function(attrName) {
|
|
|
|
let doMods = this.editor._startModifyingAttributes();
|
|
|
|
let undoMods = this.editor._startModifyingAttributes();
|
|
|
|
this.editor._saveAttribute(attrName, undoMods);
|
|
|
|
doMods.removeAttribute(attrName);
|
|
|
|
this.undo.do(() => {
|
|
|
|
doMods.apply();
|
|
|
|
}, () => {
|
|
|
|
undoMods.apply();
|
|
|
|
});
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Dummy container node used for the root document element.
|
|
|
|
*/
|
2013-09-12 14:48:13 +00:00
|
|
|
function RootContainer(aMarkupView, aNode) {
|
2012-08-23 18:00:43 +00:00
|
|
|
this.doc = aMarkupView.doc;
|
|
|
|
this.elt = this.doc.createElement("ul");
|
2013-06-11 04:18:46 +00:00
|
|
|
this.elt.container = this;
|
2012-08-23 18:00:43 +00:00
|
|
|
this.children = this.elt;
|
|
|
|
this.node = aNode;
|
2013-10-25 19:21:01 +00:00
|
|
|
this.toString = () => "[root container]";
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2013-09-12 14:48:13 +00:00
|
|
|
RootContainer.prototype = {
|
|
|
|
hasChildren: true,
|
|
|
|
expanded: true,
|
2013-10-11 15:50:33 +00:00
|
|
|
update: function() {},
|
2014-10-08 22:46:16 +00:00
|
|
|
destroy: function() {},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the node has children, return the list of containers for all these
|
|
|
|
* children.
|
|
|
|
*/
|
|
|
|
getChildContainers: function() {
|
|
|
|
return [...this.children.children].map(node => node.container);
|
2015-05-13 21:55:09 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
setExpanded: function(aValue) {}
|
2013-09-12 14:48:13 +00:00
|
|
|
};
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
2014-09-29 07:29:00 +00:00
|
|
|
* Creates an editor for non-editable nodes.
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
function GenericEditor(aContainer, aNode) {
|
2014-09-29 07:29:00 +00:00
|
|
|
this.container = aContainer;
|
|
|
|
this.markup = this.container.markup;
|
|
|
|
this.template = this.markup.template.bind(this.markup);
|
|
|
|
this.elt = null;
|
|
|
|
this.template("generic", this);
|
2013-10-11 15:50:33 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
if (aNode.isPseudoElement) {
|
|
|
|
this.tag.classList.add("theme-fg-color5");
|
|
|
|
this.tag.textContent = aNode.isBeforePseudoElement ? "::before" : "::after";
|
|
|
|
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
|
|
|
|
this.elt.classList.add("comment");
|
2015-07-02 20:43:19 +00:00
|
|
|
this.tag.textContent = aNode.doctypeString;
|
2014-09-29 07:29:00 +00:00
|
|
|
} else {
|
|
|
|
this.tag.textContent = aNode.nodeName;
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
GenericEditor.prototype = {
|
|
|
|
destroy: function() {
|
|
|
|
this.elt.remove();
|
2015-10-20 14:47:02 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stub method for consistency with ElementEditor.
|
|
|
|
*/
|
|
|
|
getInfoAtNode: function() {
|
|
|
|
return null;
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
2013-10-11 15:50:33 +00:00
|
|
|
};
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
function TextEditor(aContainer, aNode, aTemplate) {
|
2014-09-29 07:29:00 +00:00
|
|
|
this.container = aContainer;
|
|
|
|
this.markup = this.container.markup;
|
2012-08-23 18:00:43 +00:00
|
|
|
this.node = aNode;
|
2014-09-29 07:29:00 +00:00
|
|
|
this.template = this.markup.template.bind(aTemplate);
|
2013-06-11 04:18:46 +00:00
|
|
|
this._selected = false;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
this.markup.template(aTemplate, this);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-06-17 13:52:55 +00:00
|
|
|
editableField({
|
|
|
|
element: this.value,
|
|
|
|
stopOnReturn: true,
|
|
|
|
trigger: "dblclick",
|
|
|
|
multiline: true,
|
2015-04-30 00:27:19 +00:00
|
|
|
trimOutput: false,
|
2013-06-17 13:52:55 +00:00
|
|
|
done: (aVal, aCommit) => {
|
|
|
|
if (!aCommit) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.node.getNodeValue().then(longstr => {
|
|
|
|
longstr.string().then(oldValue => {
|
|
|
|
longstr.release().then(null, console.error);
|
|
|
|
|
2014-09-29 07:29:00 +00:00
|
|
|
this.container.undo.do(() => {
|
2015-05-11 14:47:24 +00:00
|
|
|
this.node.setNodeValue(aVal);
|
2013-06-17 13:52:55 +00:00
|
|
|
}, () => {
|
2015-05-11 14:47:24 +00:00
|
|
|
this.node.setNodeValue(oldValue);
|
2013-06-17 13:52:55 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
this.update();
|
|
|
|
}
|
|
|
|
|
|
|
|
TextEditor.prototype = {
|
2015-05-22 18:50:01 +00:00
|
|
|
get selected() {
|
|
|
|
return this._selected;
|
|
|
|
},
|
|
|
|
|
2013-06-11 04:18:46 +00:00
|
|
|
set selected(aValue) {
|
|
|
|
if (aValue === this._selected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._selected = aValue;
|
|
|
|
this.update();
|
|
|
|
},
|
|
|
|
|
2013-10-02 00:14:00 +00:00
|
|
|
update: function() {
|
2013-06-11 04:18:46 +00:00
|
|
|
if (!this.selected || !this.node.incompleteValue) {
|
|
|
|
let text = this.node.shortValue;
|
|
|
|
if (this.node.incompleteValue) {
|
2015-02-03 18:30:45 +00:00
|
|
|
text += ELLIPSIS;
|
2013-06-11 04:18:46 +00:00
|
|
|
}
|
|
|
|
this.value.textContent = text;
|
|
|
|
} else {
|
|
|
|
let longstr = null;
|
|
|
|
this.node.getNodeValue().then(ret => {
|
|
|
|
longstr = ret;
|
|
|
|
return longstr.string();
|
|
|
|
}).then(str => {
|
|
|
|
longstr.release().then(null, console.error);
|
|
|
|
if (this.selected) {
|
|
|
|
this.value.textContent = str;
|
2015-05-13 21:55:09 +00:00
|
|
|
this.markup.emit("text-expand")
|
2013-06-11 04:18:46 +00:00
|
|
|
}
|
|
|
|
}).then(null, console.error);
|
|
|
|
}
|
2013-10-11 15:50:33 +00:00
|
|
|
},
|
|
|
|
|
2015-10-20 14:47:02 +00:00
|
|
|
destroy: function() {},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stub method for consistency with ElementEditor.
|
|
|
|
*/
|
|
|
|
getInfoAtNode: function() {
|
|
|
|
return null;
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates an editor for an Element node.
|
|
|
|
*
|
|
|
|
* @param MarkupContainer aContainer The container owning this editor.
|
|
|
|
* @param Element aNode The node being edited.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
function ElementEditor(aContainer, aNode) {
|
2012-08-25 18:04:46 +00:00
|
|
|
this.container = aContainer;
|
2012-08-23 18:00:43 +00:00
|
|
|
this.node = aNode;
|
2014-09-29 07:29:00 +00:00
|
|
|
this.markup = this.container.markup;
|
|
|
|
this.template = this.markup.template.bind(this.markup);
|
|
|
|
this.doc = this.markup.doc;
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-03-27 14:30:09 +00:00
|
|
|
this.attrElements = new Map();
|
2015-03-24 21:57:57 +00:00
|
|
|
this.animationTimers = {};
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
// The templates will fill the following properties
|
|
|
|
this.elt = null;
|
|
|
|
this.tag = null;
|
2013-09-12 14:48:13 +00:00
|
|
|
this.closeTag = null;
|
2012-08-23 18:00:43 +00:00
|
|
|
this.attrList = null;
|
|
|
|
this.newAttr = null;
|
|
|
|
this.closeElt = null;
|
|
|
|
|
|
|
|
// Create the main editor
|
2012-12-20 00:16:45 +00:00
|
|
|
this.template("element", this);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-06-17 13:52:55 +00:00
|
|
|
// Make the tag name editable (unless this is a remote node or
|
|
|
|
// a document element)
|
2014-10-08 22:46:16 +00:00
|
|
|
if (!aNode.isDocumentElement) {
|
2013-06-17 13:52:55 +00:00
|
|
|
this.tag.setAttribute("tabindex", "0");
|
2013-03-20 12:11:50 +00:00
|
|
|
editableField({
|
2013-06-17 13:52:55 +00:00
|
|
|
element: this.tag,
|
2012-08-23 18:00:43 +00:00
|
|
|
trigger: "dblclick",
|
|
|
|
stopOnReturn: true,
|
2013-06-17 13:52:55 +00:00
|
|
|
done: this.onTagEdit.bind(this),
|
2012-08-23 18:00:43 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-06-17 13:52:55 +00:00
|
|
|
// Make the new attribute space editable.
|
2015-01-15 00:46:00 +00:00
|
|
|
this.newAttr.editMode = editableField({
|
2013-06-17 13:52:55 +00:00
|
|
|
element: this.newAttr,
|
|
|
|
trigger: "dblclick",
|
|
|
|
stopOnReturn: true,
|
2013-08-02 10:35:50 +00:00
|
|
|
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
|
|
|
|
popup: this.markup.popup,
|
2013-06-17 13:52:55 +00:00
|
|
|
done: (aVal, aCommit) => {
|
|
|
|
if (!aCommit) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-20 14:47:02 +00:00
|
|
|
let doMods = this._startModifyingAttributes();
|
|
|
|
let undoMods = this._startModifyingAttributes();
|
|
|
|
this._applyAttributes(aVal, null, doMods, undoMods);
|
|
|
|
this.container.undo.do(() => {
|
|
|
|
doMods.apply();
|
|
|
|
}, function() {
|
|
|
|
undoMods.apply();
|
|
|
|
});
|
2013-06-17 13:52:55 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
let tagName = this.node.nodeName.toLowerCase();
|
|
|
|
this.tag.textContent = tagName;
|
|
|
|
this.closeTag.textContent = tagName;
|
2014-07-20 11:03:59 +00:00
|
|
|
this.eventNode.style.display = this.node.hasEventListeners ? "inline-block" : "none";
|
2012-08-23 18:00:43 +00:00
|
|
|
|
|
|
|
this.update();
|
2015-03-24 21:57:57 +00:00
|
|
|
this.initialized = true;
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ElementEditor.prototype = {
|
2015-03-24 21:57:57 +00:00
|
|
|
|
2015-05-13 21:55:09 +00:00
|
|
|
set selected(aValue) {
|
|
|
|
if (this.textEditor) {
|
|
|
|
this.textEditor.selected = aValue;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-24 21:57:57 +00:00
|
|
|
flashAttribute: function(attrName) {
|
|
|
|
if (this.animationTimers[attrName]) {
|
|
|
|
clearTimeout(this.animationTimers[attrName]);
|
|
|
|
}
|
|
|
|
|
|
|
|
flashElementOn(this.getAttributeElement(attrName));
|
|
|
|
|
|
|
|
this.animationTimers[attrName] = setTimeout(() => {
|
|
|
|
flashElementOff(this.getAttributeElement(attrName));
|
|
|
|
}, this.markup.CONTAINER_FLASHING_DURATION);
|
|
|
|
},
|
2015-10-20 14:47:02 +00:00
|
|
|
/**
|
|
|
|
* Returns information about node in the editor.
|
|
|
|
*
|
|
|
|
* @param {DOMNode} node
|
|
|
|
* The node to get information from.
|
|
|
|
*
|
|
|
|
* @return {Object}
|
|
|
|
* An object literal with the following information:
|
|
|
|
* {type: "attribute", name: "rel", value: "index", el: node}
|
|
|
|
*/
|
|
|
|
getInfoAtNode: function(node) {
|
|
|
|
if (!node) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let type = null;
|
|
|
|
let name = null;
|
|
|
|
let value = null;
|
|
|
|
|
|
|
|
// Attribute
|
|
|
|
let attribute = node.closest('.attreditor');
|
|
|
|
if (attribute) {
|
|
|
|
type = "attribute";
|
|
|
|
name = attribute.querySelector('.attr-name').textContent;
|
|
|
|
value = attribute.querySelector('.attr-value').textContent;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {type, name, value, el: node};
|
|
|
|
},
|
2015-03-24 21:57:57 +00:00
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Update the state of the editor from the node.
|
|
|
|
*/
|
2013-10-08 12:53:19 +00:00
|
|
|
update: function() {
|
2015-03-27 14:30:09 +00:00
|
|
|
let nodeAttributes = this.node.attributes || [];
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-03-27 14:30:09 +00:00
|
|
|
// Keep the data model in sync with attributes on the node.
|
|
|
|
let currentAttributes = new Set(nodeAttributes.map(a=>a.name));
|
|
|
|
for (let name of this.attrElements.keys()) {
|
|
|
|
if (!currentAttributes.has(name)) {
|
|
|
|
this.removeAttribute(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only loop through the current attributes on the node. Missing
|
|
|
|
// attributes have already been removed at this point.
|
|
|
|
for (let attr of nodeAttributes) {
|
|
|
|
let el = this.attrElements.get(attr.name);
|
2015-04-30 08:43:04 +00:00
|
|
|
let valueChanged = el && el.querySelector(".attr-value").textContent !== attr.value;
|
2015-03-09 17:02:21 +00:00
|
|
|
let isEditing = el && el.querySelector(".editable").inplaceEditor;
|
2015-03-24 21:57:57 +00:00
|
|
|
let canSimplyShowEditor = el && (!valueChanged || isEditing);
|
2015-03-09 17:02:21 +00:00
|
|
|
|
2015-03-24 21:57:57 +00:00
|
|
|
if (canSimplyShowEditor) {
|
2015-03-09 17:02:21 +00:00
|
|
|
// Element already exists and doesn't need to be recreated.
|
|
|
|
// Just show it (it's hidden by default due to the template).
|
|
|
|
el.style.removeProperty("display");
|
|
|
|
} else {
|
|
|
|
// Create a new editor, because the value of an existing attribute
|
|
|
|
// has changed.
|
2015-11-26 03:44:00 +00:00
|
|
|
let attribute = this._createAttribute(attr, el);
|
2013-09-16 10:01:25 +00:00
|
|
|
attribute.style.removeProperty("display");
|
2015-03-24 21:57:57 +00:00
|
|
|
|
|
|
|
// Temporarily flash the attribute to highlight the change.
|
|
|
|
// But not if this is the first time the editor instance has
|
|
|
|
// been created.
|
|
|
|
if (this.initialized) {
|
|
|
|
this.flashAttribute(attr.name);
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
}
|
2015-05-13 21:55:09 +00:00
|
|
|
|
|
|
|
this.updateTextEditor();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the inline text editor in case of a single text child node.
|
|
|
|
*/
|
|
|
|
updateTextEditor: function() {
|
|
|
|
let node = this.node.singleTextChild;
|
|
|
|
|
|
|
|
if (this.textEditor && this.textEditor.node != node) {
|
|
|
|
this.elt.removeChild(this.textEditor.elt);
|
|
|
|
this.textEditor = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node && !this.textEditor) {
|
|
|
|
// Create a text editor added to this editor.
|
|
|
|
// This editor won't receive an update automatically, so we rely on
|
|
|
|
// child text editors to let us know that we need updating.
|
|
|
|
this.textEditor = new TextEditor(this.container, node, "text");
|
|
|
|
this.elt.insertBefore(this.textEditor.elt,
|
|
|
|
this.elt.firstChild.nextSibling.nextSibling);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.textEditor) {
|
|
|
|
this.textEditor.update();
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
2013-06-17 13:52:55 +00:00
|
|
|
_startModifyingAttributes: function() {
|
|
|
|
return this.node.startModifyingAttributes();
|
|
|
|
},
|
|
|
|
|
2013-10-25 19:21:01 +00:00
|
|
|
/**
|
|
|
|
* Get the element used for one of the attributes of this element
|
|
|
|
* @param string attrName The name of the attribute to get the element for
|
|
|
|
* @return DOMElement
|
|
|
|
*/
|
|
|
|
getAttributeElement: function(attrName) {
|
|
|
|
return this.attrList.querySelector(
|
|
|
|
".attreditor[data-attr=" + attrName + "] .attr-value");
|
|
|
|
},
|
|
|
|
|
2015-03-27 14:30:09 +00:00
|
|
|
/**
|
|
|
|
* Remove an attribute from the attrElements object and the DOM
|
|
|
|
* @param string attrName The name of the attribute to remove
|
|
|
|
*/
|
|
|
|
removeAttribute: function(attrName) {
|
|
|
|
let attr = this.attrElements.get(attrName);
|
|
|
|
if (attr) {
|
|
|
|
this.attrElements.delete(attrName);
|
|
|
|
attr.remove();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-10-02 00:14:00 +00:00
|
|
|
_createAttribute: function(aAttr, aBefore = null) {
|
2013-08-15 14:40:44 +00:00
|
|
|
// Create the template editor, which will save some variables here.
|
|
|
|
let data = {
|
|
|
|
attrName: aAttr.name,
|
|
|
|
};
|
|
|
|
this.template("attribute", data);
|
2015-04-30 08:43:04 +00:00
|
|
|
let {attr, inner, name, val} = data;
|
2013-08-15 14:40:44 +00:00
|
|
|
|
|
|
|
// Double quotes need to be handled specially to prevent DOMParser failing.
|
|
|
|
// name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
|
|
|
|
// name="v'a"l'u"e" when editing -> name="v'a"l'u"e"
|
2013-09-08 09:01:00 +00:00
|
|
|
let editValueDisplayed = aAttr.value || "";
|
2015-04-29 15:32:05 +00:00
|
|
|
let hasDoubleQuote = editValueDisplayed.includes('"');
|
|
|
|
let hasSingleQuote = editValueDisplayed.includes("'");
|
2013-08-15 14:40:44 +00:00
|
|
|
let initial = aAttr.name + '="' + editValueDisplayed + '"';
|
|
|
|
|
|
|
|
// Can't just wrap value with ' since the value contains both " and '.
|
|
|
|
if (hasDoubleQuote && hasSingleQuote) {
|
2015-04-30 08:43:04 +00:00
|
|
|
editValueDisplayed = editValueDisplayed.replace(/\"/g, """);
|
|
|
|
initial = aAttr.name + '="' + editValueDisplayed + '"';
|
2013-08-15 14:40:44 +00:00
|
|
|
}
|
2013-06-17 13:52:55 +00:00
|
|
|
|
2013-08-15 14:40:44 +00:00
|
|
|
// Wrap with ' since there are no single quotes in the attribute value.
|
|
|
|
if (hasDoubleQuote && !hasSingleQuote) {
|
2015-04-30 08:43:04 +00:00
|
|
|
initial = aAttr.name + "='" + editValueDisplayed + "'";
|
2013-08-15 14:40:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make the attribute editable.
|
2015-01-15 00:46:00 +00:00
|
|
|
attr.editMode = editableField({
|
2013-08-15 14:40:44 +00:00
|
|
|
element: inner,
|
|
|
|
trigger: "dblclick",
|
|
|
|
stopOnReturn: true,
|
|
|
|
selectAll: false,
|
|
|
|
initial: initial,
|
|
|
|
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
|
|
|
|
popup: this.markup.popup,
|
|
|
|
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);
|
2015-04-30 08:43:04 +00:00
|
|
|
} else if (aEvent && aEvent.target.closest(".attr-value") === val) {
|
2013-08-15 14:40:44 +00:00
|
|
|
let length = editValueDisplayed.length;
|
|
|
|
let editorLength = aEditor.input.value.length;
|
|
|
|
let start = editorLength - (length + 1);
|
|
|
|
aEditor.input.setSelectionRange(start, start + length);
|
|
|
|
} else {
|
|
|
|
aEditor.input.select();
|
2013-06-17 13:52:55 +00:00
|
|
|
}
|
2013-08-15 14:40:44 +00:00
|
|
|
},
|
2015-01-15 00:46:00 +00:00
|
|
|
done: (aVal, aCommit, direction) => {
|
2013-09-23 06:46:12 +00:00
|
|
|
if (!aCommit || aVal === initial) {
|
2013-08-15 14:40:44 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let doMods = this._startModifyingAttributes();
|
|
|
|
let undoMods = this._startModifyingAttributes();
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2013-08-15 14:40:44 +00:00
|
|
|
// Remove the attribute stored in this editor and re-add any attributes
|
|
|
|
// parsed out of the input element. Restore original attribute if
|
|
|
|
// parsing fails.
|
2015-10-20 14:47:02 +00:00
|
|
|
this.refocusOnEdit(aAttr.name, attr, direction);
|
|
|
|
this._saveAttribute(aAttr.name, undoMods);
|
|
|
|
doMods.removeAttribute(aAttr.name);
|
|
|
|
this._applyAttributes(aVal, attr, doMods, undoMods);
|
|
|
|
this.container.undo.do(() => {
|
|
|
|
doMods.apply();
|
|
|
|
}, () => {
|
|
|
|
undoMods.apply();
|
|
|
|
});
|
2013-08-15 14:40:44 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Figure out where we should place the attribute.
|
|
|
|
let before = aBefore;
|
|
|
|
if (aAttr.name == "id") {
|
|
|
|
before = this.attrList.firstChild;
|
|
|
|
} else if (aAttr.name == "class") {
|
2015-03-27 14:30:09 +00:00
|
|
|
let idNode = this.attrElements.get("id");
|
2013-08-15 14:40:44 +00:00
|
|
|
before = idNode ? idNode.nextSibling : this.attrList.firstChild;
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
2013-08-15 14:40:44 +00:00
|
|
|
this.attrList.insertBefore(attr, before);
|
|
|
|
|
2015-03-27 14:30:09 +00:00
|
|
|
this.removeAttribute(aAttr.name);
|
|
|
|
this.attrElements.set(aAttr.name, attr);
|
2012-08-23 18:00:43 +00:00
|
|
|
|
2015-04-30 08:43:04 +00:00
|
|
|
// Parse the attribute value to detect whether there are linkable parts in
|
|
|
|
// it (make sure to pass a complete list of existing attributes to the
|
|
|
|
// parseAttribute function, by concatenating aAttr, because this could be a
|
|
|
|
// newly added attribute not yet on this.node).
|
|
|
|
let attributes = this.node.attributes.filter(({name}) => name !== aAttr.name);
|
|
|
|
attributes.push(aAttr);
|
|
|
|
let parsedLinksData = parseAttribute(this.node.namespaceURI,
|
|
|
|
this.node.tagName, attributes, aAttr.name);
|
|
|
|
|
|
|
|
// Create links in the attribute value, and collapse long attributes if
|
|
|
|
// needed.
|
|
|
|
let collapse = value => {
|
2015-08-12 13:38:31 +00:00
|
|
|
if (value && value.match(COLLAPSE_DATA_URL_REGEX)) {
|
2015-04-30 08:43:04 +00:00
|
|
|
return truncateString(value, COLLAPSE_DATA_URL_LENGTH);
|
|
|
|
}
|
2015-11-20 17:24:00 +00:00
|
|
|
return this.markup.collapseAttributeLength < 0
|
|
|
|
? value :
|
|
|
|
truncateString(value, this.markup.collapseAttributeLength);
|
2015-04-30 08:43:04 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
val.innerHTML = "";
|
|
|
|
for (let token of parsedLinksData) {
|
|
|
|
if (token.type === "string") {
|
|
|
|
val.appendChild(this.doc.createTextNode(collapse(token.value)));
|
|
|
|
} else {
|
|
|
|
let link = this.doc.createElement("span");
|
|
|
|
link.classList.add("link");
|
|
|
|
link.setAttribute("data-type", token.type);
|
|
|
|
link.setAttribute("data-link", token.value);
|
|
|
|
link.textContent = collapse(token.value);
|
|
|
|
val.appendChild(link);
|
|
|
|
}
|
2013-09-12 22:25:08 +00:00
|
|
|
}
|
|
|
|
|
2014-01-10 17:55:58 +00:00
|
|
|
name.textContent = aAttr.name;
|
2013-10-18 14:01:20 +00:00
|
|
|
|
2014-01-10 17:55:58 +00:00
|
|
|
return attr;
|
2012-08-23 18:00:43 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_applyAttributes: function(aValue, aAttrNode, aDoMods, aUndoMods) {
|
2013-08-08 13:55:39 +00:00
|
|
|
let attrs = parseAttributeValues(aValue, this.doc);
|
2013-03-21 11:36:52 +00:00
|
|
|
for (let attr of attrs) {
|
2012-08-23 18:00:43 +00:00
|
|
|
// Create an attribute editor next to the current attribute if needed.
|
2013-06-17 13:52:55 +00:00
|
|
|
this._createAttribute(attr, aAttrNode ? aAttrNode.nextSibling : null);
|
|
|
|
this._saveAttribute(attr.name, aUndoMods);
|
|
|
|
aDoMods.setAttribute(attr.name, attr.value);
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2013-06-17 13:52:55 +00:00
|
|
|
* Saves the current state of the given attribute into an attribute
|
|
|
|
* modification list.
|
2012-08-23 18:00:43 +00:00
|
|
|
*/
|
2013-10-02 00:14:00 +00:00
|
|
|
_saveAttribute: function(aName, aUndoMods) {
|
2013-06-17 13:52:55 +00:00
|
|
|
let node = this.node;
|
|
|
|
if (node.hasAttribute(aName)) {
|
|
|
|
let oldValue = node.getAttribute(aName);
|
|
|
|
aUndoMods.setAttribute(aName, oldValue);
|
|
|
|
} else {
|
|
|
|
aUndoMods.removeAttribute(aName);
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-01-15 00:46:00 +00:00
|
|
|
/**
|
|
|
|
* Listen to mutations, and when the attribute list is regenerated
|
|
|
|
* try to focus on the attribute after the one that's being edited now.
|
|
|
|
* If the attribute order changes, go to the beginning of the attribute list.
|
|
|
|
*/
|
|
|
|
refocusOnEdit: function(attrName, attrNode, direction) {
|
|
|
|
// Only allow one refocus on attribute change at a time, so when there's
|
|
|
|
// more than 1 request in parallel, the last one wins.
|
|
|
|
if (this._editedAttributeObserver) {
|
|
|
|
this.markup._inspector.off("markupmutation", this._editedAttributeObserver);
|
|
|
|
this._editedAttributeObserver = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let container = this.markup.getContainer(this.node);
|
|
|
|
|
|
|
|
let activeAttrs = [...this.attrList.childNodes].filter(el => el.style.display != "none");
|
|
|
|
let attributeIndex = activeAttrs.indexOf(attrNode);
|
|
|
|
|
|
|
|
let onMutations = this._editedAttributeObserver = (e, mutations) => {
|
|
|
|
let isDeletedAttribute = false;
|
|
|
|
let isNewAttribute = false;
|
|
|
|
for (let mutation of mutations) {
|
|
|
|
let inContainer = this.markup.getContainer(mutation.target) === container;
|
|
|
|
if (!inContainer) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let isOriginalAttribute = mutation.attributeName === attrName;
|
|
|
|
|
|
|
|
isDeletedAttribute = isDeletedAttribute || isOriginalAttribute && mutation.newValue === null;
|
|
|
|
isNewAttribute = isNewAttribute || mutation.attributeName !== attrName;
|
|
|
|
}
|
|
|
|
let isModifiedOrder = isDeletedAttribute && isNewAttribute;
|
|
|
|
this._editedAttributeObserver = null;
|
|
|
|
|
|
|
|
// "Deleted" attributes are merely hidden, so filter them out.
|
|
|
|
let visibleAttrs = [...this.attrList.childNodes].filter(el => el.style.display != "none");
|
|
|
|
let activeEditor;
|
|
|
|
if (visibleAttrs.length > 0) {
|
|
|
|
if (!direction) {
|
|
|
|
// No direction was given; stay on current attribute.
|
|
|
|
activeEditor = visibleAttrs[attributeIndex];
|
|
|
|
} else if (isModifiedOrder) {
|
|
|
|
// The attribute was renamed, reordering the existing attributes.
|
|
|
|
// So let's go to the beginning of the attribute list for consistency.
|
|
|
|
activeEditor = visibleAttrs[0];
|
|
|
|
} else {
|
|
|
|
let newAttributeIndex;
|
|
|
|
if (isDeletedAttribute) {
|
|
|
|
newAttributeIndex = attributeIndex;
|
|
|
|
} else {
|
|
|
|
if (direction == Ci.nsIFocusManager.MOVEFOCUS_FORWARD) {
|
|
|
|
newAttributeIndex = attributeIndex + 1;
|
|
|
|
} else if (direction == Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) {
|
|
|
|
newAttributeIndex = attributeIndex - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The number of attributes changed (deleted), or we moved through the array
|
|
|
|
// so check we're still within bounds.
|
|
|
|
if (newAttributeIndex >= 0 && newAttributeIndex <= visibleAttrs.length - 1) {
|
|
|
|
activeEditor = visibleAttrs[newAttributeIndex];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Either we have no attributes left,
|
|
|
|
// or we just edited the last attribute and want to move on.
|
|
|
|
if (!activeEditor) {
|
|
|
|
activeEditor = this.newAttr;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refocus was triggered by tab or shift-tab.
|
|
|
|
// Continue in edit mode.
|
|
|
|
if (direction) {
|
|
|
|
activeEditor.editMode();
|
|
|
|
} else {
|
|
|
|
// Refocus was triggered by enter.
|
|
|
|
// Exit edit mode (but restore focus).
|
|
|
|
let editable = activeEditor === this.newAttr ? activeEditor : activeEditor.querySelector(".editable");
|
|
|
|
editable.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.markup.emit("refocusedonedit");
|
|
|
|
};
|
|
|
|
|
|
|
|
// Start listening for mutations until we find an attributes change
|
|
|
|
// that modifies this attribute.
|
|
|
|
this.markup._inspector.once("markupmutation", onMutations);
|
|
|
|
},
|
|
|
|
|
2012-08-23 18:00:43 +00:00
|
|
|
/**
|
|
|
|
* Called when the tag name editor has is done editing.
|
|
|
|
*/
|
2014-10-08 22:46:16 +00:00
|
|
|
onTagEdit: function(newTagName, isCommit) {
|
2014-12-05 00:34:47 +00:00
|
|
|
if (!isCommit || newTagName.toLowerCase() === this.node.tagName.toLowerCase() ||
|
2014-10-08 22:46:16 +00:00
|
|
|
!("editTagName" in this.markup.walker)) {
|
2012-08-25 18:04:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-10-08 22:46:16 +00:00
|
|
|
// Changing the tagName removes the node. Make sure the replacing node gets
|
|
|
|
// selected afterwards.
|
|
|
|
this.markup.reselectOnRemoved(this.node, "edittagname");
|
|
|
|
this.markup.walker.editTagName(this.node, newTagName).then(null, () => {
|
|
|
|
// Failed to edit the tag name, cancel the reselection.
|
|
|
|
this.markup.cancelReselectOnRemoved();
|
|
|
|
});
|
2013-10-11 15:50:33 +00:00
|
|
|
},
|
|
|
|
|
2015-03-24 21:57:57 +00:00
|
|
|
destroy: function() {
|
|
|
|
for (let key in this.animationTimers) {
|
|
|
|
clearTimeout(this.animationTimers[key]);
|
|
|
|
}
|
|
|
|
this.animationTimers = null;
|
|
|
|
}
|
2012-08-23 18:00:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
function nodeDocument(node) {
|
2013-10-02 00:14:00 +00:00
|
|
|
return node.ownerDocument ||
|
|
|
|
(node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2013-09-12 22:25:08 +00:00
|
|
|
function truncateString(str, maxLength) {
|
2015-08-12 13:38:31 +00:00
|
|
|
if (!str || str.length <= maxLength) {
|
2013-09-12 22:25:08 +00:00
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
|
|
|
return str.substring(0, Math.ceil(maxLength / 2)) +
|
|
|
|
"…" +
|
|
|
|
str.substring(str.length - Math.floor(maxLength / 2));
|
|
|
|
}
|
2014-09-08 12:30:00 +00:00
|
|
|
|
2013-03-21 11:36:52 +00:00
|
|
|
/**
|
2013-08-08 13:55:39 +00:00
|
|
|
* Parse attribute names and values from a string.
|
2013-03-21 11:36:52 +00:00
|
|
|
*
|
|
|
|
* @param {String} attr
|
2013-08-08 13:55:39 +00:00
|
|
|
* The input string for which names/values are to be parsed.
|
|
|
|
* @param {HTMLDocument} doc
|
|
|
|
* A document that can be used to test valid attributes.
|
2013-03-21 11:36:52 +00:00
|
|
|
* @return {Array}
|
2013-08-08 13:55:39 +00:00
|
|
|
* An array of attribute names and their values.
|
2013-03-21 11:36:52 +00:00
|
|
|
*/
|
2013-08-08 13:55:39 +00:00
|
|
|
function parseAttributeValues(attr, doc) {
|
|
|
|
attr = attr.trim();
|
2013-03-21 11:36:52 +00:00
|
|
|
|
2014-09-08 12:30:00 +00:00
|
|
|
// Handle bad user inputs by appending a " or ' if it fails to parse without
|
|
|
|
// them. Also note that a SVG tag is used to make sure the HTML parser
|
|
|
|
// preserves mixed-case attributes
|
|
|
|
let el = DOMParser.parseFromString("<svg " + attr + "></svg>", "text/html").body.childNodes[0] ||
|
|
|
|
DOMParser.parseFromString("<svg " + attr + "\"></svg>", "text/html").body.childNodes[0] ||
|
|
|
|
DOMParser.parseFromString("<svg " + attr + "'></svg>", "text/html").body.childNodes[0];
|
2014-09-04 08:28:24 +00:00
|
|
|
|
2013-08-08 13:55:39 +00:00
|
|
|
let div = doc.createElement("div");
|
|
|
|
let attributes = [];
|
2014-09-08 12:30:00 +00:00
|
|
|
for (let {name, value} of el.attributes) {
|
2013-08-08 13:55:39 +00:00
|
|
|
// Try to set on an element in the document, throws exception on bad input.
|
|
|
|
// Prevents InvalidCharacterError - "String contains an invalid character".
|
|
|
|
try {
|
2014-09-04 08:28:24 +00:00
|
|
|
div.setAttribute(name, value);
|
|
|
|
attributes.push({ name, value });
|
2013-03-21 11:36:52 +00:00
|
|
|
}
|
2013-08-08 13:55:39 +00:00
|
|
|
catch(e) { }
|
2013-03-21 11:36:52 +00:00
|
|
|
}
|
|
|
|
|
2013-08-08 13:55:39 +00:00
|
|
|
// Attributes return from DOMParser in reverse order from how they are entered.
|
|
|
|
return attributes.reverse();
|
2012-08-23 18:00:43 +00:00
|
|
|
}
|
|
|
|
|
2015-03-24 21:57:57 +00:00
|
|
|
/**
|
|
|
|
* Apply a 'flashed' background and foreground color to elements. Intended
|
|
|
|
* to be used with flashElementOff as a way of drawing attention to an element.
|
|
|
|
*
|
|
|
|
* @param {Node} backgroundElt
|
|
|
|
* The element to set the highlighted background color on.
|
|
|
|
* @param {Node} foregroundElt
|
|
|
|
* The element to set the matching foreground color on.
|
|
|
|
* Optional. This will equal backgroundElt if not set.
|
|
|
|
*/
|
|
|
|
function flashElementOn(backgroundElt, foregroundElt=backgroundElt) {
|
|
|
|
if (!backgroundElt || !foregroundElt) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the animation class is not here
|
|
|
|
backgroundElt.classList.remove("flash-out");
|
|
|
|
|
|
|
|
// Change the background
|
|
|
|
backgroundElt.classList.add("theme-bg-contrast");
|
|
|
|
|
|
|
|
foregroundElt.classList.add("theme-fg-contrast");
|
|
|
|
[].forEach.call(
|
|
|
|
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
|
|
|
|
span => span.classList.add("theme-fg-contrast")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a 'flashed' background and foreground color to elements.
|
|
|
|
* See flashElementOn.
|
|
|
|
*
|
|
|
|
* @param {Node} backgroundElt
|
|
|
|
* The element to reomve the highlighted background color on.
|
|
|
|
* @param {Node} foregroundElt
|
|
|
|
* The element to remove the matching foreground color on.
|
|
|
|
* Optional. This will equal backgroundElt if not set.
|
|
|
|
*/
|
|
|
|
function flashElementOff(backgroundElt, foregroundElt=backgroundElt) {
|
|
|
|
if (!backgroundElt || !foregroundElt) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the animation class to smoothly remove the background
|
|
|
|
backgroundElt.classList.add("flash-out");
|
|
|
|
|
|
|
|
// Remove the background
|
|
|
|
backgroundElt.classList.remove("theme-bg-contrast");
|
|
|
|
|
|
|
|
foregroundElt.classList.remove("theme-fg-contrast");
|
|
|
|
[].forEach.call(
|
|
|
|
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
|
|
|
|
span => span.classList.remove("theme-fg-contrast")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2014-12-26 10:53:00 +00:00
|
|
|
/**
|
|
|
|
* Map a number from one range to another.
|
|
|
|
*/
|
|
|
|
function map(value, oldMin, oldMax, newMin, newMax) {
|
|
|
|
let ratio = oldMax - oldMin;
|
|
|
|
if (ratio == 0) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
return newMin + (newMax - newMin) * ((value - oldMin) / ratio);
|
|
|
|
}
|
|
|
|
|
2013-04-11 20:59:08 +00:00
|
|
|
loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
|
2015-11-04 21:35:53 +00:00
|
|
|
"chrome://devtools/locale/inspector.properties"
|
2013-04-11 20:59:08 +00:00
|
|
|
));
|
2014-01-30 16:33:53 +00:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
|
|
|
|
return Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
|
|
getService(Ci.nsIClipboardHelper);
|
|
|
|
});
|