| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * 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. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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. |
| */ |
| |
| WebInspector.CSSStyleDeclarationTextEditor = function(delegate, style, element) |
| { |
| WebInspector.Object.call(this); |
| |
| this._element = element || document.createElement("div"); |
| this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName); |
| this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName); |
| |
| this._showsImplicitProperties = true; |
| this._alwaysShowPropertyNames = {}; |
| this._sortProperties = false; |
| |
| this._prefixWhitespace = ""; |
| this._suffixWhitespace = ""; |
| this._linePrefixWhitespace = ""; |
| |
| this._delegate = delegate || null; |
| |
| this._codeMirror = CodeMirror(this.element, { |
| readOnly: true, |
| lineWrapping: true, |
| mode: "css-rule", |
| electricChars: false, |
| indentWithTabs: true, |
| indentUnit: 4, |
| smartIndent: false, |
| matchBrackets: true, |
| autoCloseBrackets: true |
| }); |
| |
| this._codeMirror.on("change", this._contentChanged.bind(this)); |
| this._codeMirror.on("blur", this._editorBlured.bind(this)); |
| |
| this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this); |
| this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this); |
| |
| this._jumpToSymbolTrackingModeEnabled = false; |
| this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName; |
| this._tokenTrackingController.mouseOverDelayDuration = 0; |
| this._tokenTrackingController.mouseOutReleaseDelayDuration = 0; |
| this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens; |
| |
| this.style = style; |
| }; |
| |
| WebInspector.Object.addConstructorFunctions(WebInspector.CSSStyleDeclarationTextEditor); |
| |
| WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor"; |
| WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only"; |
| WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch"; |
| WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder"; |
| WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line"; |
| WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250; |
| WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000; |
| |
| WebInspector.CSSStyleDeclarationTextEditor.prototype = { |
| constructor: WebInspector.CSSStyleDeclarationTextEditor, |
| |
| // Public |
| |
| get element() |
| { |
| return this._element; |
| }, |
| |
| get delegate() |
| { |
| return this._delegate; |
| }, |
| |
| set delegate(delegate) |
| { |
| this._delegate = delegate || null; |
| }, |
| |
| get style() |
| { |
| return this._style; |
| }, |
| |
| set style(style) |
| { |
| if (this._style === style) |
| return; |
| |
| if (this._style) { |
| this._style.removeEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); |
| if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation) |
| WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this); |
| } |
| |
| this._style = style || null; |
| |
| if (this._style) { |
| this._style.addEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); |
| if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation) |
| WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this); |
| } |
| |
| this._updateJumpToSymbolTrackingMode(); |
| |
| this._resetContent(); |
| }, |
| |
| get focused() |
| { |
| return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused"); |
| }, |
| |
| get alwaysShowPropertyNames() |
| { |
| return Object.keys(this._alwaysShowPropertyNames); |
| }, |
| |
| set alwaysShowPropertyNames(alwaysShowPropertyNames) |
| { |
| this._alwaysShowPropertyNames = (alwaysShowPropertyNames || []).keySet(); |
| |
| this._resetContent(); |
| }, |
| |
| get showsImplicitProperties() |
| { |
| return this._showsImplicitProperties; |
| }, |
| |
| set showsImplicitProperties(showsImplicitProperties) |
| { |
| if (this._showsImplicitProperties === showsImplicitProperties) |
| return; |
| |
| this._showsImplicitProperties = showsImplicitProperties; |
| |
| this._resetContent(); |
| }, |
| |
| get sortProperties() |
| { |
| return this._sortProperties; |
| }, |
| |
| set sortProperties(sortProperties) |
| { |
| if (this._sortProperties === sortProperties) |
| return; |
| |
| this._sortProperties = sortProperties; |
| |
| this._resetContent(); |
| }, |
| |
| focus: function() |
| { |
| this._codeMirror.focus(); |
| }, |
| |
| refresh: function() |
| { |
| this._resetContent(); |
| }, |
| |
| updateLayout: function(force) |
| { |
| this._codeMirror.refresh(); |
| }, |
| |
| // Protected |
| |
| didDismissPopover: function(popover) |
| { |
| if (popover === this._colorPickerPopover) |
| delete this._colorPickerPopover; |
| }, |
| |
| completionControllerCompletionsHidden: function(completionController) |
| { |
| var styleText = this._style.text; |
| var currentText = this._formattedContent(); |
| |
| // If the style text and the current editor text differ then we need to commit. |
| // Otherwise we can just update the properties that got skipped because a completion |
| // was pending the last time _propertiesChanged was called. |
| if (styleText !== currentText) |
| this._commitChanges(); |
| else |
| this._propertiesChanged(); |
| }, |
| |
| // Private |
| |
| _clearRemoveEditingLineClassesTimeout: function() |
| { |
| if (!this._removeEditingLineClassesTimeout) |
| return; |
| |
| clearTimeout(this._removeEditingLineClassesTimeout); |
| delete this._removeEditingLineClassesTimeout; |
| }, |
| |
| _removeEditingLineClasses: function() |
| { |
| this._clearRemoveEditingLineClassesTimeout(); |
| |
| function removeEditingLineClasses() |
| { |
| var lineCount = this._codeMirror.lineCount(); |
| for (var i = 0; i < lineCount; ++i) |
| this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); |
| } |
| |
| this._codeMirror.operation(removeEditingLineClasses.bind(this)); |
| }, |
| |
| _removeEditingLineClassesSoon: function() |
| { |
| if (this._removeEditingLineClassesTimeout) |
| return; |
| this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay); |
| }, |
| |
| _formattedContent: function() |
| { |
| // Start with the prefix whitespace we stripped. |
| var content = this._prefixWhitespace; |
| |
| // Get each line and add the line prefix whitespace and newlines. |
| var lineCount = this._codeMirror.lineCount(); |
| for (var i = 0; i < lineCount; ++i) { |
| var lineContent = this._codeMirror.getLine(i); |
| content += this._linePrefixWhitespace + lineContent; |
| if (i !== lineCount - 1) |
| content += "\n"; |
| } |
| |
| // Add the suffix whitespace we stripped. |
| content += this._suffixWhitespace; |
| |
| return content; |
| }, |
| |
| _commitChanges: function() |
| { |
| if (this._commitChangesTimeout) { |
| clearTimeout(this._commitChangesTimeout); |
| delete this._commitChangesTimeout; |
| } |
| |
| this._style.text = this._formattedContent(); |
| }, |
| |
| _editorBlured: function(codeMirror) |
| { |
| // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case. |
| if (this._completionController.isHandlingClickEvent()) |
| return; |
| |
| // Reset the content on blur since we stop accepting external changes while the the editor is focused. |
| // This causes us to pick up any change that was suppressed while the editor was focused. |
| this._resetContent(); |
| }, |
| |
| _contentChanged: function(codeMirror, change) |
| { |
| // Return early if the style isn't editable. This still can be called when readOnly is set because |
| // clicking on a color swatch modifies the text. |
| if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent) |
| return; |
| |
| this._markLinesWithCheckboxPlaceholder(); |
| |
| this._clearRemoveEditingLineClassesTimeout(); |
| this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); |
| |
| // When the change is a completion change, create color swatches now since the changes |
| // will not go through _propertiesChanged until completionControllerCompletionsHidden happens. |
| // This way any auto completed colors get swatches right away. |
| if (this._completionController.isCompletionChange(change)) |
| this._createColorSwatches(false, change.from.line); |
| |
| // Use a short delay for user input to coalesce more changes before committing. Other actions like |
| // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that |
| // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing. |
| const delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0; |
| |
| // Reset the timeout so rapid changes coalesce after a short delay. |
| if (this._commitChangesTimeout) |
| clearTimeout(this._commitChangesTimeout); |
| this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay); |
| }, |
| |
| _updateTextMarkers: function(nonatomic) |
| { |
| function update() |
| { |
| this._clearTextMarkers(true); |
| |
| var styleText = this._style.text; |
| |
| this._iterateOverProperties(true, function(property) { |
| var styleTextRange = property.styleDeclarationTextRange; |
| console.assert(styleTextRange); |
| if (!styleTextRange) |
| return; |
| |
| var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn}; |
| var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn}; |
| |
| // Adjust the line position for the missing prefix line. |
| if (this._prefixWhitespace) { |
| --from.line; |
| --to.line; |
| } |
| |
| // Adjust the column for the stripped line prefix whitespace. |
| from.ch -= this._linePrefixWhitespace.length; |
| to.ch -= this._linePrefixWhitespace.length; |
| |
| this._createTextMarkerForPropertyIfNeeded(from, to, property); |
| }); |
| |
| if (!this._codeMirror.getOption("readOnly")) { |
| // Matches a comment like: /* -webkit-foo: bar; */ |
| const commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g; |
| |
| // Look for comments that look like properties and add checkboxes in front of them. |
| var lineCount = this._codeMirror.lineCount(); |
| for (var i = 0; i < lineCount; ++i) { |
| var lineContent = this._codeMirror.getLine(i); |
| |
| var match = commentedPropertyRegex.exec(lineContent); |
| while (match) { |
| var checkboxElement = document.createElement("input"); |
| checkboxElement.type = "checkbox"; |
| checkboxElement.checked = false; |
| checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this)); |
| |
| var from = {line: i, ch: match.index}; |
| var to = {line: i, ch: match.index + match[0].length}; |
| |
| var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement); |
| checkboxMarker.__propertyCheckbox = true; |
| |
| var commentTextMarker = this._codeMirror.markText(from, to); |
| |
| checkboxElement.__commentTextMarker = commentTextMarker; |
| |
| match = commentedPropertyRegex.exec(lineContent); |
| } |
| } |
| } |
| |
| // Look for colors and make swatches. |
| this._createColorSwatches(true); |
| |
| this._markLinesWithCheckboxPlaceholder(); |
| } |
| |
| if (nonatomic) |
| update.call(this); |
| else |
| this._codeMirror.operation(update.bind(this)); |
| }, |
| |
| _createColorSwatches: function(nonatomic, lineNumber) |
| { |
| function update() |
| { |
| // Look for color strings and add swatches in front of them. |
| this._codeMirror.createColorMarkers(lineNumber, function(marker, color, colorString) { |
| var swatchElement = document.createElement("span"); |
| swatchElement.title = WebInspector.UIString("Click to open a colorpicker. Shift-click to change color format."); |
| swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName; |
| swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this)); |
| |
| var swatchInnerElement = document.createElement("span"); |
| swatchInnerElement.style.backgroundColor = colorString; |
| swatchElement.appendChild(swatchInnerElement); |
| |
| var codeMirrorTextMarker = marker.codeMirrorTextMarker; |
| var swatchMarker = this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement); |
| |
| swatchInnerElement.__colorTextMarker = codeMirrorTextMarker; |
| swatchInnerElement.__color = color; |
| }.bind(this)); |
| } |
| |
| if (nonatomic) |
| update.call(this); |
| else |
| this._codeMirror.operation(update.bind(this)); |
| }, |
| |
| _updateTextMarkerForPropertyIfNeeded: function(property) |
| { |
| var textMarker = property.__propertyTextMarker; |
| console.assert(textMarker); |
| if (!textMarker) |
| return; |
| |
| var range = textMarker.find(); |
| console.assert(range); |
| if (!range) |
| return; |
| |
| this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property); |
| }, |
| |
| _createTextMarkerForPropertyIfNeeded: function(from, to, property) |
| { |
| if (!this._codeMirror.getOption("readOnly")) { |
| // Create a new checkbox element and marker. |
| |
| console.assert(property.enabled); |
| |
| var checkboxElement = document.createElement("input"); |
| checkboxElement.type = "checkbox"; |
| checkboxElement.checked = true; |
| checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this)); |
| checkboxElement.__cssProperty = property; |
| |
| var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement); |
| checkboxMarker.__propertyCheckbox = true; |
| } |
| |
| var classNames = ["css-style-declaration-property"]; |
| |
| if (property.overridden) |
| classNames.push("overridden"); |
| |
| if (property.implicit) |
| classNames.push("implicit"); |
| |
| if (this._style.inherited && !property.inherited) |
| classNames.push("not-inherited"); |
| |
| if (!property.valid && property.hasOtherVendorNameOrKeyword()) |
| classNames.push("other-vendor"); |
| else if (!property.valid) |
| classNames.push("invalid"); |
| |
| if (!property.enabled) |
| classNames.push("disabled"); |
| |
| var classNamesString = classNames.join(" "); |
| |
| // If there is already a text marker and it's in the same document, then try to avoid recreating it. |
| // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause |
| // both editors to fight and always recreate their text markers. This isn't really common. |
| if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) { |
| // If the class name is the same then we don't need to make a new marker. |
| if (property.__propertyTextMarker.className === classNamesString) |
| return; |
| |
| property.__propertyTextMarker.clear(); |
| } |
| |
| var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString}); |
| |
| propertyTextMarker.__cssProperty = property; |
| property.__propertyTextMarker = propertyTextMarker; |
| |
| property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this); |
| |
| this._removeCheckboxPlaceholder(from.line); |
| }, |
| |
| _clearTextMarkers: function(nonatomic, all) |
| { |
| function clear() |
| { |
| var markers = this._codeMirror.getAllMarks(); |
| for (var i = 0; i < markers.length; ++i) { |
| var textMarker = markers[i]; |
| |
| if (!all && textMarker.__checkboxPlaceholder) { |
| var position = textMarker.find(); |
| |
| // Only keep checkbox placeholders if they are in the first column. |
| if (position && !position.ch) |
| continue; |
| } |
| |
| if (textMarker.__cssProperty) { |
| textMarker.__cssProperty.removeEventListener(null, null, this); |
| |
| delete textMarker.__cssProperty.__propertyTextMarker; |
| delete textMarker.__cssProperty; |
| } |
| |
| textMarker.clear(); |
| } |
| } |
| |
| if (nonatomic) |
| clear.call(this); |
| else |
| this._codeMirror.operation(clear.bind(this)); |
| }, |
| |
| _iterateOverProperties: function(onlyVisibleProperties, callback) |
| { |
| var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties; |
| |
| if (!onlyVisibleProperties) { |
| // Filter based on options only when all properties are used. |
| properties = properties.filter((function(property) { |
| return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames; |
| }).bind(this)); |
| |
| if (this._sortProperties) |
| properties.sort(function(a, b) { return a.name.localeCompare(b.name) }); |
| } |
| |
| for (var i = 0; i < properties.length; ++i) { |
| if (callback.call(this, properties[i], i === properties.length - 1)) |
| break; |
| } |
| }, |
| |
| _propertyCheckboxChanged: function(event) |
| { |
| var property = event.target.__cssProperty; |
| console.assert(property); |
| if (!property) |
| return; |
| |
| var textMarker = property.__propertyTextMarker; |
| console.assert(textMarker); |
| if (!textMarker) |
| return; |
| |
| // Check if the property has been removed already, like from double-clicking |
| // the checkbox and calling this event listener multiple times. |
| var range = textMarker.find(); |
| if (!range) |
| return; |
| |
| var text = this._codeMirror.getRange(range.from, range.to); |
| |
| function update() |
| { |
| // Replace the text with a commented version. |
| this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to); |
| |
| // Update the line for any color swatches that got removed. |
| this._createColorSwatches(true, range.from.line); |
| } |
| |
| this._codeMirror.operation(update.bind(this)); |
| }, |
| |
| _propertyCommentCheckboxChanged: function(event) |
| { |
| var commentTextMarker = event.target.__commentTextMarker; |
| console.assert(commentTextMarker); |
| if (!commentTextMarker) |
| return; |
| |
| // Check if the comment has been removed already, like from double-clicking |
| // the checkbox and calling event listener multiple times. |
| var range = commentTextMarker.find(); |
| if (!range) |
| return; |
| |
| var text = this._codeMirror.getRange(range.from, range.to); |
| |
| // Remove the comment prefix and suffix. |
| text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, ""); |
| |
| // Add a semicolon if there isn't one already. |
| if (text.length && text.charAt(text.length - 1) !== ";") |
| text += ";"; |
| |
| function update() |
| { |
| this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); |
| this._codeMirror.replaceRange(text, range.from, range.to); |
| |
| // Update the line for any color swatches that got removed. |
| this._createColorSwatches(true, range.from.line); |
| } |
| |
| this._codeMirror.operation(update.bind(this)); |
| }, |
| |
| _colorSwatchClicked: function(event) |
| { |
| if (this._colorPickerPopover) |
| return; |
| |
| var swatch = event.target; |
| |
| var color = swatch.__color; |
| console.assert(color); |
| if (!color) |
| return; |
| |
| var colorTextMarker = swatch.__colorTextMarker; |
| console.assert(colorTextMarker); |
| if (!colorTextMarker) |
| return; |
| |
| var range = colorTextMarker.find(); |
| console.assert(range); |
| if (!range) |
| return; |
| |
| function updateCodeMirror(newColorText) |
| { |
| function update() |
| { |
| // The original text marker might have been cleared by a style update, |
| // in this case we need to find the new color text marker so we know |
| // the right range for the new style color text. |
| if (!colorTextMarker || !colorTextMarker.find()) { |
| colorTextMarker = null; |
| |
| var marks = this._codeMirror.findMarksAt(range.from); |
| if (!marks.length) |
| return; |
| |
| for (var i = 0; i < marks.length; ++i) { |
| var mark = marks[i]; |
| if (!mark.__markedColor) |
| continue; |
| colorTextMarker = mark; |
| break; |
| } |
| } |
| |
| if (!colorTextMarker) |
| return; |
| |
| // Sometimes we still might find a stale text marker with findMarksAt. |
| var newRange = colorTextMarker.find(); |
| if (!newRange) |
| return; |
| |
| range = newRange; |
| |
| colorTextMarker.clear(); |
| |
| this._codeMirror.replaceRange(newColorText, range.from, range.to); |
| |
| // The color's text format could have changed, so we need to update the "range" |
| // variable to anticipate a different "range.to" property. |
| range.to.ch = range.from.ch + newColorText.length; |
| |
| colorTextMarker = this._codeMirror.markText(range.from, range.to); |
| colorTextMarker.__markedColor = true; |
| |
| swatch.__colorTextMarker = colorTextMarker; |
| } |
| |
| this._codeMirror.operation(update.bind(this)); |
| } |
| |
| if (event.shiftKey) { |
| var nextFormat = color.nextFormat(); |
| console.assert(nextFormat); |
| if (!nextFormat) |
| return; |
| color.format = nextFormat; |
| |
| var newColorText = color.toString(); |
| |
| // Ignore the change so we don't commit the format change. However, any future user |
| // edits will commit the color format. |
| this._ignoreCodeMirrorContentDidChangeEvent = true; |
| updateCodeMirror.call(this, newColorText); |
| delete this._ignoreCodeMirrorContentDidChangeEvent; |
| } else { |
| this._colorPickerPopover = new WebInspector.Popover(this); |
| |
| var colorPicker = new WebInspector.ColorPicker; |
| |
| colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) { |
| updateCodeMirror.call(this, event.data.color.toString()); |
| }.bind(this)); |
| |
| var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect()); |
| |
| this._colorPickerPopover.content = colorPicker.element; |
| this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]); |
| |
| colorPicker.color = color; |
| } |
| }, |
| |
| _propertyOverriddenStatusChanged: function(event) |
| { |
| this._updateTextMarkerForPropertyIfNeeded(event.target); |
| }, |
| |
| _propertiesChanged: function(event) |
| { |
| // Don't try to update the document while completions are showing. Doing so will clear |
| // the completion hint and prevent further interaction with the completion. |
| if (this._completionController.isShowingCompletions()) |
| return; |
| |
| // Reset the content if the text is different and we are not focused. |
| if (!this.focused && this._style.text !== this._formattedContent()) { |
| this._resetContent(); |
| return; |
| } |
| |
| this._removeEditingLineClassesSoon(); |
| |
| this._updateTextMarkers(); |
| }, |
| |
| _markLinesWithCheckboxPlaceholder: function() |
| { |
| if (this._codeMirror.getOption("readOnly")) |
| return; |
| |
| var linesWithPropertyCheckboxes = {}; |
| var linesWithCheckboxPlaceholders = {}; |
| |
| var markers = this._codeMirror.getAllMarks(); |
| for (var i = 0; i < markers.length; ++i) { |
| var textMarker = markers[i]; |
| if (textMarker.__propertyCheckbox) { |
| var position = textMarker.find(); |
| if (position) |
| linesWithPropertyCheckboxes[position.line] = true; |
| } else if (textMarker.__checkboxPlaceholder) { |
| var position = textMarker.find(); |
| if (position) |
| linesWithCheckboxPlaceholders[position.line] = true; |
| } |
| } |
| |
| var lineCount = this._codeMirror.lineCount(); |
| |
| for (var i = 0; i < lineCount; ++i) { |
| if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders) |
| continue; |
| |
| var position = {line: i, ch: 0}; |
| |
| var placeholderElement = document.createElement("div"); |
| placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName; |
| |
| var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement); |
| placeholderMark.__checkboxPlaceholder = true; |
| } |
| }, |
| |
| _removeCheckboxPlaceholder: function(lineNumber) |
| { |
| var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0}); |
| for (var i = 0; i < marks.length; ++i) { |
| var mark = marks[i]; |
| if (!mark.__checkboxPlaceholder) |
| continue; |
| |
| mark.clear(); |
| return; |
| } |
| }, |
| |
| _resetContent: function() |
| { |
| if (this._commitChangesTimeout) { |
| clearTimeout(this._commitChangesTimeout); |
| delete this._commitChangesTimeout; |
| } |
| |
| this._removeEditingLineClasses(); |
| |
| // Only allow editing if we have a style, it is editable and we have text range in the stylesheet. |
| var readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange; |
| this._codeMirror.setOption("readOnly", readOnly); |
| |
| if (readOnly) { |
| this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName); |
| this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties")); |
| } else { |
| this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName); |
| this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit")); |
| } |
| |
| if (!this._style) { |
| this._ignoreCodeMirrorContentDidChangeEvent = true; |
| |
| this._clearTextMarkers(false, true); |
| |
| this._codeMirror.setValue(""); |
| this._codeMirror.clearHistory(); |
| this._codeMirror.markClean(); |
| |
| delete this._ignoreCodeMirrorContentDidChangeEvent; |
| |
| return; |
| } |
| |
| function update() |
| { |
| // Remember the cursor position/selection. |
| var selectionAnchor = this._codeMirror.getCursor("anchor"); |
| var selectionHead = this._codeMirror.getCursor("head"); |
| |
| function countNewLineCharacters(text) |
| { |
| var matches = text.match(/\n/g); |
| return matches ? matches.length : 0; |
| } |
| |
| var styleText = this._style.text; |
| |
| // Pretty print the content if there are more properties than there are lines. |
| // This could be an option exposed to the user; however, it is almost always |
| // desired in this case. |
| |
| if (styleText && this._style.visibleProperties.length <= countNewLineCharacters(styleText.trim()) + 1) { |
| // This style has formatted text content, so use it for a high-fidelity experience. |
| |
| var prefixWhitespaceMatch = styleText.match(/^[ \t]*\n/); |
| this._prefixWhitespace = prefixWhitespaceMatch ? prefixWhitespaceMatch[0] : ""; |
| |
| var suffixWhitespaceMatch = styleText.match(/\n[ \t]*$/); |
| this._suffixWhitespace = suffixWhitespaceMatch ? suffixWhitespaceMatch[0] : ""; |
| |
| this._codeMirror.setValue(styleText); |
| |
| if (this._prefixWhitespace) |
| this._codeMirror.removeLine(0); |
| |
| if (this._suffixWhitespace) { |
| var lineCount = this._codeMirror.lineCount(); |
| this._codeMirror.replaceRange("", {line: lineCount - 2}, {line: lineCount - 1}); |
| } |
| |
| this._linePrefixWhitespace = ""; |
| |
| var linesToStrip = []; |
| |
| // Remember the whitespace so it can be restored on commit. |
| var lineCount = this._codeMirror.lineCount(); |
| for (var i = 0; i < lineCount; ++i) { |
| var lineContent = this._codeMirror.getLine(i); |
| |
| var prefixWhitespaceMatch = lineContent.match(/^\s+/); |
| if (!prefixWhitespaceMatch) |
| continue; |
| |
| linesToStrip.push(i); |
| |
| // Only remember the shortest whitespace so we don't loose any of the |
| // original author's whitespace if their indentation lengths differed. |
| // Using the shortest also makes the adjustment work in _updateTextMarkers. |
| |
| // FIXME: This messes up if there is a mix of spaces and tabs. One tab |
| // will be shorter than 4 or 8 spaces, but will look the same visually. |
| if (!this._linePrefixWhitespace || prefixWhitespaceMatch[0].length < this._linePrefixWhitespace.length) |
| this._linePrefixWhitespace = prefixWhitespaceMatch[0]; |
| } |
| |
| // Strip the whitespace from the beginning of each line. |
| for (var i = 0; i < linesToStrip.length; ++i) { |
| var lineNumber = linesToStrip[i]; |
| var from = {line: lineNumber, ch: 0}; |
| var to = {line: lineNumber, ch: this._linePrefixWhitespace.length}; |
| this._codeMirror.replaceRange("", from, to); |
| } |
| |
| // Update all the text markers. |
| this._updateTextMarkers(true); |
| } else { |
| // This style does not have text content or it is minified, so we want to synthesize the text content. |
| |
| this._prefixWhitespace = ""; |
| this._suffixWhitespace = ""; |
| this._linePrefixWhitespace = ""; |
| |
| this._codeMirror.setValue(""); |
| |
| var lineNumber = 0; |
| |
| // Iterate only visible properties if we have original style text. That way we known we only syntesize |
| // what was originaly in the style text. |
| this._iterateOverProperties(styleText ? true : false, function(property) { |
| // Some property text can have line breaks, so consider that in the ranges below. |
| var propertyText = property.synthesizedText; |
| var propertyLineCount = countNewLineCharacters(propertyText); |
| |
| var from = {line: lineNumber, ch: 0}; |
| var to = {line: lineNumber + propertyLineCount}; |
| |
| this._codeMirror.replaceRange((lineNumber ? "\n" : "") + propertyText, from); |
| this._createTextMarkerForPropertyIfNeeded(from, to, property); |
| |
| lineNumber += propertyLineCount + 1; |
| }); |
| |
| // Look for colors and make swatches. |
| this._createColorSwatches(true); |
| } |
| |
| this._markLinesWithCheckboxPlaceholder(); |
| |
| // Restore the cursor position/selection. |
| this._codeMirror.setSelection(selectionAnchor, selectionHead); |
| |
| // Reset undo history since undo past the reset is wrong when the content was empty before |
| // or the content was representing a previous style object. |
| this._codeMirror.clearHistory(); |
| |
| // Mark the editor as clean (unedited state). |
| this._codeMirror.markClean(); |
| } |
| |
| // This needs to be done first and as a separate operation to avoid an exception in CodeMirror. |
| this._clearTextMarkers(false, true); |
| |
| this._ignoreCodeMirrorContentDidChangeEvent = true; |
| this._codeMirror.operation(update.bind(this)); |
| delete this._ignoreCodeMirrorContentDidChangeEvent; |
| }, |
| |
| _updateJumpToSymbolTrackingMode: function() |
| { |
| var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled; |
| |
| if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation) |
| this._jumpToSymbolTrackingModeEnabled = false; |
| else |
| this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.shiftKey; |
| |
| if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) { |
| if (this._jumpToSymbolTrackingModeEnabled) { |
| this._tokenTrackingController.highlightLastHoveredRange(); |
| this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly"); |
| } else { |
| this._tokenTrackingController.removeHighlightedRange(); |
| this._tokenTrackingController.enabled = false; |
| } |
| } |
| }, |
| |
| tokenTrackingControllerHighlightedRangeWasClicked: function(tokenTrackingController) |
| { |
| console.assert(this._style.ownerRule.sourceCodeLocation); |
| if (!this._style.ownerRule.sourceCodeLocation) |
| return; |
| |
| // Special case command clicking url(...) links. |
| var token = this._tokenTrackingController.candidate.hoveredToken; |
| if (/\blink\b/.test(token.type)) { |
| var url = token.string; |
| var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url; |
| WebInspector.openURL(absoluteURL(url, baseURL)); |
| return; |
| } |
| |
| // Jump to the rule if we can't find a property. |
| // Find a better source code location from the property that was clicked. |
| var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation; |
| var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start); |
| for (var i = 0; i < marks.length; ++i) { |
| var mark = marks[i]; |
| var property = mark.__cssProperty; |
| if (property) { |
| var sourceCode = sourceCodeLocation.sourceCode; |
| var styleSheetTextRange = property.styleSheetTextRange; |
| sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn); |
| } |
| } |
| |
| WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation); |
| }, |
| |
| tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate) |
| { |
| this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange); |
| } |
| }; |
| |
| WebInspector.CSSStyleDeclarationTextEditor.prototype.__proto__ = WebInspector.Object.prototype; |