Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(64)

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeElement.js

Issue 2466123002: DevTools: reformat front-end code to match chromium style. (Closed)
Patch Set: all done Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> 3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4 * Copyright (C) 2009 Joseph Pecoraro 4 * Copyright (C) 2009 Joseph Pecoraro
5 * 5 *
6 * Redistribution and use in source and binary forms, with or without 6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions 7 * modification, are permitted provided that the following conditions
8 * are met: 8 * are met:
9 * 9 *
10 * 1. Redistributions of source code must retain the above copyright 10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer. 11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright 12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the 13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution. 14 * documentation and/or other materials provided with the distribution.
15 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 15 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 * its contributors may be used to endorse or promote products derived 16 * its contributors may be used to endorse or promote products derived
17 * from this software without specific prior written permission. 17 * from this software without specific prior written permission.
18 * 18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */ 29 */
30
31 /** 30 /**
32 * @constructor 31 * @unrestricted
33 * @extends {TreeElement}
34 * @param {!WebInspector.DOMNode} node
35 * @param {boolean=} elementCloseTag
36 */ 32 */
37 WebInspector.ElementsTreeElement = function(node, elementCloseTag) 33 WebInspector.ElementsTreeElement = class extends TreeElement {
38 { 34 /**
35 * @param {!WebInspector.DOMNode} node
36 * @param {boolean=} elementCloseTag
37 */
38 constructor(node, elementCloseTag) {
39 // The title will be updated in onattach. 39 // The title will be updated in onattach.
40 TreeElement.call(this); 40 super();
41 this._node = node; 41 this._node = node;
42 42
43 this._gutterContainer = this.listItemElement.createChild("div", "gutter-cont ainer"); 43 this._gutterContainer = this.listItemElement.createChild('div', 'gutter-cont ainer');
44 this._gutterContainer.addEventListener("click", this._showContextMenu.bind(t his)); 44 this._gutterContainer.addEventListener('click', this._showContextMenu.bind(t his));
45 this._decorationsElement = this._gutterContainer.createChild("div", "hidden" ); 45 this._decorationsElement = this._gutterContainer.createChild('div', 'hidden' );
46 46
47 this._elementCloseTag = elementCloseTag; 47 this._elementCloseTag = elementCloseTag;
48 48
49 if (this._node.nodeType() === Node.ELEMENT_NODE && !elementCloseTag) 49 if (this._node.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
50 this._canAddAttributes = true; 50 this._canAddAttributes = true;
51 this._searchQuery = null; 51 this._searchQuery = null;
52 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildr enLimit; 52 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildr enLimit;
53 }
54
55 /**
56 * @param {!WebInspector.ElementsTreeElement} treeElement
57 */
58 static animateOnDOMUpdate(treeElement) {
59 var tagName = treeElement.listItemElement.querySelector('.webkit-html-tag-na me');
60 WebInspector.runCSSAnimationOnce(tagName || treeElement.listItemElement, 'do m-update-highlight');
61 }
62
63 /**
64 * @param {!WebInspector.DOMNode} node
65 * @return {!Array<!WebInspector.DOMNode>}
66 */
67 static visibleShadowRoots(node) {
68 var roots = node.shadowRoots();
69 if (roots.length && !WebInspector.moduleSetting('showUAShadowDOM').get())
70 roots = roots.filter(filter);
71
72 /**
73 * @param {!WebInspector.DOMNode} root
74 */
75 function filter(root) {
76 return root.shadowRootType() !== WebInspector.DOMNode.ShadowRootTypes.User Agent;
77 }
78 return roots;
79 }
80
81 /**
82 * @param {!WebInspector.DOMNode} node
83 * @return {boolean}
84 */
85 static canShowInlineText(node) {
86 if (node.importedDocument() || node.templateContent() ||
87 WebInspector.ElementsTreeElement.visibleShadowRoots(node).length || node .hasPseudoElements())
88 return false;
89 if (node.nodeType() !== Node.ELEMENT_NODE)
90 return false;
91 if (!node.firstChild || node.firstChild !== node.lastChild || node.firstChil d.nodeType() !== Node.TEXT_NODE)
92 return false;
93 var textChild = node.firstChild;
94 var maxInlineTextChildLength = 80;
95 if (textChild.nodeValue().length < maxInlineTextChildLength)
96 return true;
97 return false;
98 }
99
100 /**
101 * @param {!WebInspector.ContextSubMenuItem} subMenu
102 * @param {!WebInspector.DOMNode} node
103 */
104 static populateForcedPseudoStateItems(subMenu, node) {
105 const pseudoClasses = ['active', 'hover', 'focus', 'visited'];
106 var forcedPseudoState = WebInspector.CSSModel.fromNode(node).pseudoState(nod e);
107 for (var i = 0; i < pseudoClasses.length; ++i) {
108 var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
109 subMenu.appendCheckboxItem(
110 ':' + pseudoClasses[i], setPseudoStateCallback.bind(null, pseudoClasse s[i], !pseudoClassForced),
111 pseudoClassForced, false);
112 }
113
114 /**
115 * @param {string} pseudoState
116 * @param {boolean} enabled
117 */
118 function setPseudoStateCallback(pseudoState, enabled) {
119 WebInspector.CSSModel.fromNode(node).forcePseudoState(node, pseudoState, e nabled);
120 }
121 }
122
123 /**
124 * @return {boolean}
125 */
126 isClosingTag() {
127 return !!this._elementCloseTag;
128 }
129
130 /**
131 * @return {!WebInspector.DOMNode}
132 */
133 node() {
134 return this._node;
135 }
136
137 /**
138 * @return {boolean}
139 */
140 isEditing() {
141 return !!this._editing;
142 }
143
144 /**
145 * @param {string} searchQuery
146 */
147 highlightSearchResults(searchQuery) {
148 if (this._searchQuery !== searchQuery)
149 this._hideSearchHighlight();
150
151 this._searchQuery = searchQuery;
152 this._searchHighlightsVisible = true;
153 this.updateTitle(null, true);
154 }
155
156 hideSearchHighlights() {
157 delete this._searchHighlightsVisible;
158 this._hideSearchHighlight();
159 }
160
161 _hideSearchHighlight() {
162 if (!this._highlightResult)
163 return;
164
165 function updateEntryHide(entry) {
166 switch (entry.type) {
167 case 'added':
168 entry.node.remove();
169 break;
170 case 'changed':
171 entry.node.textContent = entry.oldText;
172 break;
173 }
174 }
175
176 for (var i = (this._highlightResult.length - 1); i >= 0; --i)
177 updateEntryHide(this._highlightResult[i]);
178
179 delete this._highlightResult;
180 }
181
182 /**
183 * @param {boolean} inClipboard
184 */
185 setInClipboard(inClipboard) {
186 if (this._inClipboard === inClipboard)
187 return;
188 this._inClipboard = inClipboard;
189 this.listItemElement.classList.toggle('in-clipboard', inClipboard);
190 }
191
192 get hovered() {
193 return this._hovered;
194 }
195
196 set hovered(x) {
197 if (this._hovered === x)
198 return;
199
200 this._hovered = x;
201
202 if (this.listItemElement) {
203 if (x) {
204 this._createSelection();
205 this.listItemElement.classList.add('hovered');
206 } else {
207 this.listItemElement.classList.remove('hovered');
208 }
209 }
210 }
211
212 /**
213 * @return {number}
214 */
215 expandedChildrenLimit() {
216 return this._expandedChildrenLimit;
217 }
218
219 /**
220 * @param {number} expandedChildrenLimit
221 */
222 setExpandedChildrenLimit(expandedChildrenLimit) {
223 this._expandedChildrenLimit = expandedChildrenLimit;
224 }
225
226 _createSelection() {
227 var listItemElement = this.listItemElement;
228 if (!listItemElement)
229 return;
230
231 if (!this.selectionElement) {
232 this.selectionElement = createElement('div');
233 this.selectionElement.className = 'selection fill';
234 this.selectionElement.style.setProperty('margin-left', (-this._computeLeft Indent()) + 'px');
235 listItemElement.insertBefore(this.selectionElement, listItemElement.firstC hild);
236 }
237 }
238
239 /**
240 * @override
241 */
242 onbind() {
243 if (!this._elementCloseTag)
244 this._node[this.treeOutline.treeElementSymbol()] = this;
245 }
246
247 /**
248 * @override
249 */
250 onunbind() {
251 if (this._node[this.treeOutline.treeElementSymbol()] === this)
252 this._node[this.treeOutline.treeElementSymbol()] = null;
253 }
254
255 /**
256 * @override
257 */
258 onattach() {
259 if (this._hovered) {
260 this._createSelection();
261 this.listItemElement.classList.add('hovered');
262 }
263
264 this.updateTitle();
265 this._preventFollowingLinksOnDoubleClick();
266 this.listItemElement.draggable = true;
267 }
268
269 _preventFollowingLinksOnDoubleClick() {
270 var links = this.listItemElement.querySelectorAll(
271 'li .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-li nk, li .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link');
272 if (!links)
273 return;
274
275 for (var i = 0; i < links.length; ++i)
276 links[i].preventFollowOnDoubleClick = true;
277 }
278
279 /**
280 * @override
281 */
282 onpopulate() {
283 this.populated = true;
284 this.treeOutline.populateTreeElement(this);
285 }
286
287 /**
288 * @override
289 */
290 expandRecursively() {
291 this._node.getSubtree(-1, TreeElement.prototype.expandRecursively.bind(this, Number.MAX_VALUE));
292 }
293
294 /**
295 * @override
296 */
297 onexpand() {
298 if (this._elementCloseTag)
299 return;
300
301 this.updateTitle();
302 }
303
304 /**
305 * @override
306 */
307 oncollapse() {
308 if (this._elementCloseTag)
309 return;
310
311 this.updateTitle();
312 }
313
314 /**
315 * @override
316 * @param {boolean=} omitFocus
317 * @param {boolean=} selectedByUser
318 * @return {boolean}
319 */
320 select(omitFocus, selectedByUser) {
321 if (this._editing)
322 return false;
323 return super.select(omitFocus, selectedByUser);
324 }
325
326 /**
327 * @override
328 * @param {boolean=} selectedByUser
329 * @return {boolean}
330 */
331 onselect(selectedByUser) {
332 this.treeOutline.suppressRevealAndSelect = true;
333 this.treeOutline.selectDOMNode(this._node, selectedByUser);
334 if (selectedByUser)
335 this._node.highlight();
336 this._createSelection();
337 this.treeOutline.suppressRevealAndSelect = false;
338 return true;
339 }
340
341 /**
342 * @override
343 * @return {boolean}
344 */
345 ondelete() {
346 var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
347 startTagTreeElement ? startTagTreeElement.remove() : this.remove();
348 return true;
349 }
350
351 /**
352 * @override
353 * @return {boolean}
354 */
355 onenter() {
356 // On Enter or Return start editing the first attribute
357 // or create a new attribute on the selected element.
358 if (this._editing)
359 return false;
360
361 this._startEditing();
362
363 // prevent a newline from being immediately inserted
364 return true;
365 }
366
367 /**
368 * @override
369 */
370 selectOnMouseDown(event) {
371 super.selectOnMouseDown(event);
372
373 if (this._editing)
374 return;
375
376 // Prevent selecting the nearest word on double click.
377 if (event.detail >= 2)
378 event.preventDefault();
379 }
380
381 /**
382 * @override
383 * @return {boolean}
384 */
385 ondblclick(event) {
386 if (this._editing || this._elementCloseTag)
387 return false;
388
389 if (this._startEditingTarget(/** @type {!Element} */ (event.target)))
390 return false;
391
392 if (this.isExpandable() && !this.expanded)
393 this.expand();
394 return false;
395 }
396
397 /**
398 * @return {boolean}
399 */
400 hasEditableNode() {
401 return !this._node.isShadowRoot() && !this._node.ancestorUserAgentShadowRoot ();
402 }
403
404 _insertInLastAttributePosition(tag, node) {
405 if (tag.getElementsByClassName('webkit-html-attribute').length > 0)
406 tag.insertBefore(node, tag.lastChild);
407 else {
408 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
409 tag.textContent = '';
410 tag.createTextChild('<' + nodeName);
411 tag.appendChild(node);
412 tag.createTextChild('>');
413 }
414 }
415
416 /**
417 * @param {!Element} eventTarget
418 * @return {boolean}
419 */
420 _startEditingTarget(eventTarget) {
421 if (this.treeOutline.selectedDOMNode() !== this._node)
422 return false;
423
424 if (this._node.nodeType() !== Node.ELEMENT_NODE && this._node.nodeType() !== Node.TEXT_NODE)
425 return false;
426
427 var textNode = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-text-no de');
428 if (textNode)
429 return this._startEditingTextNode(textNode);
430
431 var attribute = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-attrib ute');
432 if (attribute)
433 return this._startEditingAttribute(attribute, eventTarget);
434
435 var tagName = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-tag-name ');
436 if (tagName)
437 return this._startEditingTagName(tagName);
438
439 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass('add-attribute') ;
440 if (newAttribute)
441 return this._addNewAttribute();
442
443 return false;
444 }
445
446 /**
447 * @param {!Event} event
448 */
449 _showContextMenu(event) {
450 this.treeOutline.showContextMenu(this, event);
451 }
452
453 /**
454 * @param {!WebInspector.ContextMenu} contextMenu
455 * @param {!Event} event
456 */
457 populateTagContextMenu(contextMenu, event) {
458 // Add attribute-related actions.
459 var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(t his._node) : this;
460 contextMenu.appendItem(
461 WebInspector.UIString.capitalize('Add ^attribute'), treeElement._addNewA ttribute.bind(treeElement));
462
463 var attribute = event.target.enclosingNodeOrSelfWithClass('webkit-html-attri bute');
464 var newAttribute = event.target.enclosingNodeOrSelfWithClass('add-attribute' );
465 if (attribute && !newAttribute)
466 contextMenu.appendItem(
467 WebInspector.UIString.capitalize('Edit ^attribute'),
468 this._startEditingAttribute.bind(this, attribute, event.target));
469 this.populateNodeContextMenu(contextMenu);
470 WebInspector.ElementsTreeElement.populateForcedPseudoStateItems(contextMenu, treeElement.node());
471 contextMenu.appendSeparator();
472 this.populateScrollIntoView(contextMenu);
473 }
474
475 /**
476 * @param {!WebInspector.ContextMenu} contextMenu
477 */
478 populateScrollIntoView(contextMenu) {
479 contextMenu.appendItem(WebInspector.UIString.capitalize('Scroll into ^view') , this._scrollIntoView.bind(this));
480 }
481
482 populateTextContextMenu(contextMenu, textNode) {
483 if (!this._editing)
484 contextMenu.appendItem(
485 WebInspector.UIString.capitalize('Edit ^text'), this._startEditingText Node.bind(this, textNode));
486 this.populateNodeContextMenu(contextMenu);
487 }
488
489 populateNodeContextMenu(contextMenu) {
490 // Add free-form node-related actions.
491 var openTagElement = this._node[this.treeOutline.treeElementSymbol()] || thi s;
492 var isEditable = this.hasEditableNode();
493 if (isEditable && !this._editing)
494 contextMenu.appendItem(WebInspector.UIString('Edit as HTML'), this._editAs HTML.bind(this));
495 var isShadowRoot = this._node.isShadowRoot();
496
497 // Place it here so that all "Copy"-ing items stick together.
498 var copyMenu = contextMenu.appendSubMenuItem(WebInspector.UIString('Copy'));
499 var createShortcut = WebInspector.KeyboardShortcut.shortcutToString;
500 var modifier = WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta;
501 var treeOutline = this.treeOutline;
502 var menuItem;
503 if (!isShadowRoot) {
504 menuItem = copyMenu.appendItem(
505 WebInspector.UIString('Copy outerHTML'), treeOutline.performCopyOrCut. bind(treeOutline, false, this._node));
506 menuItem.setShortcut(createShortcut('V', modifier));
507 }
508 if (this._node.nodeType() === Node.ELEMENT_NODE)
509 copyMenu.appendItem(WebInspector.UIString.capitalize('Copy selector'), thi s._copyCSSPath.bind(this));
510 if (!isShadowRoot)
511 copyMenu.appendItem(WebInspector.UIString('Copy XPath'), this._copyXPath.b ind(this));
512 if (!isShadowRoot) {
513 menuItem = copyMenu.appendItem(
514 WebInspector.UIString('Cut element'), treeOutline.performCopyOrCut.bin d(treeOutline, true, this._node),
515 !this.hasEditableNode());
516 menuItem.setShortcut(createShortcut('X', modifier));
517 menuItem = copyMenu.appendItem(
518 WebInspector.UIString('Copy element'), treeOutline.performCopyOrCut.bi nd(treeOutline, false, this._node));
519 menuItem.setShortcut(createShortcut('C', modifier));
520 menuItem = copyMenu.appendItem(
521 WebInspector.UIString('Paste element'), treeOutline.pasteNode.bind(tre eOutline, this._node),
522 !treeOutline.canPaste(this._node));
523 menuItem.setShortcut(createShortcut('V', modifier));
524 }
525
526 contextMenu.appendSeparator();
527 menuItem = contextMenu.appendCheckboxItem(
528 WebInspector.UIString('Hide element'), treeOutline.toggleHideElement.bin d(treeOutline, this._node),
529 treeOutline.isToggledToHidden(this._node));
530 menuItem.setShortcut(WebInspector.shortcutRegistry.shortcutTitleForAction('e lements.hide-element'));
531
532 if (isEditable)
533 contextMenu.appendItem(WebInspector.UIString('Delete element'), this.remov e.bind(this));
534 contextMenu.appendSeparator();
535
536 contextMenu.appendItem(WebInspector.UIString('Expand all'), this.expandRecur sively.bind(this));
537 contextMenu.appendItem(WebInspector.UIString('Collapse all'), this.collapseR ecursively.bind(this));
538 contextMenu.appendSeparator();
539 }
540
541 _startEditing() {
542 if (this.treeOutline.selectedDOMNode() !== this._node)
543 return;
544
545 var listItem = this._listItemNode;
546
547 if (this._canAddAttributes) {
548 var attribute = listItem.getElementsByClassName('webkit-html-attribute')[0 ];
549 if (attribute)
550 return this._startEditingAttribute(
551 attribute, attribute.getElementsByClassName('webkit-html-attribute-v alue')[0]);
552
553 return this._addNewAttribute();
554 }
555
556 if (this._node.nodeType() === Node.TEXT_NODE) {
557 var textNode = listItem.getElementsByClassName('webkit-html-text-node')[0] ;
558 if (textNode)
559 return this._startEditingTextNode(textNode);
560 return;
561 }
562 }
563
564 _addNewAttribute() {
565 // Cannot just convert the textual html into an element without
566 // a parent node. Use a temporary span container for the HTML.
567 var container = createElement('span');
568 this._buildAttributeDOM(container, ' ', '', null);
569 var attr = container.firstElementChild;
570 attr.style.marginLeft = '2px'; // overrides the .editing margin rule
571 attr.style.marginRight = '2px'; // overrides the .editing margin rule
572
573 var tag = this.listItemElement.getElementsByClassName('webkit-html-tag')[0];
574 this._insertInLastAttributePosition(tag, attr);
575 attr.scrollIntoViewIfNeeded(true);
576 return this._startEditingAttribute(attr, attr);
577 }
578
579 _triggerEditAttribute(attributeName) {
580 var attributeElements = this.listItemElement.getElementsByClassName('webkit- html-attribute-name');
581 for (var i = 0, len = attributeElements.length; i < len; ++i) {
582 if (attributeElements[i].textContent === attributeName) {
583 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.next Sibling) {
584 if (elem.nodeType !== Node.ELEMENT_NODE)
585 continue;
586
587 if (elem.classList.contains('webkit-html-attribute-value'))
588 return this._startEditingAttribute(elem.parentNode, elem);
589 }
590 }
591 }
592 }
593
594 _startEditingAttribute(attribute, elementForSelection) {
595 console.assert(this.listItemElement.isAncestor(attribute));
596
597 if (WebInspector.isBeingEdited(attribute))
598 return true;
599
600 var attributeNameElement = attribute.getElementsByClassName('webkit-html-att ribute-name')[0];
601 if (!attributeNameElement)
602 return false;
603
604 var attributeName = attributeNameElement.textContent;
605 var attributeValueElement = attribute.getElementsByClassName('webkit-html-at tribute-value')[0];
606
607 // Make sure elementForSelection is not a child of attributeValueElement.
608 elementForSelection =
609 attributeValueElement.isAncestor(elementForSelection) ? attributeValueEl ement : elementForSelection;
610
611 function removeZeroWidthSpaceRecursive(node) {
612 if (node.nodeType === Node.TEXT_NODE) {
613 node.nodeValue = node.nodeValue.replace(/\u200B/g, '');
614 return;
615 }
616
617 if (node.nodeType !== Node.ELEMENT_NODE)
618 return;
619
620 for (var child = node.firstChild; child; child = child.nextSibling)
621 removeZeroWidthSpaceRecursive(child);
622 }
623
624 var attributeValue = attributeName && attributeValueElement ? this._node.get Attribute(attributeName) : undefined;
625 if (attributeValue !== undefined)
626 attributeValueElement.setTextContentTruncatedIfNeeded(
627 attributeValue, WebInspector.UIString('<value is too large to edit>')) ;
628
629 // Remove zero-width spaces that were added by nodeTitleInfo.
630 removeZeroWidthSpaceRecursive(attribute);
631
632 var config = new WebInspector.InplaceEditor.Config(
633 this._attributeEditingCommitted.bind(this), this._editingCancelled.bind( this), attributeName);
634
635 /**
636 * @param {!Event} event
637 * @return {string}
638 */
639 function postKeyDownFinishHandler(event) {
640 WebInspector.handleElementValueModifications(event, attribute);
641 return '';
642 }
643
644 if (!attributeValueElement.textContent.asParsedURL())
645 config.setPostKeydownFinishHandler(postKeyDownFinishHandler);
646
647 this._editing = WebInspector.InplaceEditor.startEditing(attribute, config);
648
649 this.listItemElement.getComponentSelection().setBaseAndExtent(elementForSele ction, 0, elementForSelection, 1);
650
651 return true;
652 }
653
654 /**
655 * @param {!Element} textNodeElement
656 */
657 _startEditingTextNode(textNodeElement) {
658 if (WebInspector.isBeingEdited(textNodeElement))
659 return true;
660
661 var textNode = this._node;
662 // We only show text nodes inline in elements if the element only
663 // has a single child, and that child is a text node.
664 if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
665 textNode = textNode.firstChild;
666
667 var container = textNodeElement.enclosingNodeOrSelfWithClass('webkit-html-te xt-node');
668 if (container)
669 container.textContent = textNode.nodeValue(); // Strip the CSS or JS high lighting if present.
670 var config = new WebInspector.InplaceEditor.Config(
671 this._textNodeEditingCommitted.bind(this, textNode), this._editingCancel led.bind(this));
672 this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, con fig);
673 this.listItemElement.getComponentSelection().setBaseAndExtent(textNodeElemen t, 0, textNodeElement, 1);
674
675 return true;
676 }
677
678 /**
679 * @param {!Element=} tagNameElement
680 */
681 _startEditingTagName(tagNameElement) {
682 if (!tagNameElement) {
683 tagNameElement = this.listItemElement.getElementsByClassName('webkit-html- tag-name')[0];
684 if (!tagNameElement)
685 return false;
686 }
687
688 var tagName = tagNameElement.textContent;
689 if (WebInspector.ElementsTreeElement.EditTagBlacklist.has(tagName.toLowerCas e()))
690 return false;
691
692 if (WebInspector.isBeingEdited(tagNameElement))
693 return true;
694
695 var closingTagElement = this._distinctClosingTagElement();
696
697 /**
698 * @param {!Event} event
699 */
700 function keyupListener(event) {
701 if (closingTagElement)
702 closingTagElement.textContent = '</' + tagNameElement.textContent + '>';
703 }
704
705 /**
706 * @param {!Element} element
707 * @param {string} newTagName
708 * @this {WebInspector.ElementsTreeElement}
709 */
710 function editingComitted(element, newTagName) {
711 tagNameElement.removeEventListener('keyup', keyupListener, false);
712 this._tagNameEditingCommitted.apply(this, arguments);
713 }
714
715 /**
716 * @this {WebInspector.ElementsTreeElement}
717 */
718 function editingCancelled() {
719 tagNameElement.removeEventListener('keyup', keyupListener, false);
720 this._editingCancelled.apply(this, arguments);
721 }
722
723 tagNameElement.addEventListener('keyup', keyupListener, false);
724
725 var config =
726 new WebInspector.InplaceEditor.Config(editingComitted.bind(this), editin gCancelled.bind(this), tagName);
727 this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, conf ig);
728 this.listItemElement.getComponentSelection().setBaseAndExtent(tagNameElement , 0, tagNameElement, 1);
729 return true;
730 }
731
732 /**
733 * @param {function(string, string)} commitCallback
734 * @param {function()} disposeCallback
735 * @param {?Protocol.Error} error
736 * @param {string} initialValue
737 */
738 _startEditingAsHTML(commitCallback, disposeCallback, error, initialValue) {
739 if (error)
740 return;
741 if (this._editing)
742 return;
743
744 function consume(event) {
745 if (event.eventPhase === Event.AT_TARGET)
746 event.consume(true);
747 }
748
749 initialValue = this._convertWhitespaceToEntities(initialValue).text;
750
751 this._htmlEditElement = createElement('div');
752 this._htmlEditElement.className = 'source-code elements-tree-editor';
753
754 // Hide header items.
755 var child = this.listItemElement.firstChild;
756 while (child) {
757 child.style.display = 'none';
758 child = child.nextSibling;
759 }
760 // Hide children item.
761 if (this._childrenListNode)
762 this._childrenListNode.style.display = 'none';
763 // Append editor.
764 this.listItemElement.appendChild(this._htmlEditElement);
765 this.listItemElement.classList.add('editing-as-html');
766 this.treeOutline.element.addEventListener('mousedown', consume, false);
767
768 /**
769 * @param {!Element} element
770 * @param {string} newValue
771 * @this {WebInspector.ElementsTreeElement}
772 */
773 function commit(element, newValue) {
774 commitCallback(initialValue, newValue);
775 dispose.call(this);
776 }
777
778 /**
779 * @this {WebInspector.ElementsTreeElement}
780 */
781 function dispose() {
782 disposeCallback();
783 delete this._editing;
784 this.treeOutline.setMultilineEditing(null);
785
786 this.listItemElement.classList.remove('editing-as-html');
787 // Remove editor.
788 this.listItemElement.removeChild(this._htmlEditElement);
789 delete this._htmlEditElement;
790 // Unhide children item.
791 if (this._childrenListNode)
792 this._childrenListNode.style.removeProperty('display');
793 // Unhide header items.
794 var child = this.listItemElement.firstChild;
795 while (child) {
796 child.style.removeProperty('display');
797 child = child.nextSibling;
798 }
799
800 this.treeOutline.element.removeEventListener('mousedown', consume, false);
801 this.treeOutline.focus();
802 }
803
804 var config = new WebInspector.InplaceEditor.Config(commit.bind(this), dispos e.bind(this));
805 config.setMultilineOptions(
806 initialValue, {name: 'xml', htmlMode: true}, 'web-inspector-html',
807 WebInspector.moduleSetting('domWordWrap').get(), true);
808 WebInspector.InplaceEditor.startMultilineEditing(this._htmlEditElement, conf ig).then(markAsBeingEdited.bind(this));
809
810 /**
811 * @param {!Object} controller
812 * @this {WebInspector.ElementsTreeElement}
813 */
814 function markAsBeingEdited(controller) {
815 this._editing = /** @type {!WebInspector.InplaceEditor.Controller} */ (con troller);
816 this._editing.setWidth(this.treeOutline.visibleWidth() - this._computeLeft Indent());
817 this.treeOutline.setMultilineEditing(this._editing);
818 }
819 }
820
821 _attributeEditingCommitted(element, newText, oldText, attributeName, moveDirec tion) {
822 delete this._editing;
823
824 var treeOutline = this.treeOutline;
825
826 /**
827 * @param {?Protocol.Error=} error
828 * @this {WebInspector.ElementsTreeElement}
829 */
830 function moveToNextAttributeIfNeeded(error) {
831 if (error)
832 this._editingCancelled(element, attributeName);
833
834 if (!moveDirection)
835 return;
836
837 treeOutline.runPendingUpdates();
838
839 // Search for the attribute's position, and then decide where to move to.
840 var attributes = this._node.attributes();
841 for (var i = 0; i < attributes.length; ++i) {
842 if (attributes[i].name !== attributeName)
843 continue;
844
845 if (moveDirection === 'backward') {
846 if (i === 0)
847 this._startEditingTagName();
848 else
849 this._triggerEditAttribute(attributes[i - 1].name);
850 } else {
851 if (i === attributes.length - 1)
852 this._addNewAttribute();
853 else
854 this._triggerEditAttribute(attributes[i + 1].name);
855 }
856 return;
857 }
858
859 // Moving From the "New Attribute" position.
860 if (moveDirection === 'backward') {
861 if (newText === ' ') {
862 // Moving from "New Attribute" that was not edited
863 if (attributes.length > 0)
864 this._triggerEditAttribute(attributes[attributes.length - 1].name);
865 } else {
866 // Moving from "New Attribute" that holds new value
867 if (attributes.length > 1)
868 this._triggerEditAttribute(attributes[attributes.length - 2].name);
869 }
870 } else if (moveDirection === 'forward') {
871 if (!newText.isWhitespace())
872 this._addNewAttribute();
873 else
874 this._startEditingTagName();
875 }
876 }
877
878 if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
879 this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeede d.bind(this));
880 return;
881 }
882
883 this.updateTitle();
884 moveToNextAttributeIfNeeded.call(this);
885 }
886
887 _tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection) {
888 delete this._editing;
889 var self = this;
890
891 function cancel() {
892 var closingTagElement = self._distinctClosingTagElement();
893 if (closingTagElement)
894 closingTagElement.textContent = '</' + tagName + '>';
895
896 self._editingCancelled(element, tagName);
897 moveToNextAttributeIfNeeded.call(self);
898 }
899
900 /**
901 * @this {WebInspector.ElementsTreeElement}
902 */
903 function moveToNextAttributeIfNeeded() {
904 if (moveDirection !== 'forward') {
905 this._addNewAttribute();
906 return;
907 }
908
909 var attributes = this._node.attributes();
910 if (attributes.length > 0)
911 this._triggerEditAttribute(attributes[0].name);
912 else
913 this._addNewAttribute();
914 }
915
916 newText = newText.trim();
917 if (newText === oldText) {
918 cancel();
919 return;
920 }
921
922 var treeOutline = this.treeOutline;
923 var wasExpanded = this.expanded;
924
925 function changeTagNameCallback(error, nodeId) {
926 if (error || !nodeId) {
927 cancel();
928 return;
929 }
930 var newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error, node Id);
931 moveToNextAttributeIfNeeded.call(newTreeItem);
932 }
933 this._node.setNodeName(newText, changeTagNameCallback);
934 }
935
936 /**
937 * @param {!WebInspector.DOMNode} textNode
938 * @param {!Element} element
939 * @param {string} newText
940 */
941 _textNodeEditingCommitted(textNode, element, newText) {
942 delete this._editing;
943
944 /**
945 * @this {WebInspector.ElementsTreeElement}
946 */
947 function callback() {
948 this.updateTitle();
949 }
950 textNode.setNodeValue(newText, callback.bind(this));
951 }
952
953 /**
954 * @param {!Element} element
955 * @param {*} context
956 */
957 _editingCancelled(element, context) {
958 delete this._editing;
959
960 // Need to restore attributes structure.
961 this.updateTitle();
962 }
963
964 /**
965 * @return {!Element}
966 */
967 _distinctClosingTagElement() {
968 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
969
970 // For an expanded element, it will be the last element with class "close"
971 // in the child element list.
972 if (this.expanded) {
973 var closers = this._childrenListNode.querySelectorAll('.close');
974 return closers[closers.length - 1];
975 }
976
977 // Remaining cases are single line non-expanded elements with a closing
978 // tag, or HTML elements without a closing tag (such as <br>). Return
979 // null in the case where there isn't a closing tag.
980 var tags = this.listItemElement.getElementsByClassName('webkit-html-tag');
981 return (tags.length === 1 ? null : tags[tags.length - 1]);
982 }
983
984 /**
985 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord=} updateRecord
986 * @param {boolean=} onlySearchQueryChanged
987 */
988 updateTitle(updateRecord, onlySearchQueryChanged) {
989 // If we are editing, return early to prevent canceling the edit.
990 // After editing is committed updateTitle will be called.
991 if (this._editing)
992 return;
993
994 if (onlySearchQueryChanged) {
995 this._hideSearchHighlight();
996 } else {
997 var nodeInfo = this._nodeTitleInfo(updateRecord || null);
998 if (this._node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE && this._node.is InShadowTree() &&
999 this._node.shadowRootType()) {
1000 this.childrenListElement.classList.add('shadow-root');
1001 var depth = 4;
1002 for (var node = this._node; depth && node; node = node.parentNode) {
1003 if (node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE)
1004 depth--;
1005 }
1006 if (!depth)
1007 this.childrenListElement.classList.add('shadow-root-deep');
1008 else
1009 this.childrenListElement.classList.add('shadow-root-depth-' + depth);
1010 }
1011 var highlightElement = createElement('span');
1012 highlightElement.className = 'highlight';
1013 highlightElement.appendChild(nodeInfo);
1014 this.title = highlightElement;
1015 this.updateDecorations();
1016 this.listItemElement.insertBefore(this._gutterContainer, this.listItemElem ent.firstChild);
1017 delete this._highlightResult;
1018 }
1019
1020 delete this.selectionElement;
1021 if (this.selected)
1022 this._createSelection();
1023 this._preventFollowingLinksOnDoubleClick();
1024 this._highlightSearchResults();
1025 }
1026
1027 /**
1028 * @return {number}
1029 */
1030 _computeLeftIndent() {
1031 var treeElement = this.parent;
1032 var depth = 0;
1033 while (treeElement !== null) {
1034 depth++;
1035 treeElement = treeElement.parent;
1036 }
1037
1038 /** Keep it in sync with elementsTreeOutline.css **/
1039 return 12 * (depth - 2) + (this.isExpandable() ? 1 : 12);
1040 }
1041
1042 updateDecorations() {
1043 this._gutterContainer.style.left = (-this._computeLeftIndent()) + 'px';
1044
1045 if (this.isClosingTag())
1046 return;
1047
1048 var node = this._node;
1049 if (node.nodeType() !== Node.ELEMENT_NODE)
1050 return;
1051
1052 if (!this.treeOutline._decoratorExtensions)
1053 /** @type {!Array.<!Runtime.Extension>} */
1054 this.treeOutline._decoratorExtensions = runtime.extensions(WebInspector.DO MPresentationUtils.MarkerDecorator);
1055
1056 var markerToExtension = new Map();
1057 for (var i = 0; i < this.treeOutline._decoratorExtensions.length; ++i)
1058 markerToExtension.set(
1059 this.treeOutline._decoratorExtensions[i].descriptor()['marker'], this. treeOutline._decoratorExtensions[i]);
1060
1061 var promises = [];
1062 var decorations = [];
1063 var descendantDecorations = [];
1064 node.traverseMarkers(visitor);
1065
1066 /**
1067 * @param {!WebInspector.DOMNode} n
1068 * @param {string} marker
1069 */
1070 function visitor(n, marker) {
1071 var extension = markerToExtension.get(marker);
1072 if (!extension)
1073 return;
1074 promises.push(extension.instance().then(collectDecoration.bind(null, n)));
1075 }
1076
1077 /**
1078 * @param {!WebInspector.DOMNode} n
1079 * @param {!WebInspector.DOMPresentationUtils.MarkerDecorator} decorator
1080 */
1081 function collectDecoration(n, decorator) {
1082 var decoration = decorator.decorate(n);
1083 if (!decoration)
1084 return;
1085 (n === node ? decorations : descendantDecorations).push(decoration);
1086 }
1087
1088 Promise.all(promises).then(updateDecorationsUI.bind(this));
1089
1090 /**
1091 * @this {WebInspector.ElementsTreeElement}
1092 */
1093 function updateDecorationsUI() {
1094 this._decorationsElement.removeChildren();
1095 this._decorationsElement.classList.add('hidden');
1096 this._gutterContainer.classList.toggle('has-decorations', decorations.leng th || descendantDecorations.length);
1097
1098 if (!decorations.length && !descendantDecorations.length)
1099 return;
1100
1101 var colors = new Set();
1102 var titles = createElement('div');
1103
1104 for (var decoration of decorations) {
1105 var titleElement = titles.createChild('div');
1106 titleElement.textContent = decoration.title;
1107 colors.add(decoration.color);
1108 }
1109 if (this.expanded && !decorations.length)
1110 return;
1111
1112 var descendantColors = new Set();
1113 if (descendantDecorations.length) {
1114 var element = titles.createChild('div');
1115 element.textContent = WebInspector.UIString('Children:');
1116 for (var decoration of descendantDecorations) {
1117 element = titles.createChild('div');
1118 element.style.marginLeft = '15px';
1119 element.textContent = decoration.title;
1120 descendantColors.add(decoration.color);
1121 }
1122 }
1123
1124 var offset = 0;
1125 processColors.call(this, colors, 'elements-gutter-decoration');
1126 if (!this.expanded)
1127 processColors.call(this, descendantColors, 'elements-gutter-decoration e lements-has-decorated-children');
1128 WebInspector.Tooltip.install(this._decorationsElement, titles);
1129
1130 /**
1131 * @param {!Set<string>} colors
1132 * @param {string} className
1133 * @this {WebInspector.ElementsTreeElement}
1134 */
1135 function processColors(colors, className) {
1136 for (var color of colors) {
1137 var child = this._decorationsElement.createChild('div', className);
1138 this._decorationsElement.classList.remove('hidden');
1139 child.style.backgroundColor = color;
1140 child.style.borderColor = color;
1141 if (offset)
1142 child.style.marginLeft = offset + 'px';
1143 offset += 3;
1144 }
1145 }
1146 }
1147 }
1148
1149 /**
1150 * @param {!Node} parentElement
1151 * @param {string} name
1152 * @param {string} value
1153 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1154 * @param {boolean=} forceValue
1155 * @param {!WebInspector.DOMNode=} node
1156 */
1157 _buildAttributeDOM(parentElement, name, value, updateRecord, forceValue, node) {
1158 var closingPunctuationRegex = /[\/;:\)\]\}]/g;
1159 var highlightIndex = 0;
1160 var highlightCount;
1161 var additionalHighlightOffset = 0;
1162 var result;
1163
1164 /**
1165 * @param {string} match
1166 * @param {number} replaceOffset
1167 * @return {string}
1168 */
1169 function replacer(match, replaceOffset) {
1170 while (highlightIndex < highlightCount && result.entityRanges[highlightInd ex].offset < replaceOffset) {
1171 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
1172 ++highlightIndex;
1173 }
1174 additionalHighlightOffset += 1;
1175 return match + '\u200B';
1176 }
1177
1178 /**
1179 * @param {!Element} element
1180 * @param {string} value
1181 * @this {WebInspector.ElementsTreeElement}
1182 */
1183 function setValueWithEntities(element, value) {
1184 result = this._convertWhitespaceToEntities(value);
1185 highlightCount = result.entityRanges.length;
1186 value = result.text.replace(closingPunctuationRegex, replacer);
1187 while (highlightIndex < highlightCount) {
1188 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
1189 ++highlightIndex;
1190 }
1191 element.setTextContentTruncatedIfNeeded(value);
1192 WebInspector.highlightRangesWithStyleClass(element, result.entityRanges, ' webkit-html-entity-value');
1193 }
1194
1195 var hasText = (forceValue || value.length > 0);
1196 var attrSpanElement = parentElement.createChild('span', 'webkit-html-attribu te');
1197 var attrNameElement = attrSpanElement.createChild('span', 'webkit-html-attri bute-name');
1198 attrNameElement.textContent = name;
1199
1200 if (hasText)
1201 attrSpanElement.createTextChild('=\u200B"');
1202
1203 var attrValueElement = attrSpanElement.createChild('span', 'webkit-html-attr ibute-value');
1204
1205 if (updateRecord && updateRecord.isAttributeModified(name))
1206 WebInspector.runCSSAnimationOnce(hasText ? attrValueElement : attrNameElem ent, 'dom-update-highlight');
1207
1208 /**
1209 * @this {WebInspector.ElementsTreeElement}
1210 * @param {string} value
1211 * @return {!Element}
1212 */
1213 function linkifyValue(value) {
1214 var rewrittenHref = node.resolveURL(value);
1215 if (rewrittenHref === null) {
1216 var span = createElement('span');
1217 setValueWithEntities.call(this, span, value);
1218 return span;
1219 }
1220 value = value.replace(closingPunctuationRegex, '$&\u200B');
1221 if (value.startsWith('data:'))
1222 value = value.trimMiddle(60);
1223 var anchor = WebInspector.linkifyURLAsNode(rewrittenHref, value, '', node. nodeName().toLowerCase() === 'a');
1224 anchor.preventFollow = true;
1225 return anchor;
1226 }
1227
1228 if (node && (name === 'src' || name === 'href')) {
1229 attrValueElement.appendChild(linkifyValue.call(this, value));
1230 } else if (
1231 node && (node.nodeName().toLowerCase() === 'img' || node.nodeName().toLo werCase() === 'source') &&
1232 name === 'srcset') {
1233 var sources = value.split(',');
1234 for (var i = 0; i < sources.length; ++i) {
1235 if (i > 0)
1236 attrValueElement.createTextChild(', ');
1237 var source = sources[i].trim();
1238 var indexOfSpace = source.indexOf(' ');
1239 var url, tail;
1240
1241 if (indexOfSpace === -1) {
1242 url = source;
1243 } else {
1244 url = source.substring(0, indexOfSpace);
1245 tail = source.substring(indexOfSpace);
1246 }
1247
1248 attrValueElement.appendChild(linkifyValue.call(this, url));
1249
1250 if (tail)
1251 attrValueElement.createTextChild(tail);
1252 }
1253 } else {
1254 setValueWithEntities.call(this, attrValueElement, value);
1255 }
1256
1257 if (hasText)
1258 attrSpanElement.createTextChild('"');
1259 }
1260
1261 /**
1262 * @param {!Node} parentElement
1263 * @param {string} pseudoElementName
1264 */
1265 _buildPseudoElementDOM(parentElement, pseudoElementName) {
1266 var pseudoElement = parentElement.createChild('span', 'webkit-html-pseudo-el ement');
1267 pseudoElement.textContent = '::' + pseudoElementName;
1268 parentElement.createTextChild('\u200B');
1269 }
1270
1271 /**
1272 * @param {!Node} parentElement
1273 * @param {string} tagName
1274 * @param {boolean} isClosingTag
1275 * @param {boolean} isDistinctTreeElement
1276 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1277 */
1278 _buildTagDOM(parentElement, tagName, isClosingTag, isDistinctTreeElement, upda teRecord) {
1279 var node = this._node;
1280 var classes = ['webkit-html-tag'];
1281 if (isClosingTag && isDistinctTreeElement)
1282 classes.push('close');
1283 var tagElement = parentElement.createChild('span', classes.join(' '));
1284 tagElement.createTextChild('<');
1285 var tagNameElement =
1286 tagElement.createChild('span', isClosingTag ? 'webkit-html-close-tag-nam e' : 'webkit-html-tag-name');
1287 tagNameElement.textContent = (isClosingTag ? '/' : '') + tagName;
1288 if (!isClosingTag) {
1289 if (node.hasAttributes()) {
1290 var attributes = node.attributes();
1291 for (var i = 0; i < attributes.length; ++i) {
1292 var attr = attributes[i];
1293 tagElement.createTextChild(' ');
1294 this._buildAttributeDOM(tagElement, attr.name, attr.value, updateRecor d, false, node);
1295 }
1296 }
1297 if (updateRecord) {
1298 var hasUpdates = updateRecord.hasRemovedAttributes() || updateRecord.has RemovedChildren();
1299 hasUpdates |= !this.expanded && updateRecord.hasChangedChildren();
1300 if (hasUpdates)
1301 WebInspector.runCSSAnimationOnce(tagNameElement, 'dom-update-highlight ');
1302 }
1303 }
1304
1305 tagElement.createTextChild('>');
1306 parentElement.createTextChild('\u200B');
1307 }
1308
1309 /**
1310 * @param {string} text
1311 * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
1312 */
1313 _convertWhitespaceToEntities(text) {
1314 var result = '';
1315 var lastIndexAfterEntity = 0;
1316 var entityRanges = [];
1317 var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
1318 for (var i = 0, size = text.length; i < size; ++i) {
1319 var char = text.charAt(i);
1320 if (charToEntity[char]) {
1321 result += text.substring(lastIndexAfterEntity, i);
1322 var entityValue = '&' + charToEntity[char] + ';';
1323 entityRanges.push({offset: result.length, length: entityValue.length});
1324 result += entityValue;
1325 lastIndexAfterEntity = i + 1;
1326 }
1327 }
1328 if (result)
1329 result += text.substring(lastIndexAfterEntity);
1330 return {text: result || text, entityRanges: entityRanges};
1331 }
1332
1333 /**
1334 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1335 * @return {!DocumentFragment} result
1336 */
1337 _nodeTitleInfo(updateRecord) {
1338 var node = this._node;
1339 var titleDOM = createDocumentFragment();
1340
1341 switch (node.nodeType()) {
1342 case Node.ATTRIBUTE_NODE:
1343 this._buildAttributeDOM(
1344 titleDOM, /** @type {string} */ (node.name), /** @type {string} */ ( node.value), updateRecord, true);
1345 break;
1346
1347 case Node.ELEMENT_NODE:
1348 var pseudoType = node.pseudoType();
1349 if (pseudoType) {
1350 this._buildPseudoElementDOM(titleDOM, pseudoType);
1351 break;
1352 }
1353
1354 var tagName = node.nodeNameInCorrectCase();
1355 if (this._elementCloseTag) {
1356 this._buildTagDOM(titleDOM, tagName, true, true, updateRecord);
1357 break;
1358 }
1359
1360 this._buildTagDOM(titleDOM, tagName, false, false, updateRecord);
1361
1362 if (this.isExpandable()) {
1363 if (!this.expanded) {
1364 var textNodeElement = titleDOM.createChild('span', 'webkit-html-text -node bogus');
1365 textNodeElement.textContent = '\u2026';
1366 titleDOM.createTextChild('\u200B');
1367 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1368 }
1369 break;
1370 }
1371
1372 if (WebInspector.ElementsTreeElement.canShowInlineText(node)) {
1373 var textNodeElement = titleDOM.createChild('span', 'webkit-html-text-n ode');
1374 var result = this._convertWhitespaceToEntities(node.firstChild.nodeVal ue());
1375 textNodeElement.textContent = result.text;
1376 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.ent ityRanges, 'webkit-html-entity-value');
1377 titleDOM.createTextChild('\u200B');
1378 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1379 if (updateRecord && updateRecord.hasChangedChildren())
1380 WebInspector.runCSSAnimationOnce(textNodeElement, 'dom-update-highli ght');
1381 if (updateRecord && updateRecord.isCharDataModified())
1382 WebInspector.runCSSAnimationOnce(textNodeElement, 'dom-update-highli ght');
1383 break;
1384 }
1385
1386 if (this.treeOutline.isXMLMimeType ||
1387 !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements.has(ta gName))
1388 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1389 break;
1390
1391 case Node.TEXT_NODE:
1392 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === 'scr ipt') {
1393 var newNode = titleDOM.createChild('span', 'webkit-html-text-node webk it-html-js-node');
1394 var text = node.nodeValue();
1395 newNode.textContent = text.startsWith('\n') ? text.substring(1) : text ;
1396
1397 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighte r('text/javascript', true);
1398 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSe archHighlight.bind(this));
1399 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() = == 'style') {
1400 var newNode = titleDOM.createChild('span', 'webkit-html-text-node webk it-html-css-node');
1401 var text = node.nodeValue();
1402 newNode.textContent = text.startsWith('\n') ? text.substring(1) : text ;
1403
1404 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter('text /css', true);
1405 cssSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSearchHig hlight.bind(this));
1406 } else {
1407 titleDOM.createTextChild('"');
1408 var textNodeElement = titleDOM.createChild('span', 'webkit-html-text-n ode');
1409 var result = this._convertWhitespaceToEntities(node.nodeValue());
1410 textNodeElement.textContent = result.text;
1411 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.ent ityRanges, 'webkit-html-entity-value');
1412 titleDOM.createTextChild('"');
1413 if (updateRecord && updateRecord.isCharDataModified())
1414 WebInspector.runCSSAnimationOnce(textNodeElement, 'dom-update-highli ght');
1415 }
1416 break;
1417
1418 case Node.COMMENT_NODE:
1419 var commentElement = titleDOM.createChild('span', 'webkit-html-comment') ;
1420 commentElement.createTextChild('<!--' + node.nodeValue() + '-->');
1421 break;
1422
1423 case Node.DOCUMENT_TYPE_NODE:
1424 var docTypeElement = titleDOM.createChild('span', 'webkit-html-doctype') ;
1425 docTypeElement.createTextChild('<!DOCTYPE ' + node.nodeName());
1426 if (node.publicId) {
1427 docTypeElement.createTextChild(' PUBLIC "' + node.publicId + '"');
1428 if (node.systemId)
1429 docTypeElement.createTextChild(' "' + node.systemId + '"');
1430 } else if (node.systemId)
1431 docTypeElement.createTextChild(' SYSTEM "' + node.systemId + '"');
1432
1433 if (node.internalSubset)
1434 docTypeElement.createTextChild(' [' + node.internalSubset + ']');
1435
1436 docTypeElement.createTextChild('>');
1437 break;
1438
1439 case Node.CDATA_SECTION_NODE:
1440 var cdataElement = titleDOM.createChild('span', 'webkit-html-text-node') ;
1441 cdataElement.createTextChild('<![CDATA[' + node.nodeValue() + ']]>');
1442 break;
1443
1444 case Node.DOCUMENT_FRAGMENT_NODE:
1445 var fragmentElement = titleDOM.createChild('span', 'webkit-html-fragment ');
1446 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhite space();
1447 break;
1448 default:
1449 titleDOM.createTextChild(node.nodeNameInCorrectCase().collapseWhitespace ());
1450 }
1451
1452 /**
1453 * @this {WebInspector.ElementsTreeElement}
1454 */
1455 function updateSearchHighlight() {
1456 delete this._highlightResult;
1457 this._highlightSearchResults();
1458 }
1459
1460 return titleDOM;
1461 }
1462
1463 remove() {
1464 if (this._node.pseudoType())
1465 return;
1466 var parentElement = this.parent;
1467 if (!parentElement)
1468 return;
1469
1470 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCU MENT_NODE)
1471 return;
1472 this._node.removeNode();
1473 }
1474
1475 /**
1476 * @param {function(boolean)=} callback
1477 * @param {boolean=} startEditing
1478 */
1479 toggleEditAsHTML(callback, startEditing) {
1480 if (this._editing && this._htmlEditElement && WebInspector.isBeingEdited(thi s._htmlEditElement)) {
1481 this._editing.commit();
1482 return;
1483 }
1484
1485 if (startEditing === false)
1486 return;
1487
1488 /**
1489 * @param {?Protocol.Error} error
1490 */
1491 function selectNode(error) {
1492 if (callback)
1493 callback(!error);
1494 }
1495
1496 /**
1497 * @param {string} initialValue
1498 * @param {string} value
1499 */
1500 function commitChange(initialValue, value) {
1501 if (initialValue !== value)
1502 node.setOuterHTML(value, selectNode);
1503 }
1504
1505 function disposeCallback() {
1506 if (callback)
1507 callback(false);
1508 }
1509
1510 var node = this._node;
1511 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange, disposeC allback));
1512 }
1513
1514 _copyCSSPath() {
1515 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath(thi s._node, true));
1516 }
1517
1518 _copyXPath() {
1519 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(this. _node, true));
1520 }
1521
1522 _highlightSearchResults() {
1523 if (!this._searchQuery || !this._searchHighlightsVisible)
1524 return;
1525 this._hideSearchHighlight();
1526
1527 var text = this.listItemElement.textContent;
1528 var regexObject = createPlainTextSearchRegex(this._searchQuery, 'gi');
1529
1530 var match = regexObject.exec(text);
1531 var matchRanges = [];
1532 while (match) {
1533 matchRanges.push(new WebInspector.SourceRange(match.index, match[0].length ));
1534 match = regexObject.exec(text);
1535 }
1536
1537 // Fall back for XPath, etc. matches.
1538 if (!matchRanges.length)
1539 matchRanges.push(new WebInspector.SourceRange(0, text.length));
1540
1541 this._highlightResult = [];
1542 WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this. _highlightResult);
1543 }
1544
1545 _scrollIntoView() {
1546 function scrollIntoViewCallback(object) {
1547 /**
1548 * @suppressReceiverCheck
1549 * @this {!Element}
1550 */
1551 function scrollIntoView() {
1552 this.scrollIntoViewIfNeeded(true);
1553 }
1554
1555 if (object)
1556 object.callFunction(scrollIntoView);
1557 }
1558
1559 this._node.resolveToObject('', scrollIntoViewCallback);
1560 }
1561
1562 _editAsHTML() {
1563 var promise = WebInspector.Revealer.revealPromise(this.node());
1564 promise.then(() => WebInspector.actionRegistry.action('elements.edit-as-html ').execute());
1565 }
53 }; 1566 };
54 1567
55 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500; 1568 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
56 1569
57 // A union of HTML4 and HTML5-Draft elements that explicitly 1570 // A union of HTML4 and HTML5-Draft elements that explicitly
58 // or implicitly (for HTML5) forbid the closing tag. 1571 // or implicitly (for HTML5) forbid the closing tag.
59 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = new Set([ 1572 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = new Set([
60 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "fram e", 1573 'area', 'base', 'basefont', 'br', 'canvas', 'col', 'command', 'embed', 'frame', 'hr',
61 "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source ", "track", "wbr" 1574 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'
62 ]); 1575 ]);
63 1576
64 // These tags we do not allow editing their tag name. 1577 // These tags we do not allow editing their tag name.
65 WebInspector.ElementsTreeElement.EditTagBlacklist = new Set([ 1578 WebInspector.ElementsTreeElement.EditTagBlacklist = new Set(['html', 'head', 'bo dy']);
66 "html", "head", "body" 1579
67 ]); 1580
68
69 /**
70 * @param {!WebInspector.ElementsTreeElement} treeElement
71 */
72 WebInspector.ElementsTreeElement.animateOnDOMUpdate = function(treeElement)
73 {
74 var tagName = treeElement.listItemElement.querySelector(".webkit-html-tag-na me");
75 WebInspector.runCSSAnimationOnce(tagName || treeElement.listItemElement, "do m-update-highlight");
76 };
77
78 /**
79 * @param {!WebInspector.DOMNode} node
80 * @return {!Array<!WebInspector.DOMNode>}
81 */
82 WebInspector.ElementsTreeElement.visibleShadowRoots = function(node)
83 {
84 var roots = node.shadowRoots();
85 if (roots.length && !WebInspector.moduleSetting("showUAShadowDOM").get())
86 roots = roots.filter(filter);
87
88 /**
89 * @param {!WebInspector.DOMNode} root
90 */
91 function filter(root)
92 {
93 return root.shadowRootType() !== WebInspector.DOMNode.ShadowRootTypes.Us erAgent;
94 }
95 return roots;
96 };
97
98 /**
99 * @param {!WebInspector.DOMNode} node
100 * @return {boolean}
101 */
102 WebInspector.ElementsTreeElement.canShowInlineText = function(node)
103 {
104 if (node.importedDocument() || node.templateContent() || WebInspector.Elemen tsTreeElement.visibleShadowRoots(node).length || node.hasPseudoElements())
105 return false;
106 if (node.nodeType() !== Node.ELEMENT_NODE)
107 return false;
108 if (!node.firstChild || node.firstChild !== node.lastChild || node.firstChil d.nodeType() !== Node.TEXT_NODE)
109 return false;
110 var textChild = node.firstChild;
111 var maxInlineTextChildLength = 80;
112 if (textChild.nodeValue().length < maxInlineTextChildLength)
113 return true;
114 return false;
115 };
116
117 /**
118 * @param {!WebInspector.ContextSubMenuItem} subMenu
119 * @param {!WebInspector.DOMNode} node
120 */
121 WebInspector.ElementsTreeElement.populateForcedPseudoStateItems = function(subMe nu, node)
122 {
123 const pseudoClasses = ["active", "hover", "focus", "visited"];
124 var forcedPseudoState = WebInspector.CSSModel.fromNode(node).pseudoState(nod e);
125 for (var i = 0; i < pseudoClasses.length; ++i) {
126 var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0 ;
127 subMenu.appendCheckboxItem(":" + pseudoClasses[i], setPseudoStateCallbac k.bind(null, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
128 }
129
130 /**
131 * @param {string} pseudoState
132 * @param {boolean} enabled
133 */
134 function setPseudoStateCallback(pseudoState, enabled)
135 {
136 WebInspector.CSSModel.fromNode(node).forcePseudoState(node, pseudoState, enabled);
137 }
138 };
139
140 WebInspector.ElementsTreeElement.prototype = {
141 /**
142 * @return {boolean}
143 */
144 isClosingTag: function()
145 {
146 return !!this._elementCloseTag;
147 },
148
149 /**
150 * @return {!WebInspector.DOMNode}
151 */
152 node: function()
153 {
154 return this._node;
155 },
156
157 /**
158 * @return {boolean}
159 */
160 isEditing: function()
161 {
162 return !!this._editing;
163 },
164
165 /**
166 * @param {string} searchQuery
167 */
168 highlightSearchResults: function(searchQuery)
169 {
170 if (this._searchQuery !== searchQuery)
171 this._hideSearchHighlight();
172
173 this._searchQuery = searchQuery;
174 this._searchHighlightsVisible = true;
175 this.updateTitle(null, true);
176 },
177
178 hideSearchHighlights: function()
179 {
180 delete this._searchHighlightsVisible;
181 this._hideSearchHighlight();
182 },
183
184 _hideSearchHighlight: function()
185 {
186 if (!this._highlightResult)
187 return;
188
189 function updateEntryHide(entry)
190 {
191 switch (entry.type) {
192 case "added":
193 entry.node.remove();
194 break;
195 case "changed":
196 entry.node.textContent = entry.oldText;
197 break;
198 }
199 }
200
201 for (var i = (this._highlightResult.length - 1); i >= 0; --i)
202 updateEntryHide(this._highlightResult[i]);
203
204 delete this._highlightResult;
205 },
206
207 /**
208 * @param {boolean} inClipboard
209 */
210 setInClipboard: function(inClipboard)
211 {
212 if (this._inClipboard === inClipboard)
213 return;
214 this._inClipboard = inClipboard;
215 this.listItemElement.classList.toggle("in-clipboard", inClipboard);
216 },
217
218 get hovered()
219 {
220 return this._hovered;
221 },
222
223 set hovered(x)
224 {
225 if (this._hovered === x)
226 return;
227
228 this._hovered = x;
229
230 if (this.listItemElement) {
231 if (x) {
232 this._createSelection();
233 this.listItemElement.classList.add("hovered");
234 } else {
235 this.listItemElement.classList.remove("hovered");
236 }
237 }
238 },
239
240 /**
241 * @return {number}
242 */
243 expandedChildrenLimit: function()
244 {
245 return this._expandedChildrenLimit;
246 },
247
248 /**
249 * @param {number} expandedChildrenLimit
250 */
251 setExpandedChildrenLimit: function(expandedChildrenLimit)
252 {
253 this._expandedChildrenLimit = expandedChildrenLimit;
254 },
255
256 _createSelection: function()
257 {
258 var listItemElement = this.listItemElement;
259 if (!listItemElement)
260 return;
261
262 if (!this.selectionElement) {
263 this.selectionElement = createElement("div");
264 this.selectionElement.className = "selection fill";
265 this.selectionElement.style.setProperty("margin-left", (-this._compu teLeftIndent()) + "px");
266 listItemElement.insertBefore(this.selectionElement, listItemElement. firstChild);
267 }
268 },
269
270 /**
271 * @override
272 */
273 onbind: function()
274 {
275 if (!this._elementCloseTag)
276 this._node[this.treeOutline.treeElementSymbol()] = this;
277 },
278
279 /**
280 * @override
281 */
282 onunbind: function()
283 {
284 if (this._node[this.treeOutline.treeElementSymbol()] === this)
285 this._node[this.treeOutline.treeElementSymbol()] = null;
286 },
287
288 /**
289 * @override
290 */
291 onattach: function()
292 {
293 if (this._hovered) {
294 this._createSelection();
295 this.listItemElement.classList.add("hovered");
296 }
297
298 this.updateTitle();
299 this._preventFollowingLinksOnDoubleClick();
300 this.listItemElement.draggable = true;
301 },
302
303 _preventFollowingLinksOnDoubleClick: function()
304 {
305 var links = this.listItemElement.querySelectorAll("li .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li .webkit-html-tag > .web kit-html-attribute > .webkit-html-resource-link");
306 if (!links)
307 return;
308
309 for (var i = 0; i < links.length; ++i)
310 links[i].preventFollowOnDoubleClick = true;
311 },
312
313 onpopulate: function()
314 {
315 this.populated = true;
316 this.treeOutline.populateTreeElement(this);
317 },
318
319 expandRecursively: function()
320 {
321 /**
322 * @this {WebInspector.ElementsTreeElement}
323 */
324 function callback()
325 {
326 TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE) ;
327 }
328
329 this._node.getSubtree(-1, callback.bind(this));
330 },
331
332 /**
333 * @override
334 */
335 onexpand: function()
336 {
337 if (this._elementCloseTag)
338 return;
339
340 this.updateTitle();
341 },
342
343 oncollapse: function()
344 {
345 if (this._elementCloseTag)
346 return;
347
348 this.updateTitle();
349 },
350
351 /**
352 * @override
353 * @param {boolean=} omitFocus
354 * @param {boolean=} selectedByUser
355 * @return {boolean}
356 */
357 select: function(omitFocus, selectedByUser)
358 {
359 if (this._editing)
360 return false;
361 return TreeElement.prototype.select.call(this, omitFocus, selectedByUser );
362 },
363
364 /**
365 * @override
366 * @param {boolean=} selectedByUser
367 * @return {boolean}
368 */
369 onselect: function(selectedByUser)
370 {
371 this.treeOutline.suppressRevealAndSelect = true;
372 this.treeOutline.selectDOMNode(this._node, selectedByUser);
373 if (selectedByUser)
374 this._node.highlight();
375 this._createSelection();
376 this.treeOutline.suppressRevealAndSelect = false;
377 return true;
378 },
379
380 /**
381 * @override
382 * @return {boolean}
383 */
384 ondelete: function()
385 {
386 var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
387 startTagTreeElement ? startTagTreeElement.remove() : this.remove();
388 return true;
389 },
390
391 /**
392 * @override
393 * @return {boolean}
394 */
395 onenter: function()
396 {
397 // On Enter or Return start editing the first attribute
398 // or create a new attribute on the selected element.
399 if (this._editing)
400 return false;
401
402 this._startEditing();
403
404 // prevent a newline from being immediately inserted
405 return true;
406 },
407
408 selectOnMouseDown: function(event)
409 {
410 TreeElement.prototype.selectOnMouseDown.call(this, event);
411
412 if (this._editing)
413 return;
414
415 // Prevent selecting the nearest word on double click.
416 if (event.detail >= 2)
417 event.preventDefault();
418 },
419
420 /**
421 * @override
422 * @return {boolean}
423 */
424 ondblclick: function(event)
425 {
426 if (this._editing || this._elementCloseTag)
427 return false;
428
429 if (this._startEditingTarget(/** @type {!Element} */(event.target)))
430 return false;
431
432 if (this.isExpandable() && !this.expanded)
433 this.expand();
434 return false;
435 },
436
437 /**
438 * @return {boolean}
439 */
440 hasEditableNode: function()
441 {
442 return !this._node.isShadowRoot() && !this._node.ancestorUserAgentShadow Root();
443 },
444
445 _insertInLastAttributePosition: function(tag, node)
446 {
447 if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
448 tag.insertBefore(node, tag.lastChild);
449 else {
450 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
451 tag.textContent = "";
452 tag.createTextChild("<" + nodeName);
453 tag.appendChild(node);
454 tag.createTextChild(">");
455 }
456 },
457
458 /**
459 * @param {!Element} eventTarget
460 * @return {boolean}
461 */
462 _startEditingTarget: function(eventTarget)
463 {
464 if (this.treeOutline.selectedDOMNode() !== this._node)
465 return false;
466
467 if (this._node.nodeType() !== Node.ELEMENT_NODE && this._node.nodeType() !== Node.TEXT_NODE)
468 return false;
469
470 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tex t-node");
471 if (textNode)
472 return this._startEditingTextNode(textNode);
473
474 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-at tribute");
475 if (attribute)
476 return this._startEditingAttribute(attribute, eventTarget);
477
478 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag- name");
479 if (tagName)
480 return this._startEditingTagName(tagName);
481
482 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribu te");
483 if (newAttribute)
484 return this._addNewAttribute();
485
486 return false;
487 },
488
489 /**
490 * @param {!Event} event
491 */
492 _showContextMenu: function(event)
493 {
494 this.treeOutline.showContextMenu(this, event);
495 },
496
497 /**
498 * @param {!WebInspector.ContextMenu} contextMenu
499 * @param {!Event} event
500 */
501 populateTagContextMenu: function(contextMenu, event)
502 {
503 // Add attribute-related actions.
504 var treeElement = this._elementCloseTag ? this.treeOutline.findTreeEleme nt(this._node) : this;
505 contextMenu.appendItem(WebInspector.UIString.capitalize("Add ^attribute" ), treeElement._addNewAttribute.bind(treeElement));
506
507 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-a ttribute");
508 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attrib ute");
509 if (attribute && !newAttribute)
510 contextMenu.appendItem(WebInspector.UIString.capitalize("Edit ^attri bute"), this._startEditingAttribute.bind(this, attribute, event.target));
511 this.populateNodeContextMenu(contextMenu);
512 WebInspector.ElementsTreeElement.populateForcedPseudoStateItems(contextM enu, treeElement.node());
513 contextMenu.appendSeparator();
514 this.populateScrollIntoView(contextMenu);
515 },
516
517 /**
518 * @param {!WebInspector.ContextMenu} contextMenu
519 */
520 populateScrollIntoView: function(contextMenu)
521 {
522 contextMenu.appendItem(WebInspector.UIString.capitalize("Scroll into ^vi ew"), this._scrollIntoView.bind(this));
523 },
524
525 populateTextContextMenu: function(contextMenu, textNode)
526 {
527 if (!this._editing)
528 contextMenu.appendItem(WebInspector.UIString.capitalize("Edit ^text" ), this._startEditingTextNode.bind(this, textNode));
529 this.populateNodeContextMenu(contextMenu);
530 },
531
532 populateNodeContextMenu: function(contextMenu)
533 {
534 // Add free-form node-related actions.
535 var openTagElement = this._node[this.treeOutline.treeElementSymbol()] || this;
536 var isEditable = this.hasEditableNode();
537 if (isEditable && !this._editing)
538 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._ editAsHTML.bind(this));
539 var isShadowRoot = this._node.isShadowRoot();
540
541 // Place it here so that all "Copy"-ing items stick together.
542 var copyMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Copy "));
543 var createShortcut = WebInspector.KeyboardShortcut.shortcutToString;
544 var modifier = WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta;
545 var treeOutline = this.treeOutline;
546 var menuItem;
547 if (!isShadowRoot) {
548 menuItem = copyMenu.appendItem(WebInspector.UIString("Copy outerHTML "), treeOutline.performCopyOrCut.bind(treeOutline, false, this._node));
549 menuItem.setShortcut(createShortcut("V", modifier));
550 }
551 if (this._node.nodeType() === Node.ELEMENT_NODE)
552 copyMenu.appendItem(WebInspector.UIString.capitalize("Copy selector" ), this._copyCSSPath.bind(this));
553 if (!isShadowRoot)
554 copyMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyX Path.bind(this));
555 if (!isShadowRoot) {
556 menuItem = copyMenu.appendItem(WebInspector.UIString("Cut element"), treeOutline.performCopyOrCut.bind(treeOutline, true, this._node), !this.hasEdit ableNode());
557 menuItem.setShortcut(createShortcut("X", modifier));
558 menuItem = copyMenu.appendItem(WebInspector.UIString("Copy element") , treeOutline.performCopyOrCut.bind(treeOutline, false, this._node));
559 menuItem.setShortcut(createShortcut("C", modifier));
560 menuItem = copyMenu.appendItem(WebInspector.UIString("Paste element" ), treeOutline.pasteNode.bind(treeOutline, this._node), !treeOutline.canPaste(th is._node));
561 menuItem.setShortcut(createShortcut("V", modifier));
562 }
563
564 contextMenu.appendSeparator();
565 menuItem = contextMenu.appendCheckboxItem(WebInspector.UIString("Hide el ement"), treeOutline.toggleHideElement.bind(treeOutline, this._node), treeOutlin e.isToggledToHidden(this._node));
566 menuItem.setShortcut(WebInspector.shortcutRegistry.shortcutTitleForActio n("elements.hide-element"));
567
568
569 if (isEditable)
570 contextMenu.appendItem(WebInspector.UIString("Delete element"), this .remove.bind(this));
571 contextMenu.appendSeparator();
572
573 contextMenu.appendItem(WebInspector.UIString("Expand all"), this.expandR ecursively.bind(this));
574 contextMenu.appendItem(WebInspector.UIString("Collapse all"), this.colla pseRecursively.bind(this));
575 contextMenu.appendSeparator();
576 },
577
578 _startEditing: function()
579 {
580 if (this.treeOutline.selectedDOMNode() !== this._node)
581 return;
582
583 var listItem = this._listItemNode;
584
585 if (this._canAddAttributes) {
586 var attribute = listItem.getElementsByClassName("webkit-html-attribu te")[0];
587 if (attribute)
588 return this._startEditingAttribute(attribute, attribute.getEleme ntsByClassName("webkit-html-attribute-value")[0]);
589
590 return this._addNewAttribute();
591 }
592
593 if (this._node.nodeType() === Node.TEXT_NODE) {
594 var textNode = listItem.getElementsByClassName("webkit-html-text-nod e")[0];
595 if (textNode)
596 return this._startEditingTextNode(textNode);
597 return;
598 }
599 },
600
601 _addNewAttribute: function()
602 {
603 // Cannot just convert the textual html into an element without
604 // a parent node. Use a temporary span container for the HTML.
605 var container = createElement("span");
606 this._buildAttributeDOM(container, " ", "", null);
607 var attr = container.firstElementChild;
608 attr.style.marginLeft = "2px"; // overrides the .editing margin rule
609 attr.style.marginRight = "2px"; // overrides the .editing margin rule
610
611 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag") [0];
612 this._insertInLastAttributePosition(tag, attr);
613 attr.scrollIntoViewIfNeeded(true);
614 return this._startEditingAttribute(attr, attr);
615 },
616
617 _triggerEditAttribute: function(attributeName)
618 {
619 var attributeElements = this.listItemElement.getElementsByClassName("web kit-html-attribute-name");
620 for (var i = 0, len = attributeElements.length; i < len; ++i) {
621 if (attributeElements[i].textContent === attributeName) {
622 for (var elem = attributeElements[i].nextSibling; elem; elem = e lem.nextSibling) {
623 if (elem.nodeType !== Node.ELEMENT_NODE)
624 continue;
625
626 if (elem.classList.contains("webkit-html-attribute-value"))
627 return this._startEditingAttribute(elem.parentNode, elem );
628 }
629 }
630 }
631 },
632
633 _startEditingAttribute: function(attribute, elementForSelection)
634 {
635 console.assert(this.listItemElement.isAncestor(attribute));
636
637 if (WebInspector.isBeingEdited(attribute))
638 return true;
639
640 var attributeNameElement = attribute.getElementsByClassName("webkit-html -attribute-name")[0];
641 if (!attributeNameElement)
642 return false;
643
644 var attributeName = attributeNameElement.textContent;
645 var attributeValueElement = attribute.getElementsByClassName("webkit-htm l-attribute-value")[0];
646
647 // Make sure elementForSelection is not a child of attributeValueElement .
648 elementForSelection = attributeValueElement.isAncestor(elementForSelecti on) ? attributeValueElement : elementForSelection;
649
650 function removeZeroWidthSpaceRecursive(node)
651 {
652 if (node.nodeType === Node.TEXT_NODE) {
653 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
654 return;
655 }
656
657 if (node.nodeType !== Node.ELEMENT_NODE)
658 return;
659
660 for (var child = node.firstChild; child; child = child.nextSibling)
661 removeZeroWidthSpaceRecursive(child);
662 }
663
664 var attributeValue = attributeName && attributeValueElement ? this._node .getAttribute(attributeName) : undefined;
665 if (attributeValue !== undefined)
666 attributeValueElement.setTextContentTruncatedIfNeeded(attributeValue , WebInspector.UIString("<value is too large to edit>"));
667
668 // Remove zero-width spaces that were added by nodeTitleInfo.
669 removeZeroWidthSpaceRecursive(attribute);
670
671 var config = new WebInspector.InplaceEditor.Config(this._attributeEditin gCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
672
673 /**
674 * @param {!Event} event
675 * @return {string}
676 */
677 function postKeyDownFinishHandler(event)
678 {
679 WebInspector.handleElementValueModifications(event, attribute);
680 return "";
681 }
682
683 if (!attributeValueElement.textContent.asParsedURL())
684 config.setPostKeydownFinishHandler(postKeyDownFinishHandler);
685
686 this._editing = WebInspector.InplaceEditor.startEditing(attribute, confi g);
687
688 this.listItemElement.getComponentSelection().setBaseAndExtent(elementFor Selection, 0, elementForSelection, 1);
689
690 return true;
691 },
692
693 /**
694 * @param {!Element} textNodeElement
695 */
696 _startEditingTextNode: function(textNodeElement)
697 {
698 if (WebInspector.isBeingEdited(textNodeElement))
699 return true;
700
701 var textNode = this._node;
702 // We only show text nodes inline in elements if the element only
703 // has a single child, and that child is a text node.
704 if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
705 textNode = textNode.firstChild;
706
707 var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-htm l-text-node");
708 if (container)
709 container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
710 var config = new WebInspector.InplaceEditor.Config(this._textNodeEditing Committed.bind(this, textNode), this._editingCancelled.bind(this));
711 this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, config);
712 this.listItemElement.getComponentSelection().setBaseAndExtent(textNodeEl ement, 0, textNodeElement, 1);
713
714 return true;
715 },
716
717 /**
718 * @param {!Element=} tagNameElement
719 */
720 _startEditingTagName: function(tagNameElement)
721 {
722 if (!tagNameElement) {
723 tagNameElement = this.listItemElement.getElementsByClassName("webkit -html-tag-name")[0];
724 if (!tagNameElement)
725 return false;
726 }
727
728 var tagName = tagNameElement.textContent;
729 if (WebInspector.ElementsTreeElement.EditTagBlacklist.has(tagName.toLowe rCase()))
730 return false;
731
732 if (WebInspector.isBeingEdited(tagNameElement))
733 return true;
734
735 var closingTagElement = this._distinctClosingTagElement();
736
737 /**
738 * @param {!Event} event
739 */
740 function keyupListener(event)
741 {
742 if (closingTagElement)
743 closingTagElement.textContent = "</" + tagNameElement.textConten t + ">";
744 }
745
746 /**
747 * @param {!Element} element
748 * @param {string} newTagName
749 * @this {WebInspector.ElementsTreeElement}
750 */
751 function editingComitted(element, newTagName)
752 {
753 tagNameElement.removeEventListener("keyup", keyupListener, false);
754 this._tagNameEditingCommitted.apply(this, arguments);
755 }
756
757 /**
758 * @this {WebInspector.ElementsTreeElement}
759 */
760 function editingCancelled()
761 {
762 tagNameElement.removeEventListener("keyup", keyupListener, false);
763 this._editingCancelled.apply(this, arguments);
764 }
765
766 tagNameElement.addEventListener("keyup", keyupListener, false);
767
768 var config = new WebInspector.InplaceEditor.Config(editingComitted.bind( this), editingCancelled.bind(this), tagName);
769 this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, config);
770 this.listItemElement.getComponentSelection().setBaseAndExtent(tagNameEle ment, 0, tagNameElement, 1);
771 return true;
772 },
773
774 /**
775 * @param {function(string, string)} commitCallback
776 * @param {function()} disposeCallback
777 * @param {?Protocol.Error} error
778 * @param {string} initialValue
779 */
780 _startEditingAsHTML: function(commitCallback, disposeCallback, error, initia lValue)
781 {
782 if (error)
783 return;
784 if (this._editing)
785 return;
786
787 function consume(event)
788 {
789 if (event.eventPhase === Event.AT_TARGET)
790 event.consume(true);
791 }
792
793 initialValue = this._convertWhitespaceToEntities(initialValue).text;
794
795 this._htmlEditElement = createElement("div");
796 this._htmlEditElement.className = "source-code elements-tree-editor";
797
798 // Hide header items.
799 var child = this.listItemElement.firstChild;
800 while (child) {
801 child.style.display = "none";
802 child = child.nextSibling;
803 }
804 // Hide children item.
805 if (this._childrenListNode)
806 this._childrenListNode.style.display = "none";
807 // Append editor.
808 this.listItemElement.appendChild(this._htmlEditElement);
809 this.listItemElement.classList.add("editing-as-html");
810 this.treeOutline.element.addEventListener("mousedown", consume, false);
811
812 /**
813 * @param {!Element} element
814 * @param {string} newValue
815 * @this {WebInspector.ElementsTreeElement}
816 */
817 function commit(element, newValue)
818 {
819 commitCallback(initialValue, newValue);
820 dispose.call(this);
821 }
822
823 /**
824 * @this {WebInspector.ElementsTreeElement}
825 */
826 function dispose()
827 {
828 disposeCallback();
829 delete this._editing;
830 this.treeOutline.setMultilineEditing(null);
831
832 this.listItemElement.classList.remove("editing-as-html");
833 // Remove editor.
834 this.listItemElement.removeChild(this._htmlEditElement);
835 delete this._htmlEditElement;
836 // Unhide children item.
837 if (this._childrenListNode)
838 this._childrenListNode.style.removeProperty("display");
839 // Unhide header items.
840 var child = this.listItemElement.firstChild;
841 while (child) {
842 child.style.removeProperty("display");
843 child = child.nextSibling;
844 }
845
846 this.treeOutline.element.removeEventListener("mousedown", consume, f alse);
847 this.treeOutline.focus();
848 }
849
850 var config = new WebInspector.InplaceEditor.Config(commit.bind(this), di spose.bind(this));
851 config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true } , "web-inspector-html", WebInspector.moduleSetting("domWordWrap").get(), true);
852 WebInspector.InplaceEditor.startMultilineEditing(this._htmlEditElement, config).then(markAsBeingEdited.bind(this));
853
854 /**
855 * @param {!Object} controller
856 * @this {WebInspector.ElementsTreeElement}
857 */
858 function markAsBeingEdited(controller)
859 {
860 this._editing = /** @type {!WebInspector.InplaceEditor.Controller} * / (controller);
861 this._editing.setWidth(this.treeOutline.visibleWidth() - this._compu teLeftIndent());
862 this.treeOutline.setMultilineEditing(this._editing);
863 }
864 },
865
866 _attributeEditingCommitted: function(element, newText, oldText, attributeNam e, moveDirection)
867 {
868 delete this._editing;
869
870 var treeOutline = this.treeOutline;
871
872 /**
873 * @param {?Protocol.Error=} error
874 * @this {WebInspector.ElementsTreeElement}
875 */
876 function moveToNextAttributeIfNeeded(error)
877 {
878 if (error)
879 this._editingCancelled(element, attributeName);
880
881 if (!moveDirection)
882 return;
883
884 treeOutline.runPendingUpdates();
885
886 // Search for the attribute's position, and then decide where to mov e to.
887 var attributes = this._node.attributes();
888 for (var i = 0; i < attributes.length; ++i) {
889 if (attributes[i].name !== attributeName)
890 continue;
891
892 if (moveDirection === "backward") {
893 if (i === 0)
894 this._startEditingTagName();
895 else
896 this._triggerEditAttribute(attributes[i - 1].name);
897 } else {
898 if (i === attributes.length - 1)
899 this._addNewAttribute();
900 else
901 this._triggerEditAttribute(attributes[i + 1].name);
902 }
903 return;
904 }
905
906 // Moving From the "New Attribute" position.
907 if (moveDirection === "backward") {
908 if (newText === " ") {
909 // Moving from "New Attribute" that was not edited
910 if (attributes.length > 0)
911 this._triggerEditAttribute(attributes[attributes.length - 1].name);
912 } else {
913 // Moving from "New Attribute" that holds new value
914 if (attributes.length > 1)
915 this._triggerEditAttribute(attributes[attributes.length - 2].name);
916 }
917 } else if (moveDirection === "forward") {
918 if (!newText.isWhitespace())
919 this._addNewAttribute();
920 else
921 this._startEditingTagName();
922 }
923 }
924
925
926 if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
927 this._node.setAttribute(attributeName, newText, moveToNextAttributeI fNeeded.bind(this));
928 return;
929 }
930
931 this.updateTitle();
932 moveToNextAttributeIfNeeded.call(this);
933 },
934
935 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveD irection)
936 {
937 delete this._editing;
938 var self = this;
939
940 function cancel()
941 {
942 var closingTagElement = self._distinctClosingTagElement();
943 if (closingTagElement)
944 closingTagElement.textContent = "</" + tagName + ">";
945
946 self._editingCancelled(element, tagName);
947 moveToNextAttributeIfNeeded.call(self);
948 }
949
950 /**
951 * @this {WebInspector.ElementsTreeElement}
952 */
953 function moveToNextAttributeIfNeeded()
954 {
955 if (moveDirection !== "forward") {
956 this._addNewAttribute();
957 return;
958 }
959
960 var attributes = this._node.attributes();
961 if (attributes.length > 0)
962 this._triggerEditAttribute(attributes[0].name);
963 else
964 this._addNewAttribute();
965 }
966
967 newText = newText.trim();
968 if (newText === oldText) {
969 cancel();
970 return;
971 }
972
973 var treeOutline = this.treeOutline;
974 var wasExpanded = this.expanded;
975
976 function changeTagNameCallback(error, nodeId)
977 {
978 if (error || !nodeId) {
979 cancel();
980 return;
981 }
982 var newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error , nodeId);
983 moveToNextAttributeIfNeeded.call(newTreeItem);
984 }
985 this._node.setNodeName(newText, changeTagNameCallback);
986 },
987
988 /**
989 * @param {!WebInspector.DOMNode} textNode
990 * @param {!Element} element
991 * @param {string} newText
992 */
993 _textNodeEditingCommitted: function(textNode, element, newText)
994 {
995 delete this._editing;
996
997 /**
998 * @this {WebInspector.ElementsTreeElement}
999 */
1000 function callback()
1001 {
1002 this.updateTitle();
1003 }
1004 textNode.setNodeValue(newText, callback.bind(this));
1005 },
1006
1007 /**
1008 * @param {!Element} element
1009 * @param {*} context
1010 */
1011 _editingCancelled: function(element, context)
1012 {
1013 delete this._editing;
1014
1015 // Need to restore attributes structure.
1016 this.updateTitle();
1017 },
1018
1019 /**
1020 * @return {!Element}
1021 */
1022 _distinctClosingTagElement: function()
1023 {
1024 // FIXME: Improve the Tree Element / Outline Abstraction to prevent craw ling the DOM
1025
1026 // For an expanded element, it will be the last element with class "clos e"
1027 // in the child element list.
1028 if (this.expanded) {
1029 var closers = this._childrenListNode.querySelectorAll(".close");
1030 return closers[closers.length - 1];
1031 }
1032
1033 // Remaining cases are single line non-expanded elements with a closing
1034 // tag, or HTML elements without a closing tag (such as <br>). Return
1035 // null in the case where there isn't a closing tag.
1036 var tags = this.listItemElement.getElementsByClassName("webkit-html-tag" );
1037 return (tags.length === 1 ? null : tags[tags.length - 1]);
1038 },
1039
1040 /**
1041 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord=} updateRecord
1042 * @param {boolean=} onlySearchQueryChanged
1043 */
1044 updateTitle: function(updateRecord, onlySearchQueryChanged)
1045 {
1046 // If we are editing, return early to prevent canceling the edit.
1047 // After editing is committed updateTitle will be called.
1048 if (this._editing)
1049 return;
1050
1051 if (onlySearchQueryChanged) {
1052 this._hideSearchHighlight();
1053 } else {
1054 var nodeInfo = this._nodeTitleInfo(updateRecord || null);
1055 if (this._node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE && this._n ode.isInShadowTree() && this._node.shadowRootType()) {
1056 this.childrenListElement.classList.add("shadow-root");
1057 var depth = 4;
1058 for (var node = this._node; depth && node; node = node.parentNod e) {
1059 if (node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE)
1060 depth--;
1061 }
1062 if (!depth)
1063 this.childrenListElement.classList.add("shadow-root-deep");
1064 else
1065 this.childrenListElement.classList.add("shadow-root-depth-" + depth);
1066 }
1067 var highlightElement = createElement("span");
1068 highlightElement.className = "highlight";
1069 highlightElement.appendChild(nodeInfo);
1070 this.title = highlightElement;
1071 this.updateDecorations();
1072 this.listItemElement.insertBefore(this._gutterContainer, this.listIt emElement.firstChild);
1073 delete this._highlightResult;
1074 }
1075
1076 delete this.selectionElement;
1077 if (this.selected)
1078 this._createSelection();
1079 this._preventFollowingLinksOnDoubleClick();
1080 this._highlightSearchResults();
1081 },
1082
1083 /**
1084 * @return {number}
1085 */
1086 _computeLeftIndent: function()
1087 {
1088 var treeElement = this.parent;
1089 var depth = 0;
1090 while (treeElement !== null) {
1091 depth++;
1092 treeElement = treeElement.parent;
1093 }
1094
1095 /** Keep it in sync with elementsTreeOutline.css **/
1096 return 12 * (depth - 2) + (this.isExpandable() ? 1 : 12);
1097 },
1098
1099 updateDecorations: function()
1100 {
1101 this._gutterContainer.style.left = (-this._computeLeftIndent()) + "px";
1102
1103 if (this.isClosingTag())
1104 return;
1105
1106 var node = this._node;
1107 if (node.nodeType() !== Node.ELEMENT_NODE)
1108 return;
1109
1110 if (!this.treeOutline._decoratorExtensions)
1111 /** @type {!Array.<!Runtime.Extension>} */
1112 this.treeOutline._decoratorExtensions = runtime.extensions(WebInspec tor.DOMPresentationUtils.MarkerDecorator);
1113
1114 var markerToExtension = new Map();
1115 for (var i = 0; i < this.treeOutline._decoratorExtensions.length; ++i)
1116 markerToExtension.set(this.treeOutline._decoratorExtensions[i].descr iptor()["marker"], this.treeOutline._decoratorExtensions[i]);
1117
1118 var promises = [];
1119 var decorations = [];
1120 var descendantDecorations = [];
1121 node.traverseMarkers(visitor);
1122
1123 /**
1124 * @param {!WebInspector.DOMNode} n
1125 * @param {string} marker
1126 */
1127 function visitor(n, marker)
1128 {
1129 var extension = markerToExtension.get(marker);
1130 if (!extension)
1131 return;
1132 promises.push(extension.instance().then(collectDecoration.bind(null, n)));
1133 }
1134
1135 /**
1136 * @param {!WebInspector.DOMNode} n
1137 * @param {!WebInspector.DOMPresentationUtils.MarkerDecorator} decorator
1138 */
1139 function collectDecoration(n, decorator)
1140 {
1141 var decoration = decorator.decorate(n);
1142 if (!decoration)
1143 return;
1144 (n === node ? decorations : descendantDecorations).push(decoration);
1145 }
1146
1147 Promise.all(promises).then(updateDecorationsUI.bind(this));
1148
1149 /**
1150 * @this {WebInspector.ElementsTreeElement}
1151 */
1152 function updateDecorationsUI()
1153 {
1154 this._decorationsElement.removeChildren();
1155 this._decorationsElement.classList.add("hidden");
1156 this._gutterContainer.classList.toggle("has-decorations", decoration s.length || descendantDecorations.length);
1157
1158 if (!decorations.length && !descendantDecorations.length)
1159 return;
1160
1161 var colors = new Set();
1162 var titles = createElement("div");
1163
1164 for (var decoration of decorations) {
1165 var titleElement = titles.createChild("div");
1166 titleElement.textContent = decoration.title;
1167 colors.add(decoration.color);
1168 }
1169 if (this.expanded && !decorations.length)
1170 return;
1171
1172 var descendantColors = new Set();
1173 if (descendantDecorations.length) {
1174 var element = titles.createChild("div");
1175 element.textContent = WebInspector.UIString("Children:");
1176 for (var decoration of descendantDecorations) {
1177 element = titles.createChild("div");
1178 element.style.marginLeft = "15px";
1179 element.textContent = decoration.title;
1180 descendantColors.add(decoration.color);
1181 }
1182 }
1183
1184 var offset = 0;
1185 processColors.call(this, colors, "elements-gutter-decoration");
1186 if (!this.expanded)
1187 processColors.call(this, descendantColors, "elements-gutter-deco ration elements-has-decorated-children");
1188 WebInspector.Tooltip.install(this._decorationsElement, titles);
1189
1190 /**
1191 * @param {!Set<string>} colors
1192 * @param {string} className
1193 * @this {WebInspector.ElementsTreeElement}
1194 */
1195 function processColors(colors, className)
1196 {
1197 for (var color of colors) {
1198 var child = this._decorationsElement.createChild("div", clas sName);
1199 this._decorationsElement.classList.remove("hidden");
1200 child.style.backgroundColor = color;
1201 child.style.borderColor = color;
1202 if (offset)
1203 child.style.marginLeft = offset + "px";
1204 offset += 3;
1205 }
1206 }
1207 }
1208 },
1209
1210 /**
1211 * @param {!Node} parentElement
1212 * @param {string} name
1213 * @param {string} value
1214 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1215 * @param {boolean=} forceValue
1216 * @param {!WebInspector.DOMNode=} node
1217 */
1218 _buildAttributeDOM: function(parentElement, name, value, updateRecord, force Value, node)
1219 {
1220 var closingPunctuationRegex = /[\/;:\)\]\}]/g;
1221 var highlightIndex = 0;
1222 var highlightCount;
1223 var additionalHighlightOffset = 0;
1224 var result;
1225
1226 /**
1227 * @param {string} match
1228 * @param {number} replaceOffset
1229 * @return {string}
1230 */
1231 function replacer(match, replaceOffset) {
1232 while (highlightIndex < highlightCount && result.entityRanges[highli ghtIndex].offset < replaceOffset) {
1233 result.entityRanges[highlightIndex].offset += additionalHighligh tOffset;
1234 ++highlightIndex;
1235 }
1236 additionalHighlightOffset += 1;
1237 return match + "\u200B";
1238 }
1239
1240 /**
1241 * @param {!Element} element
1242 * @param {string} value
1243 * @this {WebInspector.ElementsTreeElement}
1244 */
1245 function setValueWithEntities(element, value)
1246 {
1247 result = this._convertWhitespaceToEntities(value);
1248 highlightCount = result.entityRanges.length;
1249 value = result.text.replace(closingPunctuationRegex, replacer);
1250 while (highlightIndex < highlightCount) {
1251 result.entityRanges[highlightIndex].offset += additionalHighligh tOffset;
1252 ++highlightIndex;
1253 }
1254 element.setTextContentTruncatedIfNeeded(value);
1255 WebInspector.highlightRangesWithStyleClass(element, result.entityRan ges, "webkit-html-entity-value");
1256 }
1257
1258 var hasText = (forceValue || value.length > 0);
1259 var attrSpanElement = parentElement.createChild("span", "webkit-html-att ribute");
1260 var attrNameElement = attrSpanElement.createChild("span", "webkit-html-a ttribute-name");
1261 attrNameElement.textContent = name;
1262
1263 if (hasText)
1264 attrSpanElement.createTextChild("=\u200B\"");
1265
1266 var attrValueElement = attrSpanElement.createChild("span", "webkit-html- attribute-value");
1267
1268 if (updateRecord && updateRecord.isAttributeModified(name))
1269 WebInspector.runCSSAnimationOnce(hasText ? attrValueElement : attrNa meElement, "dom-update-highlight");
1270
1271 /**
1272 * @this {WebInspector.ElementsTreeElement}
1273 * @param {string} value
1274 * @return {!Element}
1275 */
1276 function linkifyValue(value)
1277 {
1278 var rewrittenHref = node.resolveURL(value);
1279 if (rewrittenHref === null) {
1280 var span = createElement("span");
1281 setValueWithEntities.call(this, span, value);
1282 return span;
1283 }
1284 value = value.replace(closingPunctuationRegex, "$&\u200B");
1285 if (value.startsWith("data:"))
1286 value = value.trimMiddle(60);
1287 var anchor = WebInspector.linkifyURLAsNode(rewrittenHref, value, "", node.nodeName().toLowerCase() === "a");
1288 anchor.preventFollow = true;
1289 return anchor;
1290 }
1291
1292 if (node && (name === "src" || name === "href")) {
1293 attrValueElement.appendChild(linkifyValue.call(this, value));
1294 } else if (node && (node.nodeName().toLowerCase() === "img" || node.node Name().toLowerCase() === "source") && name === "srcset") {
1295 var sources = value.split(",");
1296 for (var i = 0; i < sources.length; ++i) {
1297 if (i > 0)
1298 attrValueElement.createTextChild(", ");
1299 var source = sources[i].trim();
1300 var indexOfSpace = source.indexOf(" ");
1301 var url, tail;
1302
1303 if (indexOfSpace === -1) {
1304 url = source;
1305 } else {
1306 url = source.substring(0, indexOfSpace);
1307 tail = source.substring(indexOfSpace);
1308 }
1309
1310 attrValueElement.appendChild(linkifyValue.call(this, url));
1311
1312 if (tail)
1313 attrValueElement.createTextChild(tail);
1314 }
1315 } else {
1316 setValueWithEntities.call(this, attrValueElement, value);
1317 }
1318
1319 if (hasText)
1320 attrSpanElement.createTextChild("\"");
1321 },
1322
1323 /**
1324 * @param {!Node} parentElement
1325 * @param {string} pseudoElementName
1326 */
1327 _buildPseudoElementDOM: function(parentElement, pseudoElementName)
1328 {
1329 var pseudoElement = parentElement.createChild("span", "webkit-html-pseud o-element");
1330 pseudoElement.textContent = "::" + pseudoElementName;
1331 parentElement.createTextChild("\u200B");
1332 },
1333
1334 /**
1335 * @param {!Node} parentElement
1336 * @param {string} tagName
1337 * @param {boolean} isClosingTag
1338 * @param {boolean} isDistinctTreeElement
1339 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1340 */
1341 _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeE lement, updateRecord)
1342 {
1343 var node = this._node;
1344 var classes = [ "webkit-html-tag" ];
1345 if (isClosingTag && isDistinctTreeElement)
1346 classes.push("close");
1347 var tagElement = parentElement.createChild("span", classes.join(" "));
1348 tagElement.createTextChild("<");
1349 var tagNameElement = tagElement.createChild("span", isClosingTag ? "webk it-html-close-tag-name" : "webkit-html-tag-name");
1350 tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1351 if (!isClosingTag) {
1352 if (node.hasAttributes()) {
1353 var attributes = node.attributes();
1354 for (var i = 0; i < attributes.length; ++i) {
1355 var attr = attributes[i];
1356 tagElement.createTextChild(" ");
1357 this._buildAttributeDOM(tagElement, attr.name, attr.value, u pdateRecord, false, node);
1358 }
1359 }
1360 if (updateRecord) {
1361 var hasUpdates = updateRecord.hasRemovedAttributes() || updateRe cord.hasRemovedChildren();
1362 hasUpdates |= !this.expanded && updateRecord.hasChangedChildren( );
1363 if (hasUpdates)
1364 WebInspector.runCSSAnimationOnce(tagNameElement, "dom-update -highlight");
1365 }
1366 }
1367
1368 tagElement.createTextChild(">");
1369 parentElement.createTextChild("\u200B");
1370 },
1371
1372 /**
1373 * @param {string} text
1374 * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange> }}
1375 */
1376 _convertWhitespaceToEntities: function(text)
1377 {
1378 var result = "";
1379 var lastIndexAfterEntity = 0;
1380 var entityRanges = [];
1381 var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
1382 for (var i = 0, size = text.length; i < size; ++i) {
1383 var char = text.charAt(i);
1384 if (charToEntity[char]) {
1385 result += text.substring(lastIndexAfterEntity, i);
1386 var entityValue = "&" + charToEntity[char] + ";";
1387 entityRanges.push({offset: result.length, length: entityValue.le ngth});
1388 result += entityValue;
1389 lastIndexAfterEntity = i + 1;
1390 }
1391 }
1392 if (result)
1393 result += text.substring(lastIndexAfterEntity);
1394 return {text: result || text, entityRanges: entityRanges};
1395 },
1396
1397 /**
1398 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1399 * @return {!DocumentFragment} result
1400 */
1401 _nodeTitleInfo: function(updateRecord)
1402 {
1403 var node = this._node;
1404 var titleDOM = createDocumentFragment();
1405
1406 switch (node.nodeType()) {
1407 case Node.ATTRIBUTE_NODE:
1408 this._buildAttributeDOM(titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), updateRecord, true);
1409 break;
1410
1411 case Node.ELEMENT_NODE:
1412 var pseudoType = node.pseudoType();
1413 if (pseudoType) {
1414 this._buildPseudoElementDOM(titleDOM, pseudoType);
1415 break;
1416 }
1417
1418 var tagName = node.nodeNameInCorrectCase();
1419 if (this._elementCloseTag) {
1420 this._buildTagDOM(titleDOM, tagName, true, true, updateRecord);
1421 break;
1422 }
1423
1424 this._buildTagDOM(titleDOM, tagName, false, false, updateRecord);
1425
1426 if (this.isExpandable()) {
1427 if (!this.expanded) {
1428 var textNodeElement = titleDOM.createChild("span", "webkit-h tml-text-node bogus");
1429 textNodeElement.textContent = "\u2026";
1430 titleDOM.createTextChild("\u200B");
1431 this._buildTagDOM(titleDOM, tagName, true, false, updateReco rd);
1432 }
1433 break;
1434 }
1435
1436 if (WebInspector.ElementsTreeElement.canShowInlineText(node)) {
1437 var textNodeElement = titleDOM.createChild("span", "webkit-html- text-node");
1438 var result = this._convertWhitespaceToEntities(node.firstChild.n odeValue());
1439 textNodeElement.textContent = result.text;
1440 WebInspector.highlightRangesWithStyleClass(textNodeElement, resu lt.entityRanges, "webkit-html-entity-value");
1441 titleDOM.createTextChild("\u200B");
1442 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1443 if (updateRecord && updateRecord.hasChangedChildren())
1444 WebInspector.runCSSAnimationOnce(textNodeElement, "dom-updat e-highlight");
1445 if (updateRecord && updateRecord.isCharDataModified())
1446 WebInspector.runCSSAnimationOnce(textNodeElement, "dom-updat e-highlight");
1447 break;
1448 }
1449
1450 if (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElem ent.ForbiddenClosingTagElements.has(tagName))
1451 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1452 break;
1453
1454 case Node.TEXT_NODE:
1455 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1456 var newNode = titleDOM.createChild("span", "webkit-html-text-nod e webkit-html-js-node");
1457 var text = node.nodeValue();
1458 newNode.textContent = text.startsWith("\n") ? text.substring(1) : text;
1459
1460 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHigh lighter("text/javascript", true);
1461 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode).then(up dateSearchHighlight.bind(this));
1462 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase () === "style") {
1463 var newNode = titleDOM.createChild("span", "webkit-html-text-nod e webkit-html-css-node");
1464 var text = node.nodeValue();
1465 newNode.textContent = text.startsWith("\n") ? text.substring(1) : text;
1466
1467 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter ("text/css", true);
1468 cssSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSea rchHighlight.bind(this));
1469 } else {
1470 titleDOM.createTextChild("\"");
1471 var textNodeElement = titleDOM.createChild("span", "webkit-html- text-node");
1472 var result = this._convertWhitespaceToEntities(node.nodeValue()) ;
1473 textNodeElement.textContent = result.text;
1474 WebInspector.highlightRangesWithStyleClass(textNodeElement, resu lt.entityRanges, "webkit-html-entity-value");
1475 titleDOM.createTextChild("\"");
1476 if (updateRecord && updateRecord.isCharDataModified())
1477 WebInspector.runCSSAnimationOnce(textNodeElement, "dom-updat e-highlight");
1478 }
1479 break;
1480
1481 case Node.COMMENT_NODE:
1482 var commentElement = titleDOM.createChild("span", "webkit-html-comme nt");
1483 commentElement.createTextChild("<!--" + node.nodeValue() + "-->");
1484 break;
1485
1486 case Node.DOCUMENT_TYPE_NODE:
1487 var docTypeElement = titleDOM.createChild("span", "webkit-html-docty pe");
1488 docTypeElement.createTextChild("<!DOCTYPE " + node.nodeName());
1489 if (node.publicId) {
1490 docTypeElement.createTextChild(" PUBLIC \"" + node.publicId + "\ "");
1491 if (node.systemId)
1492 docTypeElement.createTextChild(" \"" + node.systemId + "\"") ;
1493 } else if (node.systemId)
1494 docTypeElement.createTextChild(" SYSTEM \"" + node.systemId + "\ "");
1495
1496 if (node.internalSubset)
1497 docTypeElement.createTextChild(" [" + node.internalSubset + "]") ;
1498
1499 docTypeElement.createTextChild(">");
1500 break;
1501
1502 case Node.CDATA_SECTION_NODE:
1503 var cdataElement = titleDOM.createChild("span", "webkit-html-text-no de");
1504 cdataElement.createTextChild("<![CDATA[" + node.nodeValue() + "]]>") ;
1505 break;
1506
1507 case Node.DOCUMENT_FRAGMENT_NODE:
1508 var fragmentElement = titleDOM.createChild("span", "webkit-html-frag ment");
1509 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseW hitespace();
1510 break;
1511 default:
1512 titleDOM.createTextChild(node.nodeNameInCorrectCase().collapseWhites pace());
1513 }
1514
1515 /**
1516 * @this {WebInspector.ElementsTreeElement}
1517 */
1518 function updateSearchHighlight()
1519 {
1520 delete this._highlightResult;
1521 this._highlightSearchResults();
1522 }
1523
1524 return titleDOM;
1525 },
1526
1527 remove: function()
1528 {
1529 if (this._node.pseudoType())
1530 return;
1531 var parentElement = this.parent;
1532 if (!parentElement)
1533 return;
1534
1535 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node. DOCUMENT_NODE)
1536 return;
1537 this._node.removeNode();
1538 },
1539
1540 /**
1541 * @param {function(boolean)=} callback
1542 * @param {boolean=} startEditing
1543 */
1544 toggleEditAsHTML: function(callback, startEditing)
1545 {
1546 if (this._editing && this._htmlEditElement && WebInspector.isBeingEdited (this._htmlEditElement)) {
1547 this._editing.commit();
1548 return;
1549 }
1550
1551 if (startEditing === false)
1552 return;
1553
1554 /**
1555 * @param {?Protocol.Error} error
1556 */
1557 function selectNode(error)
1558 {
1559 if (callback)
1560 callback(!error);
1561 }
1562
1563 /**
1564 * @param {string} initialValue
1565 * @param {string} value
1566 */
1567 function commitChange(initialValue, value)
1568 {
1569 if (initialValue !== value)
1570 node.setOuterHTML(value, selectNode);
1571 }
1572
1573 function disposeCallback()
1574 {
1575 if (callback)
1576 callback(false);
1577 }
1578
1579 var node = this._node;
1580 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange, disp oseCallback));
1581 },
1582
1583 _copyCSSPath: function()
1584 {
1585 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath (this._node, true));
1586 },
1587
1588 _copyXPath: function()
1589 {
1590 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(t his._node, true));
1591 },
1592
1593 _highlightSearchResults: function()
1594 {
1595 if (!this._searchQuery || !this._searchHighlightsVisible)
1596 return;
1597 this._hideSearchHighlight();
1598
1599 var text = this.listItemElement.textContent;
1600 var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
1601
1602 var match = regexObject.exec(text);
1603 var matchRanges = [];
1604 while (match) {
1605 matchRanges.push(new WebInspector.SourceRange(match.index, match[0]. length));
1606 match = regexObject.exec(text);
1607 }
1608
1609 // Fall back for XPath, etc. matches.
1610 if (!matchRanges.length)
1611 matchRanges.push(new WebInspector.SourceRange(0, text.length));
1612
1613 this._highlightResult = [];
1614 WebInspector.highlightSearchResults(this.listItemElement, matchRanges, t his._highlightResult);
1615 },
1616
1617 _scrollIntoView: function()
1618 {
1619 function scrollIntoViewCallback(object)
1620 {
1621 /**
1622 * @suppressReceiverCheck
1623 * @this {!Element}
1624 */
1625 function scrollIntoView()
1626 {
1627 this.scrollIntoViewIfNeeded(true);
1628 }
1629
1630 if (object)
1631 object.callFunction(scrollIntoView);
1632 }
1633
1634 this._node.resolveToObject("", scrollIntoViewCallback);
1635 },
1636
1637 _editAsHTML: function()
1638 {
1639 var promise = WebInspector.Revealer.revealPromise(this.node());
1640 promise.then(() => WebInspector.actionRegistry.action("elements.edit-as- html").execute());
1641 },
1642
1643 __proto__: TreeElement.prototype
1644 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698