Mypal/devtools/client/inspector/markup/markup.js

1876 lines
58 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const promise = require("promise");
const Services = require("Services");
const defer = require("devtools/shared/defer");
const {Task} = require("devtools/shared/task");
const nodeConstants = require("devtools/shared/dom-node-constants");
const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
const EventEmitter = require("devtools/shared/event-emitter");
const {LocalizationHelper} = require("devtools/shared/l10n");
const {PluralForm} = require("devtools/shared/plural-form");
const {template} = require("devtools/shared/gcli/templater");
const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
const {UndoStack} = require("devtools/client/shared/undo");
const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
const {PrefObserver} = require("devtools/client/styleeditor/utils");
const HTMLEditor = require("devtools/client/inspector/markup/views/html-editor");
const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
const RootContainer = require("devtools/client/inspector/markup/views/root-container");
const INSPECTOR_L10N =
new LocalizationHelper("devtools/client/locales/inspector.properties");
// Page size for pageup/pagedown
const PAGE_SIZE = 10;
const DEFAULT_MAX_CHILDREN = 100;
const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50;
const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1;
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2;
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8;
const DRAG_DROP_HEIGHT_TO_SPEED = 500;
const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5;
const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1;
const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
/**
* Vocabulary for the purposes of this file:
*
* MarkupContainer - the structure that holds an editor and its
* immediate children in the markup panel.
* - MarkupElementContainer: markup container for element nodes
* - MarkupTextContainer: markup container for text / comment nodes
* - MarkupReadonlyContainer: markup container for other nodes
* 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} inspector
* The inspector we're watching.
* @param {iframe} frame
* An iframe in which the caller has kindly loaded markup.xhtml.
*/
function MarkupView(inspector, frame, controllerWindow) {
this.inspector = inspector;
this.walker = this.inspector.walker;
this._frame = frame;
this.win = this._frame.contentWindow;
this.doc = this._frame.contentDocument;
this._elt = this.doc.querySelector("#root");
this.htmlEditor = new HTMLEditor(this.doc);
this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize",
DEFAULT_MAX_CHILDREN);
this.collapseAttributes =
Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF);
this.collapseAttributeLength =
Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF);
// Creating the popup to be used to show CSS suggestions.
// The popup will be attached to the toolbox document.
this.popup = new AutocompletePopup(inspector.toolbox.doc, {
autoSelect: true,
theme: "auto",
});
this.undo = new UndoStack();
this.undo.installController(controllerWindow);
this._containers = new Map();
// Binding functions that need to be called in scope.
this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
this._mutationObserver = this._mutationObserver.bind(this);
this._onDisplayChange = this._onDisplayChange.bind(this);
this._onMouseClick = this._onMouseClick.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onNewSelection = this._onNewSelection.bind(this);
this._onCopy = this._onCopy.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseOut = this._onMouseOut.bind(this);
this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
this._onCollapseAttributesPrefChange =
this._onCollapseAttributesPrefChange.bind(this);
this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
this._onBlur = this._onBlur.bind(this);
EventEmitter.decorate(this);
// Listening to various events.
this._elt.addEventListener("click", this._onMouseClick, false);
this._elt.addEventListener("mousemove", this._onMouseMove, false);
this._elt.addEventListener("mouseout", this._onMouseOut, false);
this._elt.addEventListener("blur", this._onBlur, true);
this.win.addEventListener("mouseup", this._onMouseUp);
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.toolbox.on("picker-canceled", this._onToolboxPickerCanceled);
this.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
this._onNewSelection();
this._initTooltips();
this._prefObserver = new PrefObserver("devtools.markup");
this._prefObserver.on(ATTR_COLLAPSE_ENABLED_PREF,
this._onCollapseAttributesPrefChange);
this._prefObserver.on(ATTR_COLLAPSE_LENGTH_PREF,
this._onCollapseAttributesPrefChange);
this._initShortcuts();
}
MarkupView.prototype = {
/**
* How long does a node flash when it mutates (in ms).
*/
CONTAINER_FLASHING_DURATION: 500,
_selectedContainer: null,
get toolbox() {
return this.inspector.toolbox;
},
/**
* Handle promise rejections for various asynchronous actions, and only log errors if
* the markup view still exists.
* This is useful to silence useless errors that happen when the markup view is
* destroyed while still initializing (and making protocol requests).
*/
_handleRejectionIfNotDestroyed: function (e) {
if (!this._destroyer) {
console.error(e);
}
},
_initTooltips: function () {
// The tooltips will be attached to the toolbox document.
this.eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc,
{type: "arrow"});
this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc,
{type: "arrow", useXulWrapper: "true"});
this._enableImagePreviewTooltip();
},
_enableImagePreviewTooltip: function () {
this.imagePreviewTooltip.startTogglingOnHover(this._elt,
this._isImagePreviewTarget);
},
_disableImagePreviewTooltip: function () {
this.imagePreviewTooltip.stopTogglingOnHover();
},
_onToolboxPickerHover: function (event, nodeFront) {
this.showNode(nodeFront).then(() => {
this._showContainerAsHovered(nodeFront);
}, e => console.error(e));
},
/**
* If the element picker gets canceled, make sure and re-center the view on the
* current selected element.
*/
_onToolboxPickerCanceled: function () {
if (this._selectedContainer) {
scrollIntoViewIfNeeded(this._selectedContainer.editor.elt);
}
},
isDragging: false,
_onMouseMove: function (event) {
let target = event.target;
// Auto-scroll if we're dragging.
if (this.isDragging) {
event.preventDefault();
this._autoScroll(event);
return;
}
// Show the current container as hovered and highlight it.
// This requires finding the current MarkupContainer (walking up the DOM).
while (!target.container) {
if (target.tagName.toLowerCase() === "body") {
return;
}
target = target.parentNode;
}
let container = target.container;
if (this._hoveredNode !== container.node) {
this._showBoxModel(container.node);
}
this._showContainerAsHovered(container.node);
this.emit("node-hover");
},
/**
* If focus is moved outside of the markup view document and there is a
* selected container, make its contents not focusable by a keyboard.
*/
_onBlur: function (event) {
if (!this._selectedContainer) {
return;
}
let {relatedTarget} = event;
if (relatedTarget && relatedTarget.ownerDocument === this.doc) {
return;
}
if (this._selectedContainer) {
this._selectedContainer.clearFocus();
}
},
/**
* 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._autoScrollAnimationFrame) {
this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
}
// 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;
let edgeDistance = Math.min(DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE,
docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO);
// The smaller the screen, the slower the movement.
let heightToSpeedRatio =
Math.max(DRAG_DROP_HEIGHT_TO_SPEED_MIN,
Math.min(DRAG_DROP_HEIGHT_TO_SPEED_MAX,
docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED));
if (fromBottom <= edgeDistance) {
// Map our distance range to a speed range so that the speed is not too
// fast or too slow.
let speed = map(
fromBottom,
0, edgeDistance,
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
this._runUpdateLoop(() => {
docEl.scrollTop -= heightToSpeedRatio *
(speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
});
}
if (fromTop <= edgeDistance) {
let speed = map(
fromTop,
0, edgeDistance,
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
this._runUpdateLoop(() => {
docEl.scrollTop += heightToSpeedRatio *
(speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
});
}
},
/**
* Run a loop on the requestAnimationFrame.
*/
_runUpdateLoop: function (update) {
let loop = () => {
update();
this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop);
};
loop();
},
_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;
}
if (container instanceof MarkupElementContainer) {
// With the newly found container, delegate the tooltip content creation
// and decision to show or not the tooltip
container._buildEventTooltipContent(event.target,
this.eventDetailsTooltip);
}
},
_onMouseUp: function () {
this.indicateDropTarget(null);
this.indicateDragTarget(null);
if (this._autoScrollAnimationFrame) {
this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
}
},
_onCollapseAttributesPrefChange: function () {
this.collapseAttributes =
Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF);
this.collapseAttributeLength =
Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF);
this.update();
},
cancelDragging: function () {
if (!this.isDragging) {
return;
}
for (let [, container] of this._containers) {
if (container.isDragging) {
container.cancelDragging();
break;
}
}
this.indicateDropTarget(null);
this.indicateDragTarget(null);
if (this._autoScrollAnimationFrame) {
this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
}
},
_hoveredNode: null,
/**
* Show a NodeFront's container as being hovered
*
* @param {NodeFront} nodeFront
* The node to show as hovered
*/
_showContainerAsHovered: function (nodeFront) {
if (this._hoveredNode === nodeFront) {
return;
}
if (this._hoveredNode) {
this.getContainer(this._hoveredNode).hovered = false;
}
this.getContainer(nodeFront).hovered = true;
this._hoveredNode = nodeFront;
// Emit an event that the container view is actually hovered now, as this function
// can be called by an asynchronous caller.
this.emit("showcontainerhovered");
},
_onMouseOut: function (event) {
// Emulate mouseleave by skipping any relatedTarget inside the markup-view.
if (this._elt.contains(event.relatedTarget)) {
return;
}
if (this._autoScrollAnimationFrame) {
this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
}
if (this.isDragging) {
return;
}
this._hideBoxModel(true);
if (this._hoveredNode) {
this.getContainer(this._hoveredNode).hovered = false;
}
this._hoveredNode = null;
this.emit("leave");
},
/**
* Show the box model highlighter on a given node front
*
* @param {NodeFront} nodeFront
* The node to show the highlighter for
* @return {Promise} Resolves when the highlighter for this nodeFront is
* shown, taking into account that there could already be highlighter
* requests queued up
*/
_showBoxModel: function (nodeFront) {
return this.toolbox.highlighterUtils.highlightNodeFront(nodeFront);
},
/**
* Hide the box model highlighter on a given node front
*
* @param {Boolean} forceHide
* See toolbox-highlighter-utils/unhighlight
* @return {Promise} Resolves when the highlighter for this nodeFront is
* hidden, taking into account that there could already be highlighter
* requests queued up
*/
_hideBoxModel: function (forceHide) {
return this.toolbox.highlighterUtils.unhighlight(forceHide);
},
_briefBoxModelTimer: null,
_clearBriefBoxModelTimer: function () {
if (this._briefBoxModelTimer) {
clearTimeout(this._briefBoxModelTimer);
this._briefBoxModelPromise.resolve();
this._briefBoxModelPromise = null;
this._briefBoxModelTimer = null;
}
},
_brieflyShowBoxModel: function (nodeFront) {
this._clearBriefBoxModelTimer();
let onShown = this._showBoxModel(nodeFront);
this._briefBoxModelPromise = defer();
this._briefBoxModelTimer = setTimeout(() => {
this._hideBoxModel()
.then(this._briefBoxModelPromise.resolve,
this._briefBoxModelPromise.resolve);
}, NEW_SELECTION_HIGHLIGHTER_TIMER);
return promise.all([onShown, this._briefBoxModelPromise.promise]);
},
template: function (name, dest, options = {stack: "markup.xhtml"}) {
let node = this.doc.getElementById("template-" + name).cloneNode(true);
node.removeAttribute("id");
template(node, dest, options);
return node;
},
/**
* Get the MarkupContainer object for a given node, or undefined if
* none exists.
*/
getContainer: function (node) {
return this._containers.get(node);
},
update: function () {
let updateChildren = (node) => {
this.getContainer(node).update();
for (let child of node.treeChildren()) {
updateChildren(child);
}
};
// 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);
},
/**
* 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.
*
* @return {Promise} the promise returned by
* MarkupElementContainer._isImagePreviewTarget
*/
_isImagePreviewTarget: Task.async(function* (target) {
// From the target passed here, let's find the parent MarkupContainer
// and ask it if the tooltip should be shown
if (this.isDragging) {
return false;
}
let parent = target, container;
while (parent !== this.doc.body) {
if (parent.container) {
container = parent.container;
break;
}
parent = parent.parentNode;
}
if (container instanceof MarkupElementContainer) {
// With the newly found container, delegate the tooltip content creation
// and decision to show or not the tooltip
return container.isImagePreviewTarget(target, this.imagePreviewTooltip);
}
return false;
}),
/**
* 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)
* 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.
*/
_shouldNewSelectionBeHighlighted: function () {
let reason = this.inspector.selection.reason;
let unwantedReasons = [
"inspector-open",
"navigateaway",
"nodeselected",
"test"
];
let isHighlight = this._hoveredNode === this.inspector.selection.nodeFront;
return !isHighlight && reason && unwantedReasons.indexOf(reason) === -1;
},
/**
* React to new-node-front selection events.
* Highlights the node if needed, and make sure it is shown and selected in
* the view.
*/
_onNewSelection: function () {
let selection = this.inspector.selection;
this.htmlEditor.hide();
if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
this.getContainer(this._hoveredNode).hovered = false;
this._hoveredNode = null;
}
if (!selection.isNode()) {
this.unmarkSelectedNode();
return;
}
let done = this.inspector.updating("markup-view");
let onShowBoxModel, onShow;
// Highlight the element briefly if needed.
if (this._shouldNewSelectionBeHighlighted()) {
onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
}
onShow = this.showNode(selection.nodeFront).then(() => {
// We could be destroyed by now.
if (this._destroyer) {
return promise.reject("markupview destroyed");
}
// Mark the node as selected.
this.markNodeAsSelected(selection.nodeFront);
// Make sure the new selection is navigated to.
this.maybeNavigateToNewSelection();
return undefined;
}).catch(this._handleRejectionIfNotDestroyed);
promise.all([onShowBoxModel, onShow]).then(done);
},
/**
* Maybe make selected the current node selection's MarkupContainer depending
* on why the current node got selected.
*/
maybeNavigateToNewSelection: function () {
let {reason, nodeFront} = this.inspector.selection;
// The list of reasons that should lead to navigating to the node.
let reasonsToNavigate = [
// If the user picked an element with the element picker.
"picker-node-picked",
// If the user shift-clicked (previewed) an element.
"picker-node-previewed",
// If the user selected an element with the browser context menu.
"browser-context-menu",
// If the user added a new node by clicking in the inspector toolbar.
"node-inserted"
];
if (reasonsToNavigate.includes(reason)) {
this.getContainer(this._rootNode).elt.focus();
this.navigate(this.getContainer(nodeFront));
}
},
/**
* Create a TreeWalker to find the next/previous
* node for selection.
*/
_selectionWalker: function (start) {
let walker = this.doc.createTreeWalker(
start || this._elt,
nodeFilterConstants.SHOW_ELEMENT,
function (element) {
if (element.container &&
element.container.elt === element &&
element.container.visible) {
return nodeFilterConstants.FILTER_ACCEPT;
}
return nodeFilterConstants.FILTER_SKIP;
}
);
walker.currentNode = this._selectedContainer.elt;
return walker;
},
_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();
},
/**
* Register all key shortcuts.
*/
_initShortcuts: function () {
let shortcuts = new KeyShortcuts({
window: this.win,
});
this._onShortcut = this._onShortcut.bind(this);
// Process localizable keys
["markupView.hide.key",
"markupView.edit.key",
"markupView.scrollInto.key"].forEach(name => {
let key = INSPECTOR_L10N.getStr(name);
shortcuts.on(key, (_, event) => this._onShortcut(name, event));
});
// Process generic keys:
["Delete", "Backspace", "Home", "Left", "Right", "Up", "Down", "PageUp",
"PageDown", "Esc", "Enter", "Space"].forEach(key => {
shortcuts.on(key, this._onShortcut);
});
},
/**
* Key shortcut listener.
*/
_onShortcut(name, event) {
if (this._isInputOrTextarea(event.target)) {
return;
}
switch (name) {
// Localizable keys
case "markupView.hide.key": {
let node = this._selectedContainer.node;
if (node.hidden) {
this.walker.unhideNode(node);
} else {
this.walker.hideNode(node);
}
break;
}
case "markupView.edit.key": {
this.beginEditingOuterHTML(this._selectedContainer.node);
break;
}
case "markupView.scrollInto.key": {
let selection = this._selectedContainer.node;
this.inspector.scrollNodeIntoView(selection);
break;
}
// Generic keys
case "Delete": {
this.deleteNodeOrAttribute();
break;
}
case "Backspace": {
this.deleteNodeOrAttribute(true);
break;
}
case "Home": {
let rootContainer = this.getContainer(this._rootNode);
this.navigate(rootContainer.children.firstChild.container);
break;
}
case "Left": {
if (this._selectedContainer.expanded) {
this.collapseNode(this._selectedContainer.node);
} else {
let parent = this._selectionWalker().parentNode();
if (parent) {
this.navigate(parent.container);
}
}
break;
}
case "Right": {
if (!this._selectedContainer.expanded &&
this._selectedContainer.hasChildren) {
this._expandContainer(this._selectedContainer);
} else {
let next = this._selectionWalker().nextNode();
if (next) {
this.navigate(next.container);
}
}
break;
}
case "Up": {
let previousNode = this._selectionWalker().previousNode();
if (previousNode) {
this.navigate(previousNode.container);
}
break;
}
case "Down": {
let nextNode = this._selectionWalker().nextNode();
if (nextNode) {
this.navigate(nextNode.container);
}
break;
}
case "PageUp": {
let walker = this._selectionWalker();
let selection = this._selectedContainer;
for (let i = 0; i < PAGE_SIZE; i++) {
let previousNode = walker.previousNode();
if (!previousNode) {
break;
}
selection = previousNode.container;
}
this.navigate(selection);
break;
}
case "PageDown": {
let walker = this._selectionWalker();
let selection = this._selectedContainer;
for (let i = 0; i < PAGE_SIZE; i++) {
let nextNode = walker.nextNode();
if (!nextNode) {
break;
}
selection = nextNode.container;
}
this.navigate(selection);
break;
}
case "Enter":
case "Space": {
if (!this._selectedContainer.canFocus) {
this._selectedContainer.canFocus = true;
this._selectedContainer.focus();
} else {
// Return early to prevent cancelling the event.
return;
}
break;
}
case "Esc": {
if (this.isDragging) {
this.cancelDragging();
} else {
// Return early to prevent cancelling the event when not
// dragging, to allow the split console to be toggled.
return;
}
break;
}
default:
console.error("Unexpected markup-view key shortcut", name);
return;
}
// Prevent default for this action
event.stopPropagation();
event.preventDefault();
},
/**
* Check if a node is an input or textarea
*/
_isInputOrTextarea: function (element) {
let name = element.tagName.toLowerCase();
return name === "input" || name === "textarea";
},
/**
* 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);
}
},
/**
* Delete a node from the DOM.
* This is an undoable action.
*
* @param {NodeFront} node
* The node to remove.
* @param {Boolean} moveBackward
* If set to true, focus the previous sibling, otherwise the next one.
*/
deleteNode: function (node, moveBackward) {
if (node.isDocumentElement ||
node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
node.isAnonymous) {
return;
}
let container = this.getContainer(node);
// Retain the node so we can undo this...
this.walker.retainNode(node).then(() => {
let parent = node.parentNode();
let nextSibling = null;
this.undo.do(() => {
this.walker.removeNode(node).then(siblings => {
nextSibling = siblings.nextSibling;
let prevSibling = siblings.previousSibling;
let focusNode = moveBackward ? prevSibling : 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 || prevSibling || parent;
}
let isNextSiblingText = nextSibling ?
nextSibling.nodeType === nodeConstants.TEXT_NODE : false;
let isPrevSiblingText = prevSibling ?
prevSibling.nodeType === nodeConstants.TEXT_NODE : false;
// If the parent had two children and the next or previous sibling
// is a text node, then it now has only a single text node, is about
// to be in-lined; and focus should move to the parent.
if (parent.numChildren === 2
&& (isNextSiblingText || isPrevSiblingText)) {
focusNode = parent;
}
if (container.selected) {
this.navigate(this.getContainer(focusNode));
}
});
}, () => {
let isValidSibling = nextSibling && !nextSibling.isPseudoElement;
nextSibling = isValidSibling ? nextSibling : null;
this.walker.insertBefore(node, parent, nextSibling);
});
}).then(null, console.error);
},
/**
* If an editable item is focused, select its container.
*/
_onFocus: function (event) {
let parent = event.target;
while (!parent.container) {
parent = parent.parentNode;
}
if (parent) {
this.navigate(parent.container);
}
},
/**
* Handle a user-requested navigation to a given MarkupContainer,
* updating the inspector's currently-selected node.
*
* @param {MarkupContainer} container
* The container we're navigating to.
*/
navigate: function (container) {
if (!container) {
return;
}
let node = container.node;
this.markNodeAsSelected(node, "treepanel");
},
/**
* Make sure a node is included in the markup tool.
*
* @param {NodeFront} node
* The node in the content document.
* @param {Boolean} flashNode
* Whether the newly imported node should be flashed
* @return {MarkupContainer} The MarkupContainer object for this element.
*/
importNode: function (node, flashNode) {
if (!node) {
return null;
}
if (this._containers.has(node)) {
return this.getContainer(node);
}
let container;
let {nodeType, isPseudoElement} = node;
if (node === this.walker.rootNode) {
container = new RootContainer(this, node);
this._elt.appendChild(container.elt);
this._rootNode = node;
} else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
container = new MarkupElementContainer(this, node, this.inspector);
} else if (nodeType == nodeConstants.COMMENT_NODE ||
nodeType == nodeConstants.TEXT_NODE) {
container = new MarkupTextContainer(this, node, this.inspector);
} else {
container = new MarkupReadOnlyContainer(this, node, this.inspector);
}
if (flashNode) {
container.flashMutation();
}
this._containers.set(node, container);
container.childrenDirty = true;
this._updateChildren(container);
this.inspector.emit("container-created", container);
return container;
},
/**
* Mutation observer used for included nodes.
*/
_mutationObserver: function (mutations) {
for (let mutation of mutations) {
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).
type = "childList";
target = mutation.targetParent;
if (!target) {
continue;
}
}
let container = this.getContainer(target);
if (!container) {
// Container might not exist if this came from a load event for a node
// we're not viewing.
continue;
}
if (type === "attributes" && mutation.attributeName === "class") {
container.updateIsDisplayed();
}
if (type === "attributes" || type === "characterData"
|| type === "events" || type === "pseudoClassLock") {
container.update();
} else if (type === "childList" || type === "nativeAnonymousChildList") {
container.childrenDirty = true;
// Update the children to take care of changes in the markup view DOM
// and update container (and its subtree) DOM tree depth level for
// accessibility where necessary.
this._updateChildren(container, {flash: true}).then(() =>
container.updateLevel());
} else if (type === "inlineTextChild") {
container.childrenDirty = true;
this._updateChildren(container, {flash: true});
container.update();
}
}
this._waitForChildren().then(() => {
if (this._destroyer) {
// Could not fully update after markup mutations, the markup-view was destroyed
// while waiting for children. Bail out silently.
return;
}
this._flashMutatedNodes(mutations);
this.inspector.emit("markupmutation", mutations);
// Since the htmlEditor is absolutely positioned, a mutation may change
// the location in which it should be shown.
this.htmlEditor.refresh();
});
},
/**
* React to display-change events from the walker
*
* @param {Array} nodes
* An array of nodeFronts
*/
_onDisplayChange: function (nodes) {
for (let node of nodes) {
let container = this.getContainer(node);
if (container) {
container.updateIsDisplayed();
}
}
},
/**
* Given a list of mutations returned by the mutation observer, flash the
* corresponding containers to attract attention.
*/
_flashMutatedNodes: function (mutations) {
let addedOrEditedContainers = new Set();
let removedContainers = new Set();
for (let {type, target, added, removed, newValue} of mutations) {
let container = this.getContainer(target);
if (container) {
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.
addedOrEditedContainers.add(container);
} else if (type === "childList") {
// If there has been removals, flash the parent
if (removed.length) {
removedContainers.add(container);
}
// If there has been additions, flash the nodes if their associated
// container exist (so if their parent is expanded in the inspector).
added.forEach(node => {
let addedContainer = this.getContainer(node);
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);
}
});
}
}
}
for (let container of removedContainers) {
container.flashMutation();
}
for (let container of addedOrEditedContainers) {
container.flashMutation();
}
},
/**
* Make sure the given node's parents are expanded and the
* node is scrolled on to screen.
*/
showNode: function (node, centered = true) {
let parent = node;
this.importNode(node);
while ((parent = parent.parentNode())) {
this.importNode(parent);
this.expandNode(parent);
}
return this._waitForChildren().then(() => {
if (this._destroyer) {
return promise.reject("markupview destroyed");
}
return this._ensureVisible(node);
}).then(() => {
scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered);
}, this._handleRejectionIfNotDestroyed);
},
/**
* Expand the container's children.
*/
_expandContainer: function (container) {
return this._updateChildren(container, {expand: true}).then(() => {
if (this._destroyer) {
// Could not expand the node, the markup-view was destroyed in the meantime. Just
// silently give up.
return;
}
container.setExpanded(true);
});
},
/**
* Expand the node's children.
*/
expandNode: function (node) {
let container = this.getContainer(node);
this._expandContainer(container);
},
/**
* Expand the entire tree beneath a container.
*
* @param {MarkupContainer} container
* The container to expand.
*/
_expandAll: function (container) {
return this._expandContainer(container).then(() => {
let child = container.children.firstChild;
let promises = [];
while (child) {
promises.push(this._expandAll(child.container));
child = child.nextSibling;
}
return promise.all(promises);
}).then(null, console.error);
},
/**
* Expand the entire tree beneath a node.
*
* @param {DOMNode} node
* The node to expand, or null to start from the top.
*/
expandAll: function (node) {
node = node || this._rootNode;
return this._expandAll(this.getContainer(node));
},
/**
* Collapse the node's children.
*/
collapseNode: function (node) {
let container = this.getContainer(node);
container.setExpanded(false);
},
/**
* Returns either the innerHTML or the outerHTML for a remote node.
*
* @param {NodeFront} node
* The NodeFront to get the outerHTML / innerHTML for.
* @param {Boolean} isOuter
* If true, makes the function return the outerHTML,
* otherwise the innerHTML.
* @return {Promise} that will be resolved with the outerHTML / innerHTML.
*/
_getNodeHTML: function (node, isOuter) {
let walkerPromise = null;
if (isOuter) {
walkerPromise = this.walker.outerHTML(node);
} else {
walkerPromise = this.walker.innerHTML(node);
}
return walkerPromise.then(longstr => {
return longstr.string().then(html => {
longstr.release().then(null, console.error);
return html;
});
});
},
/**
* Retrieve the outerHTML for a remote node.
*
* @param {NodeFront} node
* The NodeFront to get the outerHTML for.
* @return {Promise} that will be resolved with the outerHTML.
*/
getNodeOuterHTML: function (node) {
return this._getNodeHTML(node, true);
},
/**
* Retrieve the innerHTML for a remote node.
*
* @param {NodeFront} node
* The NodeFront to get the innerHTML for.
* @return {Promise} that will be resolved with the innerHTML.
*/
getNodeInnerHTML: function (node) {
return this._getNodeHTML(node);
},
/**
* 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.
*/
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;
}
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");
}
}
};
// 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");
}
},
/**
* Replace the outerHTML of any node displayed in the inspector with
* some other HTML code
*
* @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.
* @return {Promise} that will resolve when the outer HTML has been updated.
*/
updateNodeOuterHTML: function (node, newValue) {
let container = this.getContainer(node);
if (!container) {
return promise.reject();
}
// Changing the outerHTML removes the node which outerHTML was changed.
// Listen to this removal to reselect the right node afterwards.
this.reselectOnRemoved(node, "outerhtml");
return this.walker.setOuterHTML(node, newValue).then(null, () => {
this.cancelReselectOnRemoved();
});
},
/**
* 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.
* @return {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 = 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.
* @return {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 = 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;
},
/**
* Open an editor in the UI to allow editing of a node's outerHTML.
*
* @param {NodeFront} node
* The NodeFront to edit.
*/
beginEditingOuterHTML: function (node) {
this.getNodeOuterHTML(node).then(oldValue => {
let container = this.getContainer(node);
if (!container) {
return;
}
this.htmlEditor.show(container.tagLine, oldValue);
this.htmlEditor.once("popuphidden", (e, commit, value) => {
// Need to focus the <html> element instead of the frame / window
// in order to give keyboard focus back to doc (from editor).
this.doc.documentElement.focus();
if (commit) {
this.updateNodeOuterHTML(node, value, oldValue);
}
});
});
},
/**
* Mark the given node expanded.
*
* @param {NodeFront} node
* The NodeFront to mark as expanded.
* @param {Boolean} expanded
* Whether the expand or collapse.
* @param {Boolean} expandDescendants
* Whether to expand all descendants too
*/
setNodeExpanded: function (node, expanded, expandDescendants) {
if (expanded) {
if (expandDescendants) {
this.expandAll(node);
} else {
this.expandNode(node);
}
} else {
this.collapseNode(node);
}
},
/**
* Mark the given node selected, and update the inspector.selection
* object's NodeFront to keep consistent state between UI and selection.
*
* @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.
*/
markNodeAsSelected: function (node, reason) {
let container = this.getContainer(node);
if (this._selectedContainer === container) {
return false;
}
// Un-select and remove focus from the previous container.
if (this._selectedContainer) {
this._selectedContainer.selected = false;
this._selectedContainer.clearFocus();
}
// Select the new container.
this._selectedContainer = container;
if (node) {
this._selectedContainer.selected = true;
}
// Change the current selection if needed.
if (this.inspector.selection.nodeFront !== node) {
this.inspector.selection.setNodeFront(node, reason || "nodeselected");
}
return true;
},
/**
* Make sure that every ancestor of the selection are updated
* and included in the list of visible children.
*/
_ensureVisible: function (node) {
while (node) {
let container = this.getContainer(node);
let parent = node.parentNode();
if (!container.elt.parentNode) {
let parentContainer = this.getContainer(parent);
if (parentContainer) {
parentContainer.childrenDirty = true;
this._updateChildren(parentContainer, {expand: true});
}
}
node = parent;
}
return this._waitForChildren();
},
/**
* Unmark selected node (no node selected).
*/
unmarkSelectedNode: function () {
if (this._selectedContainer) {
this._selectedContainer.selected = false;
this._selectedContainer = null;
}
},
/**
* 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.
*
* @return The node that should be made visible, if any.
*/
_checkSelectionVisible: function (container) {
let centered = null;
let node = this.inspector.selection.nodeFront;
while (node) {
if (node.parentNode() === container.node) {
centered = node;
break;
}
node = node.parentNode();
}
return centered;
},
/**
* Make sure all children of the given container's node are
* imported and attached to the container in the right order.
*
* 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!
*
* @param {MarkupContainer} container
* The markup container whose children need updating
* @param {Object} options
* Options are {expand:boolean,flash:boolean}
* @return {Promise} that will be resolved when the children are ready
* (which may be immediately).
*/
_updateChildren: function (container, options) {
let expand = options && options.expand;
let flash = options && options.flash;
container.hasChildren = container.node.hasChildren;
// Accessibility should either ignore empty children or semantically
// consider them a group.
container.setChildrenRole();
if (!this._queuedChildUpdates) {
this._queuedChildUpdates = new Map();
}
if (this._queuedChildUpdates.has(container)) {
return this._queuedChildUpdates.get(container);
}
if (!container.childrenDirty) {
return promise.resolve(container);
}
if (container.inlineTextChild
&& container.inlineTextChild != container.node.inlineTextChild) {
// This container was doing double duty as a container for a single
// text child, back that out.
this._containers.delete(container.inlineTextChild);
container.clearInlineTextChild();
if (container.hasChildren && container.selected) {
container.setExpanded(true);
}
}
if (container.node.inlineTextChild) {
container.setExpanded(false);
// this container will do double duty as the container for the single
// text child.
while (container.children.firstChild) {
container.children.removeChild(container.children.firstChild);
}
container.setInlineTextChild(container.node.inlineTextChild);
this._containers.set(container.node.inlineTextChild, container);
container.childrenDirty = false;
return promise.resolve(container);
}
if (!container.hasChildren) {
while (container.children.firstChild) {
container.children.removeChild(container.children.firstChild);
}
container.childrenDirty = false;
container.setExpanded(false);
return promise.resolve(container);
}
// 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.
if (!(container.expanded || expand)) {
return promise.resolve(container);
}
// We're going to issue a children request, make sure it includes the
// centered node.
let centered = this._checkSelectionVisible(container);
// 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.
container.childrenDirty = false;
let updatePromise =
this._getVisibleChildren(container, centered).then(children => {
if (!this._containers) {
return promise.reject("markup view destroyed");
}
this._queuedChildUpdates.delete(container);
// 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 (container.childrenDirty) {
return this._updateChildren(container, {expand: centered});
}
let fragment = this.doc.createDocumentFragment();
for (let child of children.nodes) {
let childContainer = this.importNode(child, flash);
fragment.appendChild(childContainer.elt);
}
while (container.children.firstChild) {
container.children.removeChild(container.children.firstChild);
}
if (!(children.hasFirst && children.hasLast)) {
let nodesCount = container.node.numChildren;
let showAllString = PluralForm.get(nodesCount,
INSPECTOR_L10N.getStr("markupView.more.showAll2"));
let data = {
showing: INSPECTOR_L10N.getStr("markupView.more.showing"),
showAll: showAllString.replace("#1", nodesCount),
allButtonClick: () => {
container.maxChildren = -1;
container.childrenDirty = true;
this._updateChildren(container);
}
};
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);
}
}
container.children.appendChild(fragment);
return container;
}).catch(this._handleRejectionIfNotDestroyed);
this._queuedChildUpdates.set(container, updatePromise);
return updatePromise;
},
_waitForChildren: function () {
if (!this._queuedChildUpdates) {
return promise.resolve(undefined);
}
return promise.all([...this._queuedChildUpdates.values()]);
},
/**
* Return a list of the children to display for this container.
*/
_getVisibleChildren: function (container, centered) {
let maxChildren = container.maxChildren || this.maxChildren;
if (maxChildren == -1) {
maxChildren = undefined;
}
return this.walker.children(container.node, {
maxNodes: maxChildren,
center: centered
});
},
/**
* Tear down the markup panel.
*/
destroy: function () {
if (this._destroyer) {
return this._destroyer;
}
this._destroyer = promise.resolve();
this._clearBriefBoxModelTimer();
this._hoveredNode = null;
this.htmlEditor.destroy();
this.htmlEditor = null;
this.undo.destroy();
this.undo = null;
this.popup.destroy();
this.popup = null;
this._elt.removeEventListener("click", this._onMouseClick, false);
this._elt.removeEventListener("mousemove", this._onMouseMove, false);
this._elt.removeEventListener("mouseout", this._onMouseOut, false);
this._elt.removeEventListener("blur", this._onBlur, true);
this.win.removeEventListener("mouseup", this._onMouseUp);
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.toolbox.off("picker-node-hovered",
this._onToolboxPickerHover);
this._prefObserver.off(ATTR_COLLAPSE_ENABLED_PREF,
this._onCollapseAttributesPrefChange);
this._prefObserver.off(ATTR_COLLAPSE_LENGTH_PREF,
this._onCollapseAttributesPrefChange);
this._prefObserver.destroy();
this._elt = null;
for (let [, container] of this._containers) {
container.destroy();
}
this._containers = null;
this.eventDetailsTooltip.destroy();
this.eventDetailsTooltip = null;
this.imagePreviewTooltip.destroy();
this.imagePreviewTooltip = null;
this.win = null;
this.doc = null;
this._lastDropTarget = null;
this._lastDragTarget = null;
return this._destroyer;
},
/**
* 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");
},
/**
* 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");
}
if (!el) {
return;
}
let target = this.findClosestDragDropTarget(el);
if (target) {
target.classList.add("drop-target");
this._lastDropTarget = target;
}
},
/**
* 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");
}
if (!el) {
return;
}
let target = this.findClosestDragDropTarget(el);
if (target) {
target.classList.add("drag-target");
this._lastDragTarget = target;
}
},
/**
* Used to get the nodes required to modify the markup after dragging the
* element (parent/nextSibling).
*/
get dropTargetNodes() {
let target = this._lastDropTarget;
if (!target) {
return null;
}
let parent, nextSibling;
if (target.previousElementSibling &&
target.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 !== nodeConstants.ELEMENT_NODE) {
return null;
}
return {parent, nextSibling};
}
};
/**
* 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);
}
module.exports = MarkupView;