UNPKG

monaca-lib

Version:

Monaca cloud API bindings for JavaScript

1,364 lines (1,175 loc) 58.1 kB
/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @constructor * @extends {TreeElement} * @param {!WebInspector.DOMNode} node * @param {boolean=} elementCloseTag */ WebInspector.ElementsTreeElement = function(node, elementCloseTag) { // The title will be updated in onattach. TreeElement.call(this, "", elementCloseTag ? null : node); this._node = node; this._elementCloseTag = elementCloseTag; if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag) this._canAddAttributes = true; this._searchQuery = null; this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit; } WebInspector.ElementsTreeElement.InitialChildrenLimit = 500; // A union of HTML4 and HTML5-Draft elements that explicitly // or implicitly (for HTML5) forbid the closing tag. WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [ "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr" ].keySet(); // These tags we do not allow editing their tag name. WebInspector.ElementsTreeElement.EditTagBlacklist = [ "html", "head", "body" ].keySet(); /** @enum {number} */ WebInspector.ElementsTreeElement.ChildrenDisplayMode = { NoChildren: 0, InlineText: 1, HasChildren: 2 } /** * @param {!WebInspector.ElementsTreeElement} treeElement */ WebInspector.ElementsTreeElement.animateOnDOMUpdate = function(treeElement) { var tagName = treeElement.listItemElement.querySelector(".webkit-html-tag-name"); WebInspector.runCSSAnimationOnce(tagName || treeElement.listItemElement, "dom-update-highlight"); } WebInspector.ElementsTreeElement.prototype = { /** * @return {boolean} */ isClosingTag: function() { return !!this._elementCloseTag; }, /** * @return {!WebInspector.DOMNode} */ node: function() { return this._node; }, /** * @return {boolean} */ isEditing: function() { return !!this._editing; }, /** * @param {string} searchQuery */ highlightSearchResults: function(searchQuery) { if (this._searchQuery !== searchQuery) this._hideSearchHighlight(); this._searchQuery = searchQuery; this._searchHighlightsVisible = true; this.updateTitle(true); }, hideSearchHighlights: function() { delete this._searchHighlightsVisible; this._hideSearchHighlight(); }, _hideSearchHighlight: function() { if (!this._highlightResult) return; function updateEntryHide(entry) { switch (entry.type) { case "added": entry.node.remove(); break; case "changed": entry.node.textContent = entry.oldText; break; } } for (var i = (this._highlightResult.length - 1); i >= 0; --i) updateEntryHide(this._highlightResult[i]); delete this._highlightResult; }, /** * @param {boolean} inClipboard */ setInClipboard: function(inClipboard) { if (this._inClipboard === inClipboard) return; this._inClipboard = inClipboard; this.listItemElement.classList.toggle("in-clipboard", inClipboard); }, get hovered() { return this._hovered; }, set hovered(x) { if (this._hovered === x) return; this._hovered = x; if (this.listItemElement) { if (x) { this.updateSelection(); this.listItemElement.classList.add("hovered"); } else { this.listItemElement.classList.remove("hovered"); } } }, /** * @return {number} */ expandedChildrenLimit: function() { return this._expandedChildrenLimit; }, /** * @param {number} expandedChildrenLimit */ setExpandedChildrenLimit: function(expandedChildrenLimit) { this._expandedChildrenLimit = expandedChildrenLimit; }, /** * @return {!WebInspector.ElementsTreeElement.ChildrenDisplayMode} */ childrenDisplayMode: function() { return this._childrenDisplayMode; }, /** * @param {!WebInspector.ElementsTreeElement.ChildrenDisplayMode} displayMode */ setChildrenDisplayMode: function(displayMode) { this._childrenDisplayMode = displayMode; }, updateSelection: function() { var listItemElement = this.listItemElement; if (!listItemElement) return; if (!this._readyToUpdateSelection) { if (listItemElement.ownerDocument.body.offsetWidth > 0) this._readyToUpdateSelection = true; else { // The stylesheet hasn't loaded yet or the window is closed, // so we can't calculate what we need. Return early. return; } } if (!this.selectionElement) { this.selectionElement = createElement("div"); this.selectionElement.className = "selection selected"; listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); } this.selectionElement.style.height = listItemElement.offsetHeight + "px"; }, onattach: function() { if (this._hovered) { this.updateSelection(); this.listItemElement.classList.add("hovered"); } this.updateTitle(); this._preventFollowingLinksOnDoubleClick(); this.listItemElement.draggable = true; }, _preventFollowingLinksOnDoubleClick: function() { var links = this.listItemElement.querySelectorAll("li .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link"); if (!links) return; for (var i = 0; i < links.length; ++i) links[i].preventFollowOnDoubleClick = true; }, onpopulate: function() { this.populated = true; this.treeOutline.populateTreeElement(this); }, /** * @param {?WebInspector.ElementsTreeOutline.UpdateInfo} updateInfo */ setUpdateInfo: function(updateInfo) { this._updateInfo = updateInfo; }, expandRecursively: function() { /** * @this {WebInspector.ElementsTreeElement} */ function callback() { TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE); } this._node.getSubtree(-1, callback.bind(this)); }, /** * @override */ onexpand: function() { if (this._elementCloseTag) return; this.updateTitle(); this.treeOutline.updateSelection(); }, oncollapse: function() { if (this._elementCloseTag) return; this.updateTitle(); this.treeOutline.updateSelection(); }, /** * @override */ onreveal: function() { if (this.listItemElement) { var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name"); if (tagSpans.length) tagSpans[0].scrollIntoViewIfNeeded(true); else this.listItemElement.scrollIntoViewIfNeeded(true); } }, /** * @override * @param {boolean=} omitFocus * @param {boolean=} selectedByUser * @return {boolean} */ select: function(omitFocus, selectedByUser) { if (this._editing) return false; if (this.treeOutline.handlePickNode(this.title, this._node)) return true; return TreeElement.prototype.select.call(this, omitFocus, selectedByUser); }, /** * @override * @param {boolean=} selectedByUser * @return {boolean} */ onselect: function(selectedByUser) { this.treeOutline.suppressRevealAndSelect = true; this.treeOutline.selectDOMNode(this._node, selectedByUser); if (selectedByUser) this._node.highlight(); this.updateSelection(); this.treeOutline.suppressRevealAndSelect = false; return true; }, /** * @override * @return {boolean} */ ondelete: function() { var startTagTreeElement = this.treeOutline.findTreeElement(this._node); startTagTreeElement ? startTagTreeElement.remove() : this.remove(); return true; }, /** * @override * @return {boolean} */ onenter: function() { // On Enter or Return start editing the first attribute // or create a new attribute on the selected element. if (this._editing) return false; this._startEditing(); // prevent a newline from being immediately inserted return true; }, selectOnMouseDown: function(event) { TreeElement.prototype.selectOnMouseDown.call(this, event); if (this._editing) return; // Prevent selecting the nearest word on double click. if (event.detail >= 2) event.preventDefault(); }, /** * @override * @return {boolean} */ ondblclick: function(event) { if (this._editing || this._elementCloseTag) return false; if (this._startEditingTarget(/** @type {!Element} */(event.target))) return false; if (this.hasChildren && !this.expanded) this.expand(); return false; }, /** * @return {boolean} */ hasEditableNode: function() { return !this._node.isShadowRoot() && !this._node.ancestorUserAgentShadowRoot(); }, _insertInLastAttributePosition: function(tag, node) { if (tag.getElementsByClassName("webkit-html-attribute").length > 0) tag.insertBefore(node, tag.lastChild); else { var nodeName = tag.textContent.match(/^<(.*?)>$/)[1]; tag.textContent = ''; tag.createTextChild('<' + nodeName); tag.appendChild(node); tag.createTextChild('>'); } this.updateSelection(); }, /** * @param {!Element} eventTarget * @return {boolean} */ _startEditingTarget: function(eventTarget) { if (this.treeOutline.selectedDOMNode() != this._node) return false; if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE) return false; if (this.treeOutline.pickNodeMode()) return false; var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node"); if (textNode) return this._startEditingTextNode(textNode); var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute"); if (attribute) return this._startEditingAttribute(attribute, eventTarget); var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name"); if (tagName) return this._startEditingTagName(tagName); var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute"); if (newAttribute) return this._addNewAttribute(); return false; }, /** * @param {!WebInspector.ContextMenu} contextMenu * @param {!Event} event */ populateTagContextMenu: function(contextMenu, event) { // Add attribute-related actions. var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this; contextMenu.appendItem(WebInspector.UIString.capitalize("Add ^attribute"), treeElement._addNewAttribute.bind(treeElement)); var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); if (attribute && !newAttribute) contextMenu.appendItem(WebInspector.UIString.capitalize("Edit ^attribute"), this._startEditingAttribute.bind(this, attribute, event.target)); contextMenu.appendSeparator(); if (this.treeOutline.setPseudoClassCallback) { var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString.capitalize("Force ^element ^state")); this._populateForcedPseudoStateItems(pseudoSubMenu); contextMenu.appendSeparator(); } this.populateNodeContextMenu(contextMenu); this.populateScrollIntoView(contextMenu); }, /** * @param {!WebInspector.ContextMenu} contextMenu */ populateScrollIntoView: function(contextMenu) { contextMenu.appendSeparator(); contextMenu.appendItem(WebInspector.UIString.capitalize("Scroll into ^view"), this._scrollIntoView.bind(this)); }, _populateForcedPseudoStateItems: function(subMenu) { const pseudoClasses = ["active", "hover", "focus", "visited"]; var node = this._node; var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || []; for (var i = 0; i < pseudoClasses.length; ++i) { var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0; var setPseudoClassCallback = this.treeOutline.setPseudoClassCallback.bind(this.treeOutline, node, pseudoClasses[i], !pseudoClassForced); subMenu.appendCheckboxItem(":" + pseudoClasses[i], setPseudoClassCallback, pseudoClassForced, false); } }, populateTextContextMenu: function(contextMenu, textNode) { if (!this._editing) contextMenu.appendItem(WebInspector.UIString.capitalize("Edit ^text"), this._startEditingTextNode.bind(this, textNode)); this.populateNodeContextMenu(contextMenu); }, populateNodeContextMenu: function(contextMenu) { // Add free-form node-related actions. var openTagElement = this.treeOutline.getCachedTreeElement(this._node) || this; var isEditable = this.hasEditableNode(); if (isEditable && !this._editing) contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement.toggleEditAsHTML.bind(openTagElement)); var isShadowRoot = this._node.isShadowRoot(); // Place it here so that all "Copy"-ing items stick together. if (this._node.nodeType() === Node.ELEMENT_NODE) contextMenu.appendItem(WebInspector.UIString.capitalize("Copy CSS ^path"), this._copyCSSPath.bind(this)); if (!isShadowRoot) contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this)); if (!isShadowRoot) { var treeOutline = this.treeOutline; contextMenu.appendSeparator(); contextMenu.appendItem(WebInspector.UIString("Cut"), treeOutline.performCopyOrCut.bind(treeOutline, true, this._node), !this.hasEditableNode()); contextMenu.appendItem(WebInspector.UIString("Copy"), treeOutline.performCopyOrCut.bind(treeOutline, false, this._node)); contextMenu.appendItem(WebInspector.UIString("Paste"), treeOutline.pasteNode.bind(treeOutline, this._node), !treeOutline.canPaste(this._node)); } if (isEditable) contextMenu.appendItem(WebInspector.UIString("Delete"), this.remove.bind(this)); contextMenu.appendSeparator(); }, _startEditing: function() { if (this.treeOutline.selectedDOMNode() !== this._node) return; var listItem = this._listItemNode; if (this._canAddAttributes) { var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0]; if (attribute) return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]); return this._addNewAttribute(); } if (this._node.nodeType() === Node.TEXT_NODE) { var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0]; if (textNode) return this._startEditingTextNode(textNode); return; } }, _addNewAttribute: function() { // Cannot just convert the textual html into an element without // a parent node. Use a temporary span container for the HTML. var container = createElement("span"); this._buildAttributeDOM(container, " ", ""); var attr = container.firstElementChild; attr.style.marginLeft = "2px"; // overrides the .editing margin rule attr.style.marginRight = "2px"; // overrides the .editing margin rule var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0]; this._insertInLastAttributePosition(tag, attr); attr.scrollIntoViewIfNeeded(true); return this._startEditingAttribute(attr, attr); }, _triggerEditAttribute: function(attributeName) { var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name"); for (var i = 0, len = attributeElements.length; i < len; ++i) { if (attributeElements[i].textContent === attributeName) { for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) { if (elem.nodeType !== Node.ELEMENT_NODE) continue; if (elem.classList.contains("webkit-html-attribute-value")) return this._startEditingAttribute(elem.parentNode, elem); } } } }, _startEditingAttribute: function(attribute, elementForSelection) { console.assert(this.listItemElement.isAncestor(attribute)); if (WebInspector.isBeingEdited(attribute)) return true; var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; if (!attributeNameElement) return false; var attributeName = attributeNameElement.textContent; var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0]; // Make sure elementForSelection is not a child of attributeValueElement. elementForSelection = attributeValueElement.isAncestor(elementForSelection) ? attributeValueElement : elementForSelection; function removeZeroWidthSpaceRecursive(node) { if (node.nodeType === Node.TEXT_NODE) { node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; for (var child = node.firstChild; child; child = child.nextSibling) removeZeroWidthSpaceRecursive(child); } var attributeValue = attributeName && attributeValueElement ? this._node.getAttribute(attributeName) : undefined; if (attributeValue !== undefined) attributeValueElement.textContent = attributeValue; // Remove zero-width spaces that were added by nodeTitleInfo. removeZeroWidthSpaceRecursive(attribute); var config = new WebInspector.InplaceEditor.Config(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName); function handleKeyDownEvents(event) { var isMetaOrCtrl = WebInspector.isMac() ? event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey : event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey; if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl)) return "commit"; else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B") return "cancel"; else if (event.keyIdentifier === "U+0009") // Tab key return "move-" + (event.shiftKey ? "backward" : "forward"); else { WebInspector.handleElementValueModifications(event, attribute); return ""; } } config.customFinishHandler = handleKeyDownEvents; this._editing = WebInspector.InplaceEditor.startEditing(attribute, config); this.listItemElement.getComponentSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1); return true; }, /** * @param {!Element} textNodeElement */ _startEditingTextNode: function(textNodeElement) { if (WebInspector.isBeingEdited(textNodeElement)) return true; var textNode = this._node; // We only show text nodes inline in elements if the element only // has a single child, and that child is a text node. if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild) textNode = textNode.firstChild; var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node"); if (container) container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present. var config = new WebInspector.InplaceEditor.Config(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this)); this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, config); this.listItemElement.getComponentSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1); return true; }, /** * @param {!Element=} tagNameElement */ _startEditingTagName: function(tagNameElement) { if (!tagNameElement) { tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0]; if (!tagNameElement) return false; } var tagName = tagNameElement.textContent; if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()]) return false; if (WebInspector.isBeingEdited(tagNameElement)) return true; var closingTagElement = this._distinctClosingTagElement(); /** * @param {!Event} event */ function keyupListener(event) { if (closingTagElement) closingTagElement.textContent = "</" + tagNameElement.textContent + ">"; } /** * @param {!Element} element * @param {string} newTagName * @this {WebInspector.ElementsTreeElement} */ function editingComitted(element, newTagName) { tagNameElement.removeEventListener('keyup', keyupListener, false); this._tagNameEditingCommitted.apply(this, arguments); } /** * @this {WebInspector.ElementsTreeElement} */ function editingCancelled() { tagNameElement.removeEventListener('keyup', keyupListener, false); this._editingCancelled.apply(this, arguments); } tagNameElement.addEventListener('keyup', keyupListener, false); var config = new WebInspector.InplaceEditor.Config(editingComitted.bind(this), editingCancelled.bind(this), tagName); this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, config); this.listItemElement.getComponentSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1); return true; }, /** * @param {function(string, string)} commitCallback * @param {?Protocol.Error} error * @param {string} initialValue */ _startEditingAsHTML: function(commitCallback, error, initialValue) { if (error) return; if (this._editing) return; function consume(event) { if (event.eventPhase === Event.AT_TARGET) event.consume(true); } initialValue = this._convertWhitespaceToEntities(initialValue).text; this._htmlEditElement = createElement("div"); this._htmlEditElement.className = "source-code elements-tree-editor"; // Hide header items. var child = this.listItemElement.firstChild; while (child) { child.style.display = "none"; child = child.nextSibling; } // Hide children item. if (this._childrenListNode) this._childrenListNode.style.display = "none"; // Append editor. this.listItemElement.appendChild(this._htmlEditElement); this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false); this.updateSelection(); /** * @param {!Element} element * @param {string} newValue * @this {WebInspector.ElementsTreeElement} */ function commit(element, newValue) { commitCallback(initialValue, newValue); dispose.call(this); } /** * @this {WebInspector.ElementsTreeElement} */ function dispose() { delete this._editing; this.treeOutline.setMultilineEditing(null); // Remove editor. this.listItemElement.removeChild(this._htmlEditElement); delete this._htmlEditElement; // Unhide children item. if (this._childrenListNode) this._childrenListNode.style.removeProperty("display"); // Unhide header items. var child = this.listItemElement.firstChild; while (child) { child.style.removeProperty("display"); child = child.nextSibling; } this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false); this.updateSelection(); this.treeOutline.focus(); } var config = new WebInspector.InplaceEditor.Config(commit.bind(this), dispose.bind(this)); config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true); WebInspector.InplaceEditor.startMultilineEditing(this._htmlEditElement, config).then(markAsBeingEdited.bind(this)); /** * @param {!Object} controller * @this {WebInspector.ElementsTreeElement} */ function markAsBeingEdited(controller) { this._editing = /** @type {!WebInspector.InplaceEditor.Controller} */ (controller); this._editing.setWidth(this.treeOutline.visibleWidth()); this.treeOutline.setMultilineEditing(this._editing); } }, _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection) { delete this._editing; var treeOutline = this.treeOutline; /** * @param {?Protocol.Error=} error * @this {WebInspector.ElementsTreeElement} */ function moveToNextAttributeIfNeeded(error) { if (error) this._editingCancelled(element, attributeName); if (!moveDirection) return; treeOutline.runPendingUpdates(); // Search for the attribute's position, and then decide where to move to. var attributes = this._node.attributes(); for (var i = 0; i < attributes.length; ++i) { if (attributes[i].name !== attributeName) continue; if (moveDirection === "backward") { if (i === 0) this._startEditingTagName(); else this._triggerEditAttribute(attributes[i - 1].name); } else { if (i === attributes.length - 1) this._addNewAttribute(); else this._triggerEditAttribute(attributes[i + 1].name); } return; } // Moving From the "New Attribute" position. if (moveDirection === "backward") { if (newText === " ") { // Moving from "New Attribute" that was not edited if (attributes.length > 0) this._triggerEditAttribute(attributes[attributes.length - 1].name); } else { // Moving from "New Attribute" that holds new value if (attributes.length > 1) this._triggerEditAttribute(attributes[attributes.length - 2].name); } } else if (moveDirection === "forward") { if (!/^\s*$/.test(newText)) this._addNewAttribute(); else this._startEditingTagName(); } } if ((attributeName.trim() || newText.trim()) && oldText !== newText) { this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this)); return; } this.updateTitle(); moveToNextAttributeIfNeeded.call(this); }, _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection) { delete this._editing; var self = this; function cancel() { var closingTagElement = self._distinctClosingTagElement(); if (closingTagElement) closingTagElement.textContent = "</" + tagName + ">"; self._editingCancelled(element, tagName); moveToNextAttributeIfNeeded.call(self); } /** * @this {WebInspector.ElementsTreeElement} */ function moveToNextAttributeIfNeeded() { if (moveDirection !== "forward") { this._addNewAttribute(); return; } var attributes = this._node.attributes(); if (attributes.length > 0) this._triggerEditAttribute(attributes[0].name); else this._addNewAttribute(); } newText = newText.trim(); if (newText === oldText) { cancel(); return; } var treeOutline = this.treeOutline; var wasExpanded = this.expanded; function changeTagNameCallback(error, nodeId) { if (error || !nodeId) { cancel(); return; } var newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error, nodeId); moveToNextAttributeIfNeeded.call(newTreeItem); } this._node.setNodeName(newText, changeTagNameCallback); }, /** * @param {!WebInspector.DOMNode} textNode * @param {!Element} element * @param {string} newText */ _textNodeEditingCommitted: function(textNode, element, newText) { delete this._editing; /** * @this {WebInspector.ElementsTreeElement} */ function callback() { this.updateTitle(); } textNode.setNodeValue(newText, callback.bind(this)); }, /** * @param {!Element} element * @param {*} context */ _editingCancelled: function(element, context) { delete this._editing; // Need to restore attributes structure. this.updateTitle(); }, /** * @return {!Element} */ _distinctClosingTagElement: function() { // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM // For an expanded element, it will be the last element with class "close" // in the child element list. if (this.expanded) { var closers = this._childrenListNode.querySelectorAll(".close"); return closers[closers.length-1]; } // Remaining cases are single line non-expanded elements with a closing // tag, or HTML elements without a closing tag (such as <br>). Return // null in the case where there isn't a closing tag. var tags = this.listItemElement.getElementsByClassName("webkit-html-tag"); return (tags.length === 1 ? null : tags[tags.length-1]); }, /** * @param {boolean=} onlySearchQueryChanged */ updateTitle: function(onlySearchQueryChanged) { // If we are editing, return early to prevent canceling the edit. // After editing is committed updateTitle will be called. if (this._editing) return; if (onlySearchQueryChanged) { this._hideSearchHighlight(); } else { var nodeInfo = this._nodeTitleInfo(WebInspector.linkifyURLAsNode); if (nodeInfo.shadowRoot) this.listItemElement.classList.add("shadow-root"); var highlightElement = createElement("span"); highlightElement.className = "highlight"; highlightElement.appendChild(nodeInfo.titleDOM); this.title = highlightElement; this._updateDecorations(); delete this._highlightResult; } delete this.selectionElement; if (this.selected) this.updateSelection(); this._preventFollowingLinksOnDoubleClick(); this._highlightSearchResults(); }, /** * @return {?Element} */ _createDecoratorElement: function() { var node = this._node; var decoratorMessages = []; var parentDecoratorMessages = []; var decorators = this.treeOutline.nodeDecorators(); for (var i = 0; i < decorators.length; ++i) { var decorator = decorators[i]; var message = decorator.decorate(node); if (message) { decoratorMessages.push(message); continue; } if (this.expanded || this._elementCloseTag) continue; message = decorator.decorateAncestor(node); if (message) parentDecoratorMessages.push(message) } if (!decoratorMessages.length && !parentDecoratorMessages.length) return null; var decoratorElement = createElement("div"); decoratorElement.classList.add("elements-gutter-decoration"); if (!decoratorMessages.length) decoratorElement.classList.add("elements-has-decorated-children"); decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n"); return decoratorElement; }, _updateDecorations: function() { if (this._decoratorElement) this._decoratorElement.remove(); this._decoratorElement = this._createDecoratorElement(); if (this._decoratorElement && this.listItemElement) this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild); }, /** * @param {!Node} parentElement * @param {string} name * @param {string} value * @param {boolean=} forceValue * @param {!WebInspector.DOMNode=} node * @param {function(string, string, string, boolean=, string=)=} linkify */ _buildAttributeDOM: function(parentElement, name, value, forceValue, node, linkify) { var closingPunctuationRegex = /[\/;:\)\]\}]/g; var highlightIndex = 0; var highlightCount; var additionalHighlightOffset = 0; var result; /** * @param {string} match * @param {number} replaceOffset * @return {string} */ function replacer(match, replaceOffset) { while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) { result.entityRanges[highlightIndex].offset += additionalHighlightOffset; ++highlightIndex; } additionalHighlightOffset += 1; return match + "\u200B"; } /** * @param {!Element} element * @param {string} value * @this {WebInspector.ElementsTreeElement} */ function setValueWithEntities(element, value) { result = this._convertWhitespaceToEntities(value); highlightCount = result.entityRanges.length; value = result.text.replace(closingPunctuationRegex, replacer); while (highlightIndex < highlightCount) { result.entityRanges[highlightIndex].offset += additionalHighlightOffset; ++highlightIndex; } element.textContent = value; WebInspector.highlightRangesWithStyleClass(element, result.entityRanges, "webkit-html-entity-value"); } var hasText = (forceValue || value.length > 0); var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute"); var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name"); attrNameElement.textContent = name; if (hasText) attrSpanElement.createTextChild("=\u200B\""); var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value"); var updates = this._updateInfo; if (updates && updates.isAttributeModified(name)) WebInspector.runCSSAnimationOnce(hasText ? attrValueElement : attrNameElement, "dom-update-highlight"); /** * @this {WebInspector.ElementsTreeElement} * @param {string} value * @return {!Element} */ function linkifyValue(value) { var rewrittenHref = node.resolveURL(value); if (rewrittenHref === null) { var span = createElement("span"); setValueWithEntities.call(this, span, value); return span; } value = value.replace(closingPunctuationRegex, "$&\u200B"); if (value.startsWith("data:")) value = value.trimMiddle(60); var anchor = linkify(rewrittenHref, value, "", node.nodeName().toLowerCase() === "a"); anchor.preventFollow = true; return anchor; } if (linkify && (name === "src" || name === "href")) { attrValueElement.appendChild(linkifyValue.call(this, value)); } else if (linkify && node.nodeName().toLowerCase() === "img" && name === "srcset") { var sources = value.split(","); for (var i = 0; i < sources.length; ++i) { if (i > 0) attrValueElement.createTextChild(", "); var source = sources[i].trim(); var indexOfSpace = source.indexOf(" "); var url = source.substring(0, indexOfSpace); var tail = source.substring(indexOfSpace); attrValueElement.appendChild(linkifyValue.call(this, url)); attrValueElement.createTextChild(tail); } } else { setValueWithEntities.call(this, attrValueElement, value); } if (hasText) attrSpanElement.createTextChild("\""); }, /** * @param {!Node} parentElement * @param {string} pseudoElementName */ _buildPseudoElementDOM: function(parentElement, pseudoElementName) { var pseudoElement = parentElement.createChild("span", "webkit-html-pseudo-element"); pseudoElement.textContent = "::" + pseudoElementName; parentElement.createTextChild("\u200B"); }, /** * @param {!Node} parentElement * @param {string} tagName * @param {boolean} isClosingTag * @param {boolean} isDistinctTreeElement * @param {function(string, string, string, boolean=, string=)=} linkify */ _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify) { var node = this._node; var classes = [ "webkit-html-tag" ]; if (isClosingTag && isDistinctTreeElement) classes.push("close"); var tagElement = parentElement.createChild("span", classes.join(" ")); tagElement.createTextChild("<"); var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name"); tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName; if (!isClosingTag) { if (node.hasAttributes()) { var attributes = node.attributes(); for (var i = 0; i < attributes.length; ++i) { var attr = attributes[i]; tagElement.createTextChild(" "); this._buildAttributeDOM(tagElement, attr.name, attr.value, false, node, linkify); } } var hasUpdates; var updates = this._updateInfo; if (updates) { hasUpdates |= updates.hasRemovedAttributes(); var hasInlineText = this._childrenDisplayMode === WebInspector.ElementsTreeElement.ChildrenDisplayMode.InlineText; hasUpdates |= (!hasInlineText || this.expanded) && updates.hasChangedChildren(); // Highlight the tag name, as the inserted node is not visible (either child of a collapsed tree element or empty inline text). hasUpdates |= !this.expanded && updates.hasInsertedNodes() && (!hasInlineText || this._node.firstChild.nodeValue().length === 0); // Highlight the tag name, as the inline text node value has been cleared. // The respective empty node will be highlighted, but the highlight will not be visible to the user. hasUpdates |= hasInlineText && (updates.isCharDataModified() || updates.hasChangedChildren()) && this._node.firstChild.nodeValue().length === 0; } if (hasUpdates) WebInspector.runCSSAnimationOnce(tagNameElement, "dom-update-highlight"); } tagElement.createTextChild(">"); parentElement.createTextChild("\u200B"); }, /** * @param {string} text * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}} */ _convertWhitespaceToEntities: function(text) { var result = ""; var lastIndexAfterEntity = 0; var entityRanges = []; var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity; for (var i = 0, size = text.length; i < size; ++i) { var char = text.charAt(i); if (charToEntity[char]) { result += text.substring(lastIndexAfterEntity, i); var entityValue = "&" + charToEntity[char] + ";"; entityRanges.push({offset: result.length, length: entityValue.length}); result += entityValue; lastIndexAfterEntity = i + 1; } } if (result) result += text.substring(lastIndexAfterEntity); return {text: result || text, entityRanges: entityRanges}; }, /** * @param {function(string, string, string, boolean=, string=)=} linkify */ _nodeTitleInfo: function(linkify) { var node = this._node; var info = {titleDOM: createDocumentFragment(), hasChildren: this.hasChildren}; switch (node.nodeType()) { case Node.ATTRIBUTE_NODE: this._buildAttributeDOM(info.titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), true); break; case Node.ELEMENT_NODE: var pseudoType = node.pseudoType(); if (pseudoType) { this._buildPseudoElementDOM(info.titleDOM, pseudoType); info.hasChildren = false; break; } var tagName = node.nodeNameInCorrectCase(); if (this._elementCloseTag) { this._buildTagDOM(info.titleDOM, tagName, true, true); info.hasChildren = false; break; } this._buildTagDOM(info.titleDOM, tagName, false, false, linkify); switch (this._childrenDisplayMode) { case WebInspector.ElementsTreeElement.ChildrenDisplayMode.HasChildren: if (!this.expanded) { var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus"); textNodeElement.textContent = "\u2026"; info.titleDOM.createTextChild("\u200B"); this._buildTagDOM(info.titleDOM, tagName, true, false); } break; case WebInspector.ElementsTreeElement.ChildrenDisplayMode.InlineText: var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node"); var result = this._convertWhitespaceToEntities(node.firstChild.nodeValue()); textNodeElement.textContent = result.text; WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value"); info.titleDOM.createTextChild("\u200B"); info.hasChildren = false; this._buildTagDOM(info.titleDOM, tagName, true, false); var updates = this._updateInfo; if (updates && (updates.hasInsertedNodes() || updates.hasChangedChildren())) WebInspector.runCSSAnimationOnce(textNodeElement, "dom-update-highlight"); updates = this._updateInfo; if (updates && updates.isCharDataModified()) WebInspector.runCSSAnimationOnce(textNodeElement, "dom-update-highlight"); break; case WebInspector.ElementsTreeElement.ChildrenDisplayMode.NoChildren: if (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]) this._buildTagDOM(info.titleDOM, tagName, true, false); break; } break; case Node.TEXT_NODE: if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") { var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node"); newNode.textContent = node.nodeValue(); var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true); javascriptSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSearchHighlight.bind(this)); } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") { var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node"); newNode.textContent = node.nodeValue(); var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true); cssSyntaxHighlighter.syntaxHighlightNode(newNode).then(updateSearchHighlight.bind(this)); } else { info.titleDOM.createTextChild("\""); var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node"); var result = this._convertWhitespaceToEntities(node.nodeValue()); textNodeElement.textContent = result.text; WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value"); info.titleDOM.createTextChild("\""); var updates = this._updateInfo; if (updates && updates.isCharDataModified())