mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
Expanding an ElementsTreeElement used to immediately set a populated flag to true before requesting getChildNodes(). If a ChildNodeCountUpdated event arrives before the children are set, we may call updateModifiedParentNode() and try to create visibleChildren when they are null, breaking the tree. This CL moves the place where 'populated' is set, so that updateModifiedParentNode() does not update tree children before they are available. Bug: 829884 Change-Id: Id68e71fb7a58336d781207b298a53354af3e5cfc Reviewed-on: https://chromium-review.googlesource.com/1049113 Commit-Queue: Erik Luo <luoe@chromium.org> Reviewed-by: Andrey Lushnikov <lushnikov@chromium.org> Reviewed-by: Joel Einbinder <einbinder@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#557132} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 2e5ad0a88b7d04fe1aac8b2fd0c26b2d3475e4c2
1674 lines
54 KiB
JavaScript
1674 lines
54 KiB
JavaScript
/*
|
|
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
Elements.ElementsTreeElement = class extends UI.TreeElement {
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @param {boolean=} elementCloseTag
|
|
*/
|
|
constructor(node, elementCloseTag) {
|
|
// The title will be updated in onattach.
|
|
super();
|
|
this._node = node;
|
|
|
|
this._gutterContainer = this.listItemElement.createChild('div', 'gutter-container');
|
|
this._gutterContainer.addEventListener('click', this._showContextMenu.bind(this));
|
|
const gutterMenuIcon = UI.Icon.create('largeicon-menu', 'gutter-menu-icon');
|
|
this._gutterContainer.appendChild(gutterMenuIcon);
|
|
this._decorationsElement = this._gutterContainer.createChild('div', 'hidden');
|
|
|
|
this._elementCloseTag = elementCloseTag;
|
|
|
|
if (this._node.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
|
|
this._canAddAttributes = true;
|
|
this._searchQuery = null;
|
|
this._expandedChildrenLimit = Elements.ElementsTreeElement.InitialChildrenLimit;
|
|
this._decorationsThrottler = new Common.Throttler(100);
|
|
}
|
|
|
|
/**
|
|
* @param {!Elements.ElementsTreeElement} treeElement
|
|
*/
|
|
static animateOnDOMUpdate(treeElement) {
|
|
const tagName = treeElement.listItemElement.querySelector('.webkit-html-tag-name');
|
|
UI.runCSSAnimationOnce(tagName || treeElement.listItemElement, 'dom-update-highlight');
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @return {!Array<!SDK.DOMNode>}
|
|
*/
|
|
static visibleShadowRoots(node) {
|
|
let roots = node.shadowRoots();
|
|
if (roots.length && !Common.moduleSetting('showUAShadowDOM').get())
|
|
roots = roots.filter(filter);
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} root
|
|
*/
|
|
function filter(root) {
|
|
return root.shadowRootType() !== SDK.DOMNode.ShadowRootTypes.UserAgent;
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @return {boolean}
|
|
*/
|
|
static canShowInlineText(node) {
|
|
if (node.contentDocument() || node.importedDocument() || node.templateContent() ||
|
|
Elements.ElementsTreeElement.visibleShadowRoots(node).length || node.hasPseudoElements())
|
|
return false;
|
|
if (node.nodeType() !== Node.ELEMENT_NODE)
|
|
return false;
|
|
if (!node.firstChild || node.firstChild !== node.lastChild || node.firstChild.nodeType() !== Node.TEXT_NODE)
|
|
return false;
|
|
const textChild = node.firstChild;
|
|
const maxInlineTextChildLength = 80;
|
|
if (textChild.nodeValue().length < maxInlineTextChildLength)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
* @param {!SDK.DOMNode} node
|
|
* @suppressGlobalPropertiesCheck
|
|
*/
|
|
static populateForcedPseudoStateItems(contextMenu, node) {
|
|
const pseudoClasses = ['active', 'hover', 'focus', 'visited', 'focus-within'];
|
|
try {
|
|
document.querySelector(':focus-visible'); // Will throw if not supported
|
|
pseudoClasses.push('focus-visible');
|
|
} catch (e) {
|
|
}
|
|
const forcedPseudoState = node.domModel().cssModel().pseudoState(node);
|
|
const stateMenu = contextMenu.debugSection().appendSubMenuItem(Common.UIString('Force state'));
|
|
for (let i = 0; i < pseudoClasses.length; ++i) {
|
|
const pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
|
|
stateMenu.defaultSection().appendCheckboxItem(
|
|
':' + pseudoClasses[i], setPseudoStateCallback.bind(null, pseudoClasses[i], !pseudoClassForced),
|
|
pseudoClassForced, false);
|
|
}
|
|
|
|
/**
|
|
* @param {string} pseudoState
|
|
* @param {boolean} enabled
|
|
*/
|
|
function setPseudoStateCallback(pseudoState, enabled) {
|
|
node.domModel().cssModel().forcePseudoState(node, pseudoState, enabled);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isClosingTag() {
|
|
return !!this._elementCloseTag;
|
|
}
|
|
|
|
/**
|
|
* @return {!SDK.DOMNode}
|
|
*/
|
|
node() {
|
|
return this._node;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isEditing() {
|
|
return !!this._editing;
|
|
}
|
|
|
|
/**
|
|
* @param {string} searchQuery
|
|
*/
|
|
highlightSearchResults(searchQuery) {
|
|
if (this._searchQuery !== searchQuery)
|
|
this._hideSearchHighlight();
|
|
|
|
this._searchQuery = searchQuery;
|
|
this._searchHighlightsVisible = true;
|
|
this.updateTitle(null, true);
|
|
}
|
|
|
|
hideSearchHighlights() {
|
|
delete this._searchHighlightsVisible;
|
|
this._hideSearchHighlight();
|
|
}
|
|
|
|
_hideSearchHighlight() {
|
|
if (!this._highlightResult)
|
|
return;
|
|
|
|
function updateEntryHide(entry) {
|
|
switch (entry.type) {
|
|
case 'added':
|
|
entry.node.remove();
|
|
break;
|
|
case 'changed':
|
|
entry.node.textContent = entry.oldText;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (let i = (this._highlightResult.length - 1); i >= 0; --i)
|
|
updateEntryHide(this._highlightResult[i]);
|
|
|
|
delete this._highlightResult;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} inClipboard
|
|
*/
|
|
setInClipboard(inClipboard) {
|
|
if (this._inClipboard === inClipboard)
|
|
return;
|
|
this._inClipboard = inClipboard;
|
|
this.listItemElement.classList.toggle('in-clipboard', inClipboard);
|
|
}
|
|
|
|
get hovered() {
|
|
return this._hovered;
|
|
}
|
|
|
|
set hovered(x) {
|
|
if (this._hovered === x)
|
|
return;
|
|
|
|
this._hovered = x;
|
|
|
|
if (this.listItemElement) {
|
|
if (x) {
|
|
this._createSelection();
|
|
this.listItemElement.classList.add('hovered');
|
|
} else {
|
|
this.listItemElement.classList.remove('hovered');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
expandedChildrenLimit() {
|
|
return this._expandedChildrenLimit;
|
|
}
|
|
|
|
/**
|
|
* @param {number} expandedChildrenLimit
|
|
*/
|
|
setExpandedChildrenLimit(expandedChildrenLimit) {
|
|
this._expandedChildrenLimit = expandedChildrenLimit;
|
|
}
|
|
|
|
_createSelection() {
|
|
const listItemElement = this.listItemElement;
|
|
if (!listItemElement)
|
|
return;
|
|
|
|
if (!this.selectionElement) {
|
|
this.selectionElement = createElement('div');
|
|
this.selectionElement.className = 'selection fill';
|
|
this.selectionElement.style.setProperty('margin-left', (-this._computeLeftIndent()) + 'px');
|
|
listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
|
|
}
|
|
}
|
|
|
|
_createHint() {
|
|
if (this.listItemElement && !this._hintElement) {
|
|
this._hintElement = this.listItemElement.createChild('span', 'selected-hint');
|
|
this._hintElement.title = Common.UIString('Use $0 in the console to refer to this element.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onbind() {
|
|
if (!this._elementCloseTag)
|
|
this._node[this.treeOutline.treeElementSymbol()] = this;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onunbind() {
|
|
if (this._node[this.treeOutline.treeElementSymbol()] === this)
|
|
this._node[this.treeOutline.treeElementSymbol()] = null;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onattach() {
|
|
if (this._hovered) {
|
|
this._createSelection();
|
|
this.listItemElement.classList.add('hovered');
|
|
}
|
|
|
|
this.updateTitle();
|
|
this.listItemElement.draggable = true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onpopulate() {
|
|
this.treeOutline.populateTreeElement(this);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
expandRecursively() {
|
|
this._node.getSubtree(-1, true).then(UI.TreeElement.prototype.expandRecursively.bind(this, Number.MAX_VALUE));
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onexpand() {
|
|
if (this._elementCloseTag)
|
|
return;
|
|
|
|
this.updateTitle();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
oncollapse() {
|
|
if (this._elementCloseTag)
|
|
return;
|
|
|
|
this.updateTitle();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {boolean=} omitFocus
|
|
* @param {boolean=} selectedByUser
|
|
* @return {boolean}
|
|
*/
|
|
select(omitFocus, selectedByUser) {
|
|
if (this._editing)
|
|
return false;
|
|
return super.select(omitFocus, selectedByUser);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {boolean=} selectedByUser
|
|
* @return {boolean}
|
|
*/
|
|
onselect(selectedByUser) {
|
|
this.treeOutline.suppressRevealAndSelect = true;
|
|
this.treeOutline.selectDOMNode(this._node, selectedByUser);
|
|
if (selectedByUser) {
|
|
this._node.highlight();
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ChangeInspectedNodeInElementsPanel);
|
|
}
|
|
this._createSelection();
|
|
this._createHint();
|
|
this.treeOutline.suppressRevealAndSelect = false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {boolean}
|
|
*/
|
|
ondelete() {
|
|
const startTagTreeElement = this.treeOutline.findTreeElement(this._node);
|
|
startTagTreeElement ? startTagTreeElement.remove() : this.remove();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {boolean}
|
|
*/
|
|
onenter() {
|
|
// On Enter or Return start editing the first attribute
|
|
// or create a new attribute on the selected element.
|
|
if (this._editing)
|
|
return false;
|
|
|
|
this._startEditing();
|
|
|
|
// prevent a newline from being immediately inserted
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
selectOnMouseDown(event) {
|
|
super.selectOnMouseDown(event);
|
|
|
|
if (this._editing)
|
|
return;
|
|
|
|
// Prevent selecting the nearest word on double click.
|
|
if (event.detail >= 2)
|
|
event.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {boolean}
|
|
*/
|
|
ondblclick(event) {
|
|
if (this._editing || this._elementCloseTag)
|
|
return false;
|
|
|
|
if (this._startEditingTarget(/** @type {!Element} */ (event.target)))
|
|
return false;
|
|
|
|
if (this.isExpandable() && !this.expanded)
|
|
this.expand();
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
hasEditableNode() {
|
|
return !this._node.isShadowRoot() && !this._node.ancestorUserAgentShadowRoot();
|
|
}
|
|
|
|
_insertInLastAttributePosition(tag, node) {
|
|
if (tag.getElementsByClassName('webkit-html-attribute').length > 0) {
|
|
tag.insertBefore(node, tag.lastChild);
|
|
} else {
|
|
const nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
|
|
tag.textContent = '';
|
|
tag.createTextChild('<' + nodeName);
|
|
tag.appendChild(node);
|
|
tag.createTextChild('>');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} eventTarget
|
|
* @return {boolean}
|
|
*/
|
|
_startEditingTarget(eventTarget) {
|
|
if (this.treeOutline.selectedDOMNode() !== this._node)
|
|
return false;
|
|
|
|
if (this._node.nodeType() !== Node.ELEMENT_NODE && this._node.nodeType() !== Node.TEXT_NODE)
|
|
return false;
|
|
|
|
const textNode = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-text-node');
|
|
if (textNode)
|
|
return this._startEditingTextNode(textNode);
|
|
|
|
const attribute = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-attribute');
|
|
if (attribute)
|
|
return this._startEditingAttribute(attribute, eventTarget);
|
|
|
|
const tagName = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-tag-name');
|
|
if (tagName)
|
|
return this._startEditingTagName(tagName);
|
|
|
|
const newAttribute = eventTarget.enclosingNodeOrSelfWithClass('add-attribute');
|
|
if (newAttribute)
|
|
return this._addNewAttribute();
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_showContextMenu(event) {
|
|
this.treeOutline.showContextMenu(this, event);
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
* @param {!Event} event
|
|
*/
|
|
populateTagContextMenu(contextMenu, event) {
|
|
// Add attribute-related actions.
|
|
const treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this;
|
|
contextMenu.editSection().appendItem(
|
|
Common.UIString('Add attribute'), treeElement._addNewAttribute.bind(treeElement));
|
|
|
|
const attribute = event.target.enclosingNodeOrSelfWithClass('webkit-html-attribute');
|
|
const newAttribute = event.target.enclosingNodeOrSelfWithClass('add-attribute');
|
|
if (attribute && !newAttribute) {
|
|
contextMenu.editSection().appendItem(
|
|
Common.UIString('Edit attribute'), this._startEditingAttribute.bind(this, attribute, event.target));
|
|
}
|
|
this.populateNodeContextMenu(contextMenu);
|
|
Elements.ElementsTreeElement.populateForcedPseudoStateItems(contextMenu, treeElement.node());
|
|
this.populateScrollIntoView(contextMenu);
|
|
contextMenu.viewSection().appendItem(Common.UIString('Focus'), async () => {
|
|
await this._node.focus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
*/
|
|
populateScrollIntoView(contextMenu) {
|
|
contextMenu.viewSection().appendItem(Common.UIString('Scroll into view'), () => this._node.scrollIntoView());
|
|
}
|
|
|
|
populateTextContextMenu(contextMenu, textNode) {
|
|
if (!this._editing) {
|
|
contextMenu.editSection().appendItem(
|
|
Common.UIString('Edit text'), this._startEditingTextNode.bind(this, textNode));
|
|
}
|
|
this.populateNodeContextMenu(contextMenu);
|
|
}
|
|
|
|
populateNodeContextMenu(contextMenu) {
|
|
// Add free-form node-related actions.
|
|
const isEditable = this.hasEditableNode();
|
|
if (isEditable && !this._editing)
|
|
contextMenu.editSection().appendItem(Common.UIString('Edit as HTML'), this._editAsHTML.bind(this));
|
|
const isShadowRoot = this._node.isShadowRoot();
|
|
|
|
// Place it here so that all "Copy"-ing items stick together.
|
|
const copyMenu = contextMenu.clipboardSection().appendSubMenuItem(Common.UIString('Copy'));
|
|
const createShortcut = UI.KeyboardShortcut.shortcutToString;
|
|
const modifier = UI.KeyboardShortcut.Modifiers.CtrlOrMeta;
|
|
const treeOutline = this.treeOutline;
|
|
let menuItem;
|
|
let section;
|
|
if (!isShadowRoot) {
|
|
section = copyMenu.section();
|
|
menuItem = section.appendItem(
|
|
Common.UIString('Copy outerHTML'), treeOutline.performCopyOrCut.bind(treeOutline, false, this._node));
|
|
menuItem.setShortcut(createShortcut('V', modifier));
|
|
}
|
|
if (this._node.nodeType() === Node.ELEMENT_NODE)
|
|
section.appendItem(Common.UIString('Copy selector'), this._copyCSSPath.bind(this));
|
|
if (!isShadowRoot)
|
|
section.appendItem(Common.UIString('Copy XPath'), this._copyXPath.bind(this));
|
|
if (!isShadowRoot) {
|
|
menuItem = copyMenu.clipboardSection().appendItem(
|
|
Common.UIString('Cut element'), treeOutline.performCopyOrCut.bind(treeOutline, true, this._node),
|
|
!this.hasEditableNode());
|
|
menuItem.setShortcut(createShortcut('X', modifier));
|
|
menuItem = copyMenu.clipboardSection().appendItem(
|
|
Common.UIString('Copy element'), treeOutline.performCopyOrCut.bind(treeOutline, false, this._node));
|
|
menuItem.setShortcut(createShortcut('C', modifier));
|
|
menuItem = copyMenu.clipboardSection().appendItem(
|
|
Common.UIString('Paste element'), treeOutline.pasteNode.bind(treeOutline, this._node),
|
|
!treeOutline.canPaste(this._node));
|
|
menuItem.setShortcut(createShortcut('V', modifier));
|
|
}
|
|
|
|
menuItem = contextMenu.debugSection().appendCheckboxItem(
|
|
Common.UIString('Hide element'), treeOutline.toggleHideElement.bind(treeOutline, this._node),
|
|
treeOutline.isToggledToHidden(this._node));
|
|
menuItem.setShortcut(UI.shortcutRegistry.shortcutTitleForAction('elements.hide-element'));
|
|
|
|
if (isEditable)
|
|
contextMenu.editSection().appendItem(Common.UIString('Delete element'), this.remove.bind(this));
|
|
|
|
contextMenu.viewSection().appendItem(ls`Expand recursively`, this.expandRecursively.bind(this));
|
|
contextMenu.viewSection().appendItem(ls`Collapse children`, this.collapseChildren.bind(this));
|
|
}
|
|
|
|
_startEditing() {
|
|
if (this.treeOutline.selectedDOMNode() !== this._node)
|
|
return;
|
|
|
|
const listItem = this.listItemElement;
|
|
|
|
if (this._canAddAttributes) {
|
|
const attribute = listItem.getElementsByClassName('webkit-html-attribute')[0];
|
|
if (attribute) {
|
|
return this._startEditingAttribute(
|
|
attribute, attribute.getElementsByClassName('webkit-html-attribute-value')[0]);
|
|
}
|
|
|
|
return this._addNewAttribute();
|
|
}
|
|
|
|
if (this._node.nodeType() === Node.TEXT_NODE) {
|
|
const textNode = listItem.getElementsByClassName('webkit-html-text-node')[0];
|
|
if (textNode)
|
|
return this._startEditingTextNode(textNode);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_addNewAttribute() {
|
|
// Cannot just convert the textual html into an element without
|
|
// a parent node. Use a temporary span container for the HTML.
|
|
const container = createElement('span');
|
|
this._buildAttributeDOM(container, ' ', '', null);
|
|
const attr = container.firstElementChild;
|
|
attr.style.marginLeft = '2px'; // overrides the .editing margin rule
|
|
attr.style.marginRight = '2px'; // overrides the .editing margin rule
|
|
|
|
const tag = this.listItemElement.getElementsByClassName('webkit-html-tag')[0];
|
|
this._insertInLastAttributePosition(tag, attr);
|
|
attr.scrollIntoViewIfNeeded(true);
|
|
return this._startEditingAttribute(attr, attr);
|
|
}
|
|
|
|
_triggerEditAttribute(attributeName) {
|
|
const attributeElements = this.listItemElement.getElementsByClassName('webkit-html-attribute-name');
|
|
for (let i = 0, len = attributeElements.length; i < len; ++i) {
|
|
if (attributeElements[i].textContent === attributeName) {
|
|
for (let elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
|
|
if (elem.nodeType !== Node.ELEMENT_NODE)
|
|
continue;
|
|
|
|
if (elem.classList.contains('webkit-html-attribute-value'))
|
|
return this._startEditingAttribute(elem.parentNode, elem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_startEditingAttribute(attribute, elementForSelection) {
|
|
console.assert(this.listItemElement.isAncestor(attribute));
|
|
|
|
if (UI.isBeingEdited(attribute))
|
|
return true;
|
|
|
|
const attributeNameElement = attribute.getElementsByClassName('webkit-html-attribute-name')[0];
|
|
if (!attributeNameElement)
|
|
return false;
|
|
|
|
const attributeName = attributeNameElement.textContent;
|
|
const attributeValueElement = attribute.getElementsByClassName('webkit-html-attribute-value')[0];
|
|
|
|
// Make sure elementForSelection is not a child of attributeValueElement.
|
|
elementForSelection =
|
|
attributeValueElement.isAncestor(elementForSelection) ? attributeValueElement : elementForSelection;
|
|
|
|
function removeZeroWidthSpaceRecursive(node) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
node.nodeValue = node.nodeValue.replace(/\u200B/g, '');
|
|
return;
|
|
}
|
|
|
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
for (let child = node.firstChild; child; child = child.nextSibling)
|
|
removeZeroWidthSpaceRecursive(child);
|
|
}
|
|
|
|
const attributeValue = attributeName && attributeValueElement ? this._node.getAttribute(attributeName) : undefined;
|
|
if (attributeValue !== undefined) {
|
|
attributeValueElement.setTextContentTruncatedIfNeeded(
|
|
attributeValue, Common.UIString('<value is too large to edit>'));
|
|
}
|
|
|
|
// Remove zero-width spaces that were added by nodeTitleInfo.
|
|
removeZeroWidthSpaceRecursive(attribute);
|
|
|
|
const config = new UI.InplaceEditor.Config(
|
|
this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @return {string}
|
|
*/
|
|
function postKeyDownFinishHandler(event) {
|
|
UI.handleElementValueModifications(event, attribute);
|
|
return '';
|
|
}
|
|
|
|
if (!attributeValueElement.textContent.asParsedURL())
|
|
config.setPostKeydownFinishHandler(postKeyDownFinishHandler);
|
|
|
|
this._editing = UI.InplaceEditor.startEditing(attribute, config);
|
|
|
|
this.listItemElement.getComponentSelection().selectAllChildren(elementForSelection);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} textNodeElement
|
|
*/
|
|
_startEditingTextNode(textNodeElement) {
|
|
if (UI.isBeingEdited(textNodeElement))
|
|
return true;
|
|
|
|
let textNode = this._node;
|
|
// We only show text nodes inline in elements if the element only
|
|
// has a single child, and that child is a text node.
|
|
if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
|
|
textNode = textNode.firstChild;
|
|
|
|
const container = textNodeElement.enclosingNodeOrSelfWithClass('webkit-html-text-node');
|
|
if (container)
|
|
container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
|
|
const config = new UI.InplaceEditor.Config(
|
|
this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
|
|
this._editing = UI.InplaceEditor.startEditing(textNodeElement, config);
|
|
this.listItemElement.getComponentSelection().selectAllChildren(textNodeElement);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element=} tagNameElement
|
|
*/
|
|
_startEditingTagName(tagNameElement) {
|
|
if (!tagNameElement) {
|
|
tagNameElement = this.listItemElement.getElementsByClassName('webkit-html-tag-name')[0];
|
|
if (!tagNameElement)
|
|
return false;
|
|
}
|
|
|
|
const tagName = tagNameElement.textContent;
|
|
if (Elements.ElementsTreeElement.EditTagBlacklist.has(tagName.toLowerCase()))
|
|
return false;
|
|
|
|
if (UI.isBeingEdited(tagNameElement))
|
|
return true;
|
|
|
|
const closingTagElement = this._distinctClosingTagElement();
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
function keyupListener(event) {
|
|
if (closingTagElement)
|
|
closingTagElement.textContent = '</' + tagNameElement.textContent + '>';
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
const keydownListener = event => {
|
|
if (event.key !== ' ')
|
|
return;
|
|
this._editing.commit();
|
|
event.consume(true);
|
|
};
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {string} newTagName
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function editingComitted(element, newTagName) {
|
|
tagNameElement.removeEventListener('keyup', keyupListener, false);
|
|
tagNameElement.removeEventListener('keydown', keydownListener, false);
|
|
this._tagNameEditingCommitted.apply(this, arguments);
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function editingCancelled() {
|
|
tagNameElement.removeEventListener('keyup', keyupListener, false);
|
|
tagNameElement.removeEventListener('keydown', keydownListener, false);
|
|
this._editingCancelled.apply(this, arguments);
|
|
}
|
|
|
|
tagNameElement.addEventListener('keyup', keyupListener, false);
|
|
tagNameElement.addEventListener('keydown', keydownListener, false);
|
|
|
|
const config = new UI.InplaceEditor.Config(editingComitted.bind(this), editingCancelled.bind(this), tagName);
|
|
this._editing = UI.InplaceEditor.startEditing(tagNameElement, config);
|
|
this.listItemElement.getComponentSelection().selectAllChildren(tagNameElement);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {function(string, string)} commitCallback
|
|
* @param {function()} disposeCallback
|
|
* @param {?string} maybeInitialValue
|
|
*/
|
|
_startEditingAsHTML(commitCallback, disposeCallback, maybeInitialValue) {
|
|
if (maybeInitialValue === null)
|
|
return;
|
|
let initialValue = maybeInitialValue; // To suppress a compiler warning.
|
|
if (this._editing)
|
|
return;
|
|
|
|
function consume(event) {
|
|
if (event.eventPhase === Event.AT_TARGET)
|
|
event.consume(true);
|
|
}
|
|
|
|
initialValue = this._convertWhitespaceToEntities(initialValue).text;
|
|
|
|
this._htmlEditElement = createElement('div');
|
|
this._htmlEditElement.className = 'source-code elements-tree-editor';
|
|
|
|
// Hide header items.
|
|
let child = this.listItemElement.firstChild;
|
|
while (child) {
|
|
child.style.display = 'none';
|
|
child = child.nextSibling;
|
|
}
|
|
// Hide children item.
|
|
if (this.childrenListElement)
|
|
this.childrenListElement.style.display = 'none';
|
|
// Append editor.
|
|
this.listItemElement.appendChild(this._htmlEditElement);
|
|
this.treeOutline.element.addEventListener('mousedown', consume, false);
|
|
|
|
self.runtime.extension(UI.TextEditorFactory).instance().then(gotFactory.bind(this));
|
|
|
|
/**
|
|
* @param {!UI.TextEditorFactory} factory
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function gotFactory(factory) {
|
|
const editor = factory.createEditor({
|
|
lineNumbers: false,
|
|
lineWrapping: Common.moduleSetting('domWordWrap').get(),
|
|
mimeType: 'text/html',
|
|
autoHeight: false,
|
|
padBottom: false
|
|
});
|
|
this._editing =
|
|
{commit: commit.bind(this), cancel: dispose.bind(this), editor: editor, resize: resize.bind(this)};
|
|
resize.call(this);
|
|
|
|
editor.widget().show(this._htmlEditElement);
|
|
editor.setText(initialValue);
|
|
editor.widget().focus();
|
|
editor.widget().element.addEventListener('blur', this._editing.commit, true);
|
|
editor.widget().element.addEventListener('keydown', keydown.bind(this), true);
|
|
|
|
this.treeOutline.setMultilineEditing(this._editing);
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function resize() {
|
|
this._htmlEditElement.style.width = this.treeOutline.visibleWidth() - this._computeLeftIndent() - 30 + 'px';
|
|
this._editing.editor.onResize();
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function commit() {
|
|
commitCallback(initialValue, this._editing.editor.text());
|
|
dispose.call(this);
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function dispose() {
|
|
this._editing.editor.widget().element.removeEventListener('blur', this._editing.commit, true);
|
|
this._editing.editor.widget().detach();
|
|
delete this._editing;
|
|
|
|
// Remove editor.
|
|
this.listItemElement.removeChild(this._htmlEditElement);
|
|
delete this._htmlEditElement;
|
|
// Unhide children item.
|
|
if (this.childrenListElement)
|
|
this.childrenListElement.style.removeProperty('display');
|
|
// Unhide header items.
|
|
let child = this.listItemElement.firstChild;
|
|
while (child) {
|
|
child.style.removeProperty('display');
|
|
child = child.nextSibling;
|
|
}
|
|
|
|
if (this.treeOutline) {
|
|
this.treeOutline.setMultilineEditing(null);
|
|
this.treeOutline.element.removeEventListener('mousedown', consume, false);
|
|
this.treeOutline.focus();
|
|
}
|
|
|
|
disposeCallback();
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @this {!Elements.ElementsTreeElement}
|
|
*/
|
|
function keydown(event) {
|
|
const isMetaOrCtrl = UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!KeyboardEvent} */ (event)) &&
|
|
!event.altKey && !event.shiftKey;
|
|
if (isEnterKey(event) && (isMetaOrCtrl || event.isMetaOrCtrlForTest)) {
|
|
event.consume(true);
|
|
this._editing.commit();
|
|
} else if (event.keyCode === UI.KeyboardShortcut.Keys.Esc.code || event.key === 'Escape') {
|
|
event.consume(true);
|
|
this._editing.cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
_attributeEditingCommitted(element, newText, oldText, attributeName, moveDirection) {
|
|
delete this._editing;
|
|
|
|
const treeOutline = this.treeOutline;
|
|
|
|
/**
|
|
* @param {?Protocol.Error=} error
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function moveToNextAttributeIfNeeded(error) {
|
|
if (error)
|
|
this._editingCancelled(element, attributeName);
|
|
|
|
if (!moveDirection)
|
|
return;
|
|
|
|
treeOutline.runPendingUpdates();
|
|
treeOutline.focus();
|
|
|
|
// Search for the attribute's position, and then decide where to move to.
|
|
const attributes = this._node.attributes();
|
|
for (let i = 0; i < attributes.length; ++i) {
|
|
if (attributes[i].name !== attributeName)
|
|
continue;
|
|
|
|
if (moveDirection === 'backward') {
|
|
if (i === 0)
|
|
this._startEditingTagName();
|
|
else
|
|
this._triggerEditAttribute(attributes[i - 1].name);
|
|
} else {
|
|
if (i === attributes.length - 1)
|
|
this._addNewAttribute();
|
|
else
|
|
this._triggerEditAttribute(attributes[i + 1].name);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Moving From the "New Attribute" position.
|
|
if (moveDirection === 'backward') {
|
|
if (newText === ' ') {
|
|
// Moving from "New Attribute" that was not edited
|
|
if (attributes.length > 0)
|
|
this._triggerEditAttribute(attributes[attributes.length - 1].name);
|
|
} else {
|
|
// Moving from "New Attribute" that holds new value
|
|
if (attributes.length > 1)
|
|
this._triggerEditAttribute(attributes[attributes.length - 2].name);
|
|
}
|
|
} else if (moveDirection === 'forward') {
|
|
if (!newText.isWhitespace())
|
|
this._addNewAttribute();
|
|
else
|
|
this._startEditingTagName();
|
|
}
|
|
}
|
|
|
|
if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
|
|
this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
|
|
return;
|
|
}
|
|
|
|
this.updateTitle();
|
|
moveToNextAttributeIfNeeded.call(this);
|
|
}
|
|
|
|
_tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection) {
|
|
delete this._editing;
|
|
const self = this;
|
|
|
|
function cancel() {
|
|
const closingTagElement = self._distinctClosingTagElement();
|
|
if (closingTagElement)
|
|
closingTagElement.textContent = '</' + tagName + '>';
|
|
|
|
self._editingCancelled(element, tagName);
|
|
moveToNextAttributeIfNeeded.call(self);
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function moveToNextAttributeIfNeeded() {
|
|
if (moveDirection !== 'forward') {
|
|
this._addNewAttribute();
|
|
return;
|
|
}
|
|
|
|
const attributes = this._node.attributes();
|
|
if (attributes.length > 0)
|
|
this._triggerEditAttribute(attributes[0].name);
|
|
else
|
|
this._addNewAttribute();
|
|
}
|
|
|
|
newText = newText.trim();
|
|
if (newText === oldText) {
|
|
cancel();
|
|
return;
|
|
}
|
|
|
|
const treeOutline = this.treeOutline;
|
|
const wasExpanded = this.expanded;
|
|
|
|
this._node.setNodeName(newText, (error, newNode) => {
|
|
if (error || !newNode) {
|
|
cancel();
|
|
return;
|
|
}
|
|
const newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error, newNode);
|
|
moveToNextAttributeIfNeeded.call(newTreeItem);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} textNode
|
|
* @param {!Element} element
|
|
* @param {string} newText
|
|
*/
|
|
_textNodeEditingCommitted(textNode, element, newText) {
|
|
delete this._editing;
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function callback() {
|
|
this.updateTitle();
|
|
}
|
|
textNode.setNodeValue(newText, callback.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {*} context
|
|
*/
|
|
_editingCancelled(element, context) {
|
|
delete this._editing;
|
|
|
|
// Need to restore attributes structure.
|
|
this.updateTitle();
|
|
}
|
|
|
|
/**
|
|
* @return {!Element}
|
|
*/
|
|
_distinctClosingTagElement() {
|
|
// FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
|
|
|
|
// For an expanded element, it will be the last element with class "close"
|
|
// in the child element list.
|
|
if (this.expanded) {
|
|
const closers = this.childrenListElement.querySelectorAll('.close');
|
|
return closers[closers.length - 1];
|
|
}
|
|
|
|
// Remaining cases are single line non-expanded elements with a closing
|
|
// tag, or HTML elements without a closing tag (such as <br>). Return
|
|
// null in the case where there isn't a closing tag.
|
|
const tags = this.listItemElement.getElementsByClassName('webkit-html-tag');
|
|
return (tags.length === 1 ? null : tags[tags.length - 1]);
|
|
}
|
|
|
|
/**
|
|
* @param {?Elements.ElementsTreeOutline.UpdateRecord=} updateRecord
|
|
* @param {boolean=} onlySearchQueryChanged
|
|
*/
|
|
updateTitle(updateRecord, onlySearchQueryChanged) {
|
|
// If we are editing, return early to prevent canceling the edit.
|
|
// After editing is committed updateTitle will be called.
|
|
if (this._editing)
|
|
return;
|
|
|
|
if (onlySearchQueryChanged) {
|
|
this._hideSearchHighlight();
|
|
} else {
|
|
const nodeInfo = this._nodeTitleInfo(updateRecord || null);
|
|
if (this._node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE && this._node.isInShadowTree() &&
|
|
this._node.shadowRootType()) {
|
|
this.childrenListElement.classList.add('shadow-root');
|
|
let depth = 4;
|
|
for (let node = this._node; depth && node; node = node.parentNode) {
|
|
if (node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE)
|
|
depth--;
|
|
}
|
|
if (!depth)
|
|
this.childrenListElement.classList.add('shadow-root-deep');
|
|
else
|
|
this.childrenListElement.classList.add('shadow-root-depth-' + depth);
|
|
}
|
|
const highlightElement = createElement('span');
|
|
highlightElement.className = 'highlight';
|
|
highlightElement.appendChild(nodeInfo);
|
|
this.title = highlightElement;
|
|
this.updateDecorations();
|
|
this.listItemElement.insertBefore(this._gutterContainer, this.listItemElement.firstChild);
|
|
delete this._highlightResult;
|
|
delete this.selectionElement;
|
|
delete this._hintElement;
|
|
if (this.selected) {
|
|
this._createSelection();
|
|
this._createHint();
|
|
}
|
|
}
|
|
|
|
this._highlightSearchResults();
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
_computeLeftIndent() {
|
|
let treeElement = this.parent;
|
|
let depth = 0;
|
|
while (treeElement !== null) {
|
|
depth++;
|
|
treeElement = treeElement.parent;
|
|
}
|
|
|
|
/** Keep it in sync with elementsTreeOutline.css **/
|
|
return 12 * (depth - 2) + (this.isExpandable() ? 1 : 12);
|
|
}
|
|
|
|
updateDecorations() {
|
|
this._gutterContainer.style.left = (-this._computeLeftIndent()) + 'px';
|
|
|
|
if (this.isClosingTag())
|
|
return;
|
|
|
|
if (this._node.nodeType() !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
this._decorationsThrottler.schedule(this._updateDecorationsInternal.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
*/
|
|
_updateDecorationsInternal() {
|
|
if (!this.treeOutline)
|
|
return Promise.resolve();
|
|
|
|
const node = this._node;
|
|
|
|
if (!this.treeOutline._decoratorExtensions)
|
|
/** @type {!Array.<!Runtime.Extension>} */
|
|
this.treeOutline._decoratorExtensions = self.runtime.extensions(Elements.MarkerDecorator);
|
|
|
|
const markerToExtension = new Map();
|
|
for (let i = 0; i < this.treeOutline._decoratorExtensions.length; ++i) {
|
|
markerToExtension.set(
|
|
this.treeOutline._decoratorExtensions[i].descriptor()['marker'], this.treeOutline._decoratorExtensions[i]);
|
|
}
|
|
|
|
const promises = [];
|
|
const decorations = [];
|
|
const descendantDecorations = [];
|
|
node.traverseMarkers(visitor);
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} n
|
|
* @param {string} marker
|
|
*/
|
|
function visitor(n, marker) {
|
|
const extension = markerToExtension.get(marker);
|
|
if (!extension)
|
|
return;
|
|
promises.push(extension.instance().then(collectDecoration.bind(null, n)));
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} n
|
|
* @param {!Elements.MarkerDecorator} decorator
|
|
*/
|
|
function collectDecoration(n, decorator) {
|
|
const decoration = decorator.decorate(n);
|
|
if (!decoration)
|
|
return;
|
|
(n === node ? decorations : descendantDecorations).push(decoration);
|
|
}
|
|
|
|
return Promise.all(promises).then(updateDecorationsUI.bind(this));
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function updateDecorationsUI() {
|
|
this._decorationsElement.removeChildren();
|
|
this._decorationsElement.classList.add('hidden');
|
|
this._gutterContainer.classList.toggle('has-decorations', decorations.length || descendantDecorations.length);
|
|
|
|
if (!decorations.length && !descendantDecorations.length)
|
|
return;
|
|
|
|
const colors = new Set();
|
|
const titles = createElement('div');
|
|
|
|
for (const decoration of decorations) {
|
|
const titleElement = titles.createChild('div');
|
|
titleElement.textContent = decoration.title;
|
|
colors.add(decoration.color);
|
|
}
|
|
if (this.expanded && !decorations.length)
|
|
return;
|
|
|
|
const descendantColors = new Set();
|
|
if (descendantDecorations.length) {
|
|
let element = titles.createChild('div');
|
|
element.textContent = Common.UIString('Children:');
|
|
for (const decoration of descendantDecorations) {
|
|
element = titles.createChild('div');
|
|
element.style.marginLeft = '15px';
|
|
element.textContent = decoration.title;
|
|
descendantColors.add(decoration.color);
|
|
}
|
|
}
|
|
|
|
let offset = 0;
|
|
processColors.call(this, colors, 'elements-gutter-decoration');
|
|
if (!this.expanded)
|
|
processColors.call(this, descendantColors, 'elements-gutter-decoration elements-has-decorated-children');
|
|
UI.Tooltip.install(this._decorationsElement, titles);
|
|
|
|
/**
|
|
* @param {!Set<string>} colors
|
|
* @param {string} className
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function processColors(colors, className) {
|
|
for (const color of colors) {
|
|
const child = this._decorationsElement.createChild('div', className);
|
|
this._decorationsElement.classList.remove('hidden');
|
|
child.style.backgroundColor = color;
|
|
child.style.borderColor = color;
|
|
if (offset)
|
|
child.style.marginLeft = offset + 'px';
|
|
offset += 3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Node} parentElement
|
|
* @param {string} name
|
|
* @param {string} value
|
|
* @param {?Elements.ElementsTreeOutline.UpdateRecord} updateRecord
|
|
* @param {boolean=} forceValue
|
|
* @param {!SDK.DOMNode=} node
|
|
*/
|
|
_buildAttributeDOM(parentElement, name, value, updateRecord, forceValue, node) {
|
|
const closingPunctuationRegex = /[\/;:\)\]\}]/g;
|
|
let highlightIndex = 0;
|
|
let highlightCount;
|
|
let additionalHighlightOffset = 0;
|
|
let result;
|
|
|
|
/**
|
|
* @param {string} match
|
|
* @param {number} replaceOffset
|
|
* @return {string}
|
|
*/
|
|
function replacer(match, replaceOffset) {
|
|
while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) {
|
|
result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
|
|
++highlightIndex;
|
|
}
|
|
additionalHighlightOffset += 1;
|
|
return match + '\u200B';
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {string} value
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function setValueWithEntities(element, value) {
|
|
result = this._convertWhitespaceToEntities(value);
|
|
highlightCount = result.entityRanges.length;
|
|
value = result.text.replace(closingPunctuationRegex, replacer);
|
|
while (highlightIndex < highlightCount) {
|
|
result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
|
|
++highlightIndex;
|
|
}
|
|
element.setTextContentTruncatedIfNeeded(value);
|
|
UI.highlightRangesWithStyleClass(element, result.entityRanges, 'webkit-html-entity-value');
|
|
}
|
|
|
|
const hasText = (forceValue || value.length > 0);
|
|
const attrSpanElement = parentElement.createChild('span', 'webkit-html-attribute');
|
|
const attrNameElement = attrSpanElement.createChild('span', 'webkit-html-attribute-name');
|
|
attrNameElement.textContent = name;
|
|
|
|
if (hasText)
|
|
attrSpanElement.createTextChild('=\u200B"');
|
|
|
|
const attrValueElement = attrSpanElement.createChild('span', 'webkit-html-attribute-value');
|
|
|
|
if (updateRecord && updateRecord.isAttributeModified(name))
|
|
UI.runCSSAnimationOnce(hasText ? attrValueElement : attrNameElement, 'dom-update-highlight');
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
* @param {string} value
|
|
* @return {!Element}
|
|
*/
|
|
function linkifyValue(value) {
|
|
const rewrittenHref = node.resolveURL(value);
|
|
if (rewrittenHref === null) {
|
|
const span = createElement('span');
|
|
setValueWithEntities.call(this, span, value);
|
|
return span;
|
|
}
|
|
value = value.replace(closingPunctuationRegex, '$&\u200B');
|
|
if (value.startsWith('data:'))
|
|
value = value.trimMiddle(60);
|
|
const link = node.nodeName().toLowerCase() === 'a' ?
|
|
UI.XLink.create(rewrittenHref, value, '', true /* preventClick */) :
|
|
Components.Linkifier.linkifyURL(rewrittenHref, {text: value, preventClick: true});
|
|
link[Elements.ElementsTreeElement.HrefSymbol] = rewrittenHref;
|
|
return link;
|
|
}
|
|
|
|
const nodeName = node ? node.nodeName().toLowerCase() : '';
|
|
if (nodeName && (name === 'src' || name === 'href'))
|
|
attrValueElement.appendChild(linkifyValue.call(this, value));
|
|
else if ((nodeName === 'img' || nodeName === 'source') && name === 'srcset')
|
|
attrValueElement.appendChild(linkifySrcset.call(this, value));
|
|
else
|
|
setValueWithEntities.call(this, attrValueElement, value);
|
|
|
|
if (hasText)
|
|
attrSpanElement.createTextChild('"');
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @return {!DocumentFragment}
|
|
* @this {!Elements.ElementsTreeElement}
|
|
*/
|
|
function linkifySrcset(value) {
|
|
// Splitting normally on commas or spaces will break on valid srcsets "foo 1x,bar 2x" and "data:,foo 1x".
|
|
// 1) Let the index of the next space be `indexOfSpace`.
|
|
// 2a) If the character at `indexOfSpace - 1` is a comma, collect the preceding characters up to
|
|
// `indexOfSpace - 1` as a URL and repeat step 1).
|
|
// 2b) Else, collect the preceding characters as a URL.
|
|
// 3) Collect the characters from `indexOfSpace` up to the next comma as the size descriptor and repeat step 1).
|
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute
|
|
const fragment = createDocumentFragment();
|
|
let i = 0;
|
|
while (value.length) {
|
|
if (i++ > 0)
|
|
fragment.createTextChild(' ');
|
|
value = value.trim();
|
|
// The url and descriptor may end with a separating comma.
|
|
let url = '';
|
|
let descriptor = '';
|
|
const indexOfSpace = value.search(/\s/);
|
|
if (indexOfSpace === -1) {
|
|
url = value;
|
|
} else if (indexOfSpace > 0 && value[indexOfSpace - 1] === ',') {
|
|
url = value.substring(0, indexOfSpace);
|
|
} else {
|
|
url = value.substring(0, indexOfSpace);
|
|
const indexOfComma = value.indexOf(',', indexOfSpace);
|
|
if (indexOfComma !== -1)
|
|
descriptor = value.substring(indexOfSpace, indexOfComma + 1);
|
|
else
|
|
descriptor = value.substring(indexOfSpace);
|
|
}
|
|
|
|
if (url) {
|
|
// Up to one trailing comma should be removed from `url`.
|
|
if (url.endsWith(',')) {
|
|
fragment.appendChild(linkifyValue.call(this, url.substring(0, url.length - 1)));
|
|
fragment.createTextChild(',');
|
|
} else {
|
|
fragment.appendChild(linkifyValue.call(this, url));
|
|
}
|
|
}
|
|
if (descriptor)
|
|
fragment.createTextChild(descriptor);
|
|
value = value.substring(url.length + descriptor.length);
|
|
}
|
|
return fragment;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Node} parentElement
|
|
* @param {string} pseudoElementName
|
|
*/
|
|
_buildPseudoElementDOM(parentElement, pseudoElementName) {
|
|
const pseudoElement = parentElement.createChild('span', 'webkit-html-pseudo-element');
|
|
pseudoElement.textContent = '::' + pseudoElementName;
|
|
parentElement.createTextChild('\u200B');
|
|
}
|
|
|
|
/**
|
|
* @param {!Node} parentElement
|
|
* @param {string} tagName
|
|
* @param {boolean} isClosingTag
|
|
* @param {boolean} isDistinctTreeElement
|
|
* @param {?Elements.ElementsTreeOutline.UpdateRecord} updateRecord
|
|
*/
|
|
_buildTagDOM(parentElement, tagName, isClosingTag, isDistinctTreeElement, updateRecord) {
|
|
const node = this._node;
|
|
const classes = ['webkit-html-tag'];
|
|
if (isClosingTag && isDistinctTreeElement)
|
|
classes.push('close');
|
|
const tagElement = parentElement.createChild('span', classes.join(' '));
|
|
tagElement.createTextChild('<');
|
|
const tagNameElement =
|
|
tagElement.createChild('span', isClosingTag ? 'webkit-html-close-tag-name' : 'webkit-html-tag-name');
|
|
tagNameElement.textContent = (isClosingTag ? '/' : '') + tagName;
|
|
if (!isClosingTag) {
|
|
if (node.hasAttributes()) {
|
|
const attributes = node.attributes();
|
|
for (let i = 0; i < attributes.length; ++i) {
|
|
const attr = attributes[i];
|
|
tagElement.createTextChild(' ');
|
|
this._buildAttributeDOM(tagElement, attr.name, attr.value, updateRecord, false, node);
|
|
}
|
|
}
|
|
if (updateRecord) {
|
|
let hasUpdates = updateRecord.hasRemovedAttributes() || updateRecord.hasRemovedChildren();
|
|
hasUpdates |= !this.expanded && updateRecord.hasChangedChildren();
|
|
if (hasUpdates)
|
|
UI.runCSSAnimationOnce(tagNameElement, 'dom-update-highlight');
|
|
}
|
|
}
|
|
|
|
tagElement.createTextChild('>');
|
|
parentElement.createTextChild('\u200B');
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {!{text: string, entityRanges: !Array.<!TextUtils.SourceRange>}}
|
|
*/
|
|
_convertWhitespaceToEntities(text) {
|
|
let result = '';
|
|
let lastIndexAfterEntity = 0;
|
|
const entityRanges = [];
|
|
const charToEntity = Elements.ElementsTreeOutline.MappedCharToEntity;
|
|
for (let i = 0, size = text.length; i < size; ++i) {
|
|
const char = text.charAt(i);
|
|
if (charToEntity[char]) {
|
|
result += text.substring(lastIndexAfterEntity, i);
|
|
const entityValue = '&' + charToEntity[char] + ';';
|
|
entityRanges.push({offset: result.length, length: entityValue.length});
|
|
result += entityValue;
|
|
lastIndexAfterEntity = i + 1;
|
|
}
|
|
}
|
|
if (result)
|
|
result += text.substring(lastIndexAfterEntity);
|
|
return {text: result || text, entityRanges: entityRanges};
|
|
}
|
|
|
|
/**
|
|
* @param {?Elements.ElementsTreeOutline.UpdateRecord} updateRecord
|
|
* @return {!DocumentFragment} result
|
|
*/
|
|
_nodeTitleInfo(updateRecord) {
|
|
const node = this._node;
|
|
const titleDOM = createDocumentFragment();
|
|
|
|
switch (node.nodeType()) {
|
|
case Node.ATTRIBUTE_NODE:
|
|
this._buildAttributeDOM(
|
|
titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), updateRecord, true);
|
|
break;
|
|
|
|
case Node.ELEMENT_NODE:
|
|
const pseudoType = node.pseudoType();
|
|
if (pseudoType) {
|
|
this._buildPseudoElementDOM(titleDOM, pseudoType);
|
|
break;
|
|
}
|
|
|
|
const tagName = node.nodeNameInCorrectCase();
|
|
if (this._elementCloseTag) {
|
|
this._buildTagDOM(titleDOM, tagName, true, true, updateRecord);
|
|
break;
|
|
}
|
|
|
|
this._buildTagDOM(titleDOM, tagName, false, false, updateRecord);
|
|
|
|
if (this.isExpandable()) {
|
|
if (!this.expanded) {
|
|
const textNodeElement = titleDOM.createChild('span', 'webkit-html-text-node bogus');
|
|
textNodeElement.textContent = '\u2026';
|
|
titleDOM.createTextChild('\u200B');
|
|
this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (Elements.ElementsTreeElement.canShowInlineText(node)) {
|
|
const textNodeElement = titleDOM.createChild('span', 'webkit-html-text-node');
|
|
const result = this._convertWhitespaceToEntities(node.firstChild.nodeValue());
|
|
textNodeElement.textContent = result.text;
|
|
UI.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, 'webkit-html-entity-value');
|
|
titleDOM.createTextChild('\u200B');
|
|
this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
|
|
if (updateRecord && updateRecord.hasChangedChildren())
|
|
UI.runCSSAnimationOnce(textNodeElement, 'dom-update-highlight');
|
|
if (updateRecord && updateRecord.isCharDataModified())
|
|
UI.runCSSAnimationOnce(textNodeElement, 'dom-update-highlight');
|
|
break;
|
|
}
|
|
|
|
if (this.treeOutline.isXMLMimeType || !Elements.ElementsTreeElement.ForbiddenClosingTagElements.has(tagName))
|
|
this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
|
|
break;
|
|
|
|
case Node.TEXT_NODE:
|
|
if (node.parentNode && node.parentNode.nodeName().toLowerCase() === 'script') {
|
|
const newNode = titleDOM.createChild('span', 'webkit-html-text-node webkit-html-js-node');
|
|
const text = node.nodeValue();
|
|
newNode.textContent = text.startsWith('\n') ? text.substring(1) : text;
|
|
|
|
const javascriptSyntaxHighlighter = new UI.SyntaxHighlighter('text/javascript', true);
|
|
javascriptSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSearchHighlight.bind(this));
|
|
} else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === 'style') {
|
|
const newNode = titleDOM.createChild('span', 'webkit-html-text-node webkit-html-css-node');
|
|
const text = node.nodeValue();
|
|
newNode.textContent = text.startsWith('\n') ? text.substring(1) : text;
|
|
|
|
const cssSyntaxHighlighter = new UI.SyntaxHighlighter('text/css', true);
|
|
cssSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSearchHighlight.bind(this));
|
|
} else {
|
|
titleDOM.createTextChild('"');
|
|
const textNodeElement = titleDOM.createChild('span', 'webkit-html-text-node');
|
|
const result = this._convertWhitespaceToEntities(node.nodeValue());
|
|
textNodeElement.textContent = result.text;
|
|
UI.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, 'webkit-html-entity-value');
|
|
titleDOM.createTextChild('"');
|
|
if (updateRecord && updateRecord.isCharDataModified())
|
|
UI.runCSSAnimationOnce(textNodeElement, 'dom-update-highlight');
|
|
}
|
|
break;
|
|
|
|
case Node.COMMENT_NODE:
|
|
const commentElement = titleDOM.createChild('span', 'webkit-html-comment');
|
|
commentElement.createTextChild('<!--' + node.nodeValue() + '-->');
|
|
break;
|
|
|
|
case Node.DOCUMENT_TYPE_NODE:
|
|
const docTypeElement = titleDOM.createChild('span', 'webkit-html-doctype');
|
|
docTypeElement.createTextChild('<!doctype ' + node.nodeName());
|
|
if (node.publicId) {
|
|
docTypeElement.createTextChild(' PUBLIC "' + node.publicId + '"');
|
|
if (node.systemId)
|
|
docTypeElement.createTextChild(' "' + node.systemId + '"');
|
|
} else if (node.systemId) {
|
|
docTypeElement.createTextChild(' SYSTEM "' + node.systemId + '"');
|
|
}
|
|
|
|
if (node.internalSubset)
|
|
docTypeElement.createTextChild(' [' + node.internalSubset + ']');
|
|
|
|
docTypeElement.createTextChild('>');
|
|
break;
|
|
|
|
case Node.CDATA_SECTION_NODE:
|
|
const cdataElement = titleDOM.createChild('span', 'webkit-html-text-node');
|
|
cdataElement.createTextChild('<![CDATA[' + node.nodeValue() + ']]>');
|
|
break;
|
|
|
|
case Node.DOCUMENT_FRAGMENT_NODE:
|
|
const fragmentElement = titleDOM.createChild('span', 'webkit-html-fragment');
|
|
fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
|
|
break;
|
|
default:
|
|
titleDOM.createTextChild(node.nodeNameInCorrectCase().collapseWhitespace());
|
|
}
|
|
|
|
/**
|
|
* @this {Elements.ElementsTreeElement}
|
|
*/
|
|
function updateSearchHighlight() {
|
|
delete this._highlightResult;
|
|
this._highlightSearchResults();
|
|
}
|
|
|
|
return titleDOM;
|
|
}
|
|
|
|
remove() {
|
|
if (this._node.pseudoType())
|
|
return;
|
|
const parentElement = this.parent;
|
|
if (!parentElement)
|
|
return;
|
|
|
|
if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
|
|
return;
|
|
this._node.removeNode();
|
|
}
|
|
|
|
/**
|
|
* @param {function(boolean)=} callback
|
|
* @param {boolean=} startEditing
|
|
*/
|
|
toggleEditAsHTML(callback, startEditing) {
|
|
if (this._editing && this._htmlEditElement) {
|
|
this._editing.commit();
|
|
return;
|
|
}
|
|
|
|
if (startEditing === false)
|
|
return;
|
|
|
|
/**
|
|
* @param {?Protocol.Error} error
|
|
*/
|
|
function selectNode(error) {
|
|
if (callback)
|
|
callback(!error);
|
|
}
|
|
|
|
/**
|
|
* @param {string} initialValue
|
|
* @param {string} value
|
|
*/
|
|
function commitChange(initialValue, value) {
|
|
if (initialValue !== value)
|
|
node.setOuterHTML(value, selectNode);
|
|
}
|
|
|
|
function disposeCallback() {
|
|
if (callback)
|
|
callback(false);
|
|
}
|
|
|
|
const node = this._node;
|
|
node.getOuterHTML().then(this._startEditingAsHTML.bind(this, commitChange, disposeCallback));
|
|
}
|
|
|
|
_copyCSSPath() {
|
|
InspectorFrontendHost.copyText(Elements.DOMPath.cssPath(this._node, true));
|
|
}
|
|
|
|
_copyXPath() {
|
|
InspectorFrontendHost.copyText(Elements.DOMPath.xPath(this._node, true));
|
|
}
|
|
|
|
_highlightSearchResults() {
|
|
if (!this._searchQuery || !this._searchHighlightsVisible)
|
|
return;
|
|
this._hideSearchHighlight();
|
|
|
|
const text = this.listItemElement.textContent;
|
|
const regexObject = createPlainTextSearchRegex(this._searchQuery, 'gi');
|
|
|
|
let match = regexObject.exec(text);
|
|
const matchRanges = [];
|
|
while (match) {
|
|
matchRanges.push(new TextUtils.SourceRange(match.index, match[0].length));
|
|
match = regexObject.exec(text);
|
|
}
|
|
|
|
// Fall back for XPath, etc. matches.
|
|
if (!matchRanges.length)
|
|
matchRanges.push(new TextUtils.SourceRange(0, text.length));
|
|
|
|
this._highlightResult = [];
|
|
UI.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
|
|
}
|
|
|
|
_editAsHTML() {
|
|
const promise = Common.Revealer.reveal(this.node());
|
|
promise.then(() => UI.actionRegistry.action('elements.edit-as-html').execute());
|
|
}
|
|
};
|
|
|
|
Elements.ElementsTreeElement.HrefSymbol = Symbol('ElementsTreeElement.Href');
|
|
|
|
Elements.ElementsTreeElement.InitialChildrenLimit = 500;
|
|
|
|
// A union of HTML4 and HTML5-Draft elements that explicitly
|
|
// or implicitly (for HTML5) forbid the closing tag.
|
|
Elements.ElementsTreeElement.ForbiddenClosingTagElements = new Set([
|
|
'area', 'base', 'basefont', 'br', 'canvas', 'col', 'command', 'embed', 'frame', 'hr',
|
|
'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'
|
|
]);
|
|
|
|
// These tags we do not allow editing their tag name.
|
|
Elements.ElementsTreeElement.EditTagBlacklist = new Set(['html', 'head', 'body']);
|
|
|
|
/** @typedef {{cancel: function(), commit: function(), resize: function(), editor:!UI.TextEditor}} */
|
|
Elements.MultilineEditorController;
|