1/* 2 * Copyright (C) 2013 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.CSSStyleDeclarationTextEditor = function(delegate, style, element) 27{ 28 WebInspector.Object.call(this); 29 30 this._element = element || document.createElement("div"); 31 this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName); 32 this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName); 33 34 this._showsImplicitProperties = true; 35 this._alwaysShowPropertyNames = {}; 36 this._sortProperties = false; 37 38 this._prefixWhitespace = ""; 39 this._suffixWhitespace = ""; 40 this._linePrefixWhitespace = ""; 41 42 this._delegate = delegate || null; 43 44 this._codeMirror = CodeMirror(this.element, { 45 readOnly: true, 46 lineWrapping: true, 47 mode: "css-rule", 48 electricChars: false, 49 indentWithTabs: true, 50 indentUnit: 4, 51 smartIndent: false, 52 matchBrackets: true, 53 autoCloseBrackets: true 54 }); 55 56 this._codeMirror.on("change", this._contentChanged.bind(this)); 57 this._codeMirror.on("blur", this._editorBlured.bind(this)); 58 59 this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this); 60 this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this); 61 62 this._jumpToSymbolTrackingModeEnabled = false; 63 this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName; 64 this._tokenTrackingController.mouseOverDelayDuration = 0; 65 this._tokenTrackingController.mouseOutReleaseDelayDuration = 0; 66 this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens; 67 68 this.style = style; 69}; 70 71WebInspector.Object.addConstructorFunctions(WebInspector.CSSStyleDeclarationTextEditor); 72 73WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor"; 74WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only"; 75WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch"; 76WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder"; 77WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line"; 78WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250; 79WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000; 80 81WebInspector.CSSStyleDeclarationTextEditor.prototype = { 82 constructor: WebInspector.CSSStyleDeclarationTextEditor, 83 84 // Public 85 86 get element() 87 { 88 return this._element; 89 }, 90 91 get delegate() 92 { 93 return this._delegate; 94 }, 95 96 set delegate(delegate) 97 { 98 this._delegate = delegate || null; 99 }, 100 101 get style() 102 { 103 return this._style; 104 }, 105 106 set style(style) 107 { 108 if (this._style === style) 109 return; 110 111 if (this._style) { 112 this._style.removeEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); 113 if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation) 114 WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this); 115 } 116 117 this._style = style || null; 118 119 if (this._style) { 120 this._style.addEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); 121 if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation) 122 WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this); 123 } 124 125 this._updateJumpToSymbolTrackingMode(); 126 127 this._resetContent(); 128 }, 129 130 get focused() 131 { 132 return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused"); 133 }, 134 135 get alwaysShowPropertyNames() 136 { 137 return Object.keys(this._alwaysShowPropertyNames); 138 }, 139 140 set alwaysShowPropertyNames(alwaysShowPropertyNames) 141 { 142 this._alwaysShowPropertyNames = (alwaysShowPropertyNames || []).keySet(); 143 144 this._resetContent(); 145 }, 146 147 get showsImplicitProperties() 148 { 149 return this._showsImplicitProperties; 150 }, 151 152 set showsImplicitProperties(showsImplicitProperties) 153 { 154 if (this._showsImplicitProperties === showsImplicitProperties) 155 return; 156 157 this._showsImplicitProperties = showsImplicitProperties; 158 159 this._resetContent(); 160 }, 161 162 get sortProperties() 163 { 164 return this._sortProperties; 165 }, 166 167 set sortProperties(sortProperties) 168 { 169 if (this._sortProperties === sortProperties) 170 return; 171 172 this._sortProperties = sortProperties; 173 174 this._resetContent(); 175 }, 176 177 focus: function() 178 { 179 this._codeMirror.focus(); 180 }, 181 182 refresh: function() 183 { 184 this._resetContent(); 185 }, 186 187 updateLayout: function(force) 188 { 189 this._codeMirror.refresh(); 190 }, 191 192 // Protected 193 194 didDismissPopover: function(popover) 195 { 196 if (popover === this._colorPickerPopover) 197 delete this._colorPickerPopover; 198 }, 199 200 completionControllerCompletionsHidden: function(completionController) 201 { 202 var styleText = this._style.text; 203 var currentText = this._formattedContent(); 204 205 // If the style text and the current editor text differ then we need to commit. 206 // Otherwise we can just update the properties that got skipped because a completion 207 // was pending the last time _propertiesChanged was called. 208 if (styleText !== currentText) 209 this._commitChanges(); 210 else 211 this._propertiesChanged(); 212 }, 213 214 // Private 215 216 _clearRemoveEditingLineClassesTimeout: function() 217 { 218 if (!this._removeEditingLineClassesTimeout) 219 return; 220 221 clearTimeout(this._removeEditingLineClassesTimeout); 222 delete this._removeEditingLineClassesTimeout; 223 }, 224 225 _removeEditingLineClasses: function() 226 { 227 this._clearRemoveEditingLineClassesTimeout(); 228 229 function removeEditingLineClasses() 230 { 231 var lineCount = this._codeMirror.lineCount(); 232 for (var i = 0; i < lineCount; ++i) 233 this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); 234 } 235 236 this._codeMirror.operation(removeEditingLineClasses.bind(this)); 237 }, 238 239 _removeEditingLineClassesSoon: function() 240 { 241 if (this._removeEditingLineClassesTimeout) 242 return; 243 this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay); 244 }, 245 246 _formattedContent: function() 247 { 248 // Start with the prefix whitespace we stripped. 249 var content = this._prefixWhitespace; 250 251 // Get each line and add the line prefix whitespace and newlines. 252 var lineCount = this._codeMirror.lineCount(); 253 for (var i = 0; i < lineCount; ++i) { 254 var lineContent = this._codeMirror.getLine(i); 255 content += this._linePrefixWhitespace + lineContent; 256 if (i !== lineCount - 1) 257 content += "\n"; 258 } 259 260 // Add the suffix whitespace we stripped. 261 content += this._suffixWhitespace; 262 263 return content; 264 }, 265 266 _commitChanges: function() 267 { 268 if (this._commitChangesTimeout) { 269 clearTimeout(this._commitChangesTimeout); 270 delete this._commitChangesTimeout; 271 } 272 273 this._style.text = this._formattedContent(); 274 }, 275 276 _editorBlured: function(codeMirror) 277 { 278 // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case. 279 if (this._completionController.isHandlingClickEvent()) 280 return; 281 282 // Reset the content on blur since we stop accepting external changes while the the editor is focused. 283 // This causes us to pick up any change that was suppressed while the editor was focused. 284 this._resetContent(); 285 }, 286 287 _contentChanged: function(codeMirror, change) 288 { 289 // Return early if the style isn't editable. This still can be called when readOnly is set because 290 // clicking on a color swatch modifies the text. 291 if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent) 292 return; 293 294 this._markLinesWithCheckboxPlaceholder(); 295 296 this._clearRemoveEditingLineClassesTimeout(); 297 this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); 298 299 // When the change is a completion change, create color swatches now since the changes 300 // will not go through _propertiesChanged until completionControllerCompletionsHidden happens. 301 // This way any auto completed colors get swatches right away. 302 if (this._completionController.isCompletionChange(change)) 303 this._createColorSwatches(false, change.from.line); 304 305 // Use a short delay for user input to coalesce more changes before committing. Other actions like 306 // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that 307 // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing. 308 const delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0; 309 310 // Reset the timeout so rapid changes coalesce after a short delay. 311 if (this._commitChangesTimeout) 312 clearTimeout(this._commitChangesTimeout); 313 this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay); 314 }, 315 316 _updateTextMarkers: function(nonatomic) 317 { 318 function update() 319 { 320 this._clearTextMarkers(true); 321 322 var styleText = this._style.text; 323 324 this._iterateOverProperties(true, function(property) { 325 var styleTextRange = property.styleDeclarationTextRange; 326 console.assert(styleTextRange); 327 if (!styleTextRange) 328 return; 329 330 var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn}; 331 var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn}; 332 333 // Adjust the line position for the missing prefix line. 334 if (this._prefixWhitespace) { 335 --from.line; 336 --to.line; 337 } 338 339 // Adjust the column for the stripped line prefix whitespace. 340 from.ch -= this._linePrefixWhitespace.length; 341 to.ch -= this._linePrefixWhitespace.length; 342 343 this._createTextMarkerForPropertyIfNeeded(from, to, property); 344 }); 345 346 if (!this._codeMirror.getOption("readOnly")) { 347 // Matches a comment like: /* -webkit-foo: bar; */ 348 const commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g; 349 350 // Look for comments that look like properties and add checkboxes in front of them. 351 var lineCount = this._codeMirror.lineCount(); 352 for (var i = 0; i < lineCount; ++i) { 353 var lineContent = this._codeMirror.getLine(i); 354 355 var match = commentedPropertyRegex.exec(lineContent); 356 while (match) { 357 var checkboxElement = document.createElement("input"); 358 checkboxElement.type = "checkbox"; 359 checkboxElement.checked = false; 360 checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this)); 361 362 var from = {line: i, ch: match.index}; 363 var to = {line: i, ch: match.index + match[0].length}; 364 365 var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement); 366 checkboxMarker.__propertyCheckbox = true; 367 368 var commentTextMarker = this._codeMirror.markText(from, to); 369 370 checkboxElement.__commentTextMarker = commentTextMarker; 371 372 match = commentedPropertyRegex.exec(lineContent); 373 } 374 } 375 } 376 377 // Look for colors and make swatches. 378 this._createColorSwatches(true); 379 380 this._markLinesWithCheckboxPlaceholder(); 381 } 382 383 if (nonatomic) 384 update.call(this); 385 else 386 this._codeMirror.operation(update.bind(this)); 387 }, 388 389 _createColorSwatches: function(nonatomic, lineNumber) 390 { 391 function update() 392 { 393 var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null; 394 395 // Look for color strings and add swatches in front of them. 396 this._codeMirror.createColorMarkers(range, function(marker, color, colorString) { 397 var swatchElement = document.createElement("span"); 398 swatchElement.title = WebInspector.UIString("Click to open a colorpicker. Shift-click to change color format."); 399 swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName; 400 swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this)); 401 402 var swatchInnerElement = document.createElement("span"); 403 swatchInnerElement.style.backgroundColor = colorString; 404 swatchElement.appendChild(swatchInnerElement); 405 406 var codeMirrorTextMarker = marker.codeMirrorTextMarker; 407 var swatchMarker = this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement); 408 409 swatchInnerElement.__colorTextMarker = codeMirrorTextMarker; 410 swatchInnerElement.__color = color; 411 }.bind(this)); 412 } 413 414 if (nonatomic) 415 update.call(this); 416 else 417 this._codeMirror.operation(update.bind(this)); 418 }, 419 420 _updateTextMarkerForPropertyIfNeeded: function(property) 421 { 422 var textMarker = property.__propertyTextMarker; 423 console.assert(textMarker); 424 if (!textMarker) 425 return; 426 427 var range = textMarker.find(); 428 console.assert(range); 429 if (!range) 430 return; 431 432 this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property); 433 }, 434 435 _createTextMarkerForPropertyIfNeeded: function(from, to, property) 436 { 437 if (!this._codeMirror.getOption("readOnly")) { 438 // Create a new checkbox element and marker. 439 440 console.assert(property.enabled); 441 442 var checkboxElement = document.createElement("input"); 443 checkboxElement.type = "checkbox"; 444 checkboxElement.checked = true; 445 checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this)); 446 checkboxElement.__cssProperty = property; 447 448 var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement); 449 checkboxMarker.__propertyCheckbox = true; 450 } 451 452 var classNames = ["css-style-declaration-property"]; 453 454 if (property.overridden) 455 classNames.push("overridden"); 456 457 if (property.implicit) 458 classNames.push("implicit"); 459 460 if (this._style.inherited && !property.inherited) 461 classNames.push("not-inherited"); 462 463 if (!property.valid && property.hasOtherVendorNameOrKeyword()) 464 classNames.push("other-vendor"); 465 else if (!property.valid) 466 classNames.push("invalid"); 467 468 if (!property.enabled) 469 classNames.push("disabled"); 470 471 var classNamesString = classNames.join(" "); 472 473 // If there is already a text marker and it's in the same document, then try to avoid recreating it. 474 // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause 475 // both editors to fight and always recreate their text markers. This isn't really common. 476 if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) { 477 // If the class name is the same then we don't need to make a new marker. 478 if (property.__propertyTextMarker.className === classNamesString) 479 return; 480 481 property.__propertyTextMarker.clear(); 482 } 483 484 var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString}); 485 486 propertyTextMarker.__cssProperty = property; 487 property.__propertyTextMarker = propertyTextMarker; 488 489 property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this); 490 491 this._removeCheckboxPlaceholder(from.line); 492 }, 493 494 _clearTextMarkers: function(nonatomic, all) 495 { 496 function clear() 497 { 498 var markers = this._codeMirror.getAllMarks(); 499 for (var i = 0; i < markers.length; ++i) { 500 var textMarker = markers[i]; 501 502 if (!all && textMarker.__checkboxPlaceholder) { 503 var position = textMarker.find(); 504 505 // Only keep checkbox placeholders if they are in the first column. 506 if (position && !position.ch) 507 continue; 508 } 509 510 if (textMarker.__cssProperty) { 511 textMarker.__cssProperty.removeEventListener(null, null, this); 512 513 delete textMarker.__cssProperty.__propertyTextMarker; 514 delete textMarker.__cssProperty; 515 } 516 517 textMarker.clear(); 518 } 519 } 520 521 if (nonatomic) 522 clear.call(this); 523 else 524 this._codeMirror.operation(clear.bind(this)); 525 }, 526 527 _iterateOverProperties: function(onlyVisibleProperties, callback) 528 { 529 var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties; 530 531 if (!onlyVisibleProperties) { 532 // Filter based on options only when all properties are used. 533 properties = properties.filter((function(property) { 534 return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames; 535 }).bind(this)); 536 537 if (this._sortProperties) 538 properties.sort(function(a, b) { return a.name.localeCompare(b.name) }); 539 } 540 541 for (var i = 0; i < properties.length; ++i) { 542 if (callback.call(this, properties[i], i === properties.length - 1)) 543 break; 544 } 545 }, 546 547 _propertyCheckboxChanged: function(event) 548 { 549 var property = event.target.__cssProperty; 550 console.assert(property); 551 if (!property) 552 return; 553 554 var textMarker = property.__propertyTextMarker; 555 console.assert(textMarker); 556 if (!textMarker) 557 return; 558 559 // Check if the property has been removed already, like from double-clicking 560 // the checkbox and calling this event listener multiple times. 561 var range = textMarker.find(); 562 if (!range) 563 return; 564 565 var text = this._codeMirror.getRange(range.from, range.to); 566 567 function update() 568 { 569 // Replace the text with a commented version. 570 this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to); 571 572 // Update the line for any color swatches that got removed. 573 this._createColorSwatches(true, range.from.line); 574 } 575 576 this._codeMirror.operation(update.bind(this)); 577 }, 578 579 _propertyCommentCheckboxChanged: function(event) 580 { 581 var commentTextMarker = event.target.__commentTextMarker; 582 console.assert(commentTextMarker); 583 if (!commentTextMarker) 584 return; 585 586 // Check if the comment has been removed already, like from double-clicking 587 // the checkbox and calling event listener multiple times. 588 var range = commentTextMarker.find(); 589 if (!range) 590 return; 591 592 var text = this._codeMirror.getRange(range.from, range.to); 593 594 // Remove the comment prefix and suffix. 595 text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, ""); 596 597 // Add a semicolon if there isn't one already. 598 if (text.length && text.charAt(text.length - 1) !== ";") 599 text += ";"; 600 601 function update() 602 { 603 this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName); 604 this._codeMirror.replaceRange(text, range.from, range.to); 605 606 // Update the line for any color swatches that got removed. 607 this._createColorSwatches(true, range.from.line); 608 } 609 610 this._codeMirror.operation(update.bind(this)); 611 }, 612 613 _colorSwatchClicked: function(event) 614 { 615 if (this._colorPickerPopover) 616 return; 617 618 var swatch = event.target; 619 620 var color = swatch.__color; 621 console.assert(color); 622 if (!color) 623 return; 624 625 var colorTextMarker = swatch.__colorTextMarker; 626 console.assert(colorTextMarker); 627 if (!colorTextMarker) 628 return; 629 630 var range = colorTextMarker.find(); 631 console.assert(range); 632 if (!range) 633 return; 634 635 function updateCodeMirror(newColorText) 636 { 637 function update() 638 { 639 // The original text marker might have been cleared by a style update, 640 // in this case we need to find the new color text marker so we know 641 // the right range for the new style color text. 642 if (!colorTextMarker || !colorTextMarker.find()) { 643 colorTextMarker = null; 644 645 var marks = this._codeMirror.findMarksAt(range.from); 646 if (!marks.length) 647 return; 648 649 for (var i = 0; i < marks.length; ++i) { 650 var mark = marks[i]; 651 if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.Color) 652 continue; 653 colorTextMarker = mark; 654 break; 655 } 656 } 657 658 if (!colorTextMarker) 659 return; 660 661 // Sometimes we still might find a stale text marker with findMarksAt. 662 var newRange = colorTextMarker.find(); 663 if (!newRange) 664 return; 665 666 range = newRange; 667 668 colorTextMarker.clear(); 669 670 this._codeMirror.replaceRange(newColorText, range.from, range.to); 671 672 // The color's text format could have changed, so we need to update the "range" 673 // variable to anticipate a different "range.to" property. 674 range.to.ch = range.from.ch + newColorText.length; 675 676 colorTextMarker = this._codeMirror.markText(range.from, range.to); 677 678 swatch.__colorTextMarker = colorTextMarker; 679 } 680 681 this._codeMirror.operation(update.bind(this)); 682 } 683 684 if (event.shiftKey || this._codeMirror.getOption("readOnly")) { 685 var nextFormat = color.nextFormat(); 686 console.assert(nextFormat); 687 if (!nextFormat) 688 return; 689 color.format = nextFormat; 690 691 var newColorText = color.toString(); 692 693 // Ignore the change so we don't commit the format change. However, any future user 694 // edits will commit the color format. 695 this._ignoreCodeMirrorContentDidChangeEvent = true; 696 updateCodeMirror.call(this, newColorText); 697 delete this._ignoreCodeMirrorContentDidChangeEvent; 698 } else { 699 this._colorPickerPopover = new WebInspector.Popover(this); 700 701 var colorPicker = new WebInspector.ColorPicker; 702 703 colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) { 704 updateCodeMirror.call(this, event.data.color.toString()); 705 }.bind(this)); 706 707 var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect()); 708 709 this._colorPickerPopover.content = colorPicker.element; 710 this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]); 711 712 colorPicker.color = color; 713 } 714 }, 715 716 _propertyOverriddenStatusChanged: function(event) 717 { 718 this._updateTextMarkerForPropertyIfNeeded(event.target); 719 }, 720 721 _propertiesChanged: function(event) 722 { 723 // Don't try to update the document while completions are showing. Doing so will clear 724 // the completion hint and prevent further interaction with the completion. 725 if (this._completionController.isShowingCompletions()) 726 return; 727 728 // Reset the content if the text is different and we are not focused. 729 if (!this.focused && this._style.text !== this._formattedContent()) { 730 this._resetContent(); 731 return; 732 } 733 734 this._removeEditingLineClassesSoon(); 735 736 this._updateTextMarkers(); 737 }, 738 739 _markLinesWithCheckboxPlaceholder: function() 740 { 741 if (this._codeMirror.getOption("readOnly")) 742 return; 743 744 var linesWithPropertyCheckboxes = {}; 745 var linesWithCheckboxPlaceholders = {}; 746 747 var markers = this._codeMirror.getAllMarks(); 748 for (var i = 0; i < markers.length; ++i) { 749 var textMarker = markers[i]; 750 if (textMarker.__propertyCheckbox) { 751 var position = textMarker.find(); 752 if (position) 753 linesWithPropertyCheckboxes[position.line] = true; 754 } else if (textMarker.__checkboxPlaceholder) { 755 var position = textMarker.find(); 756 if (position) 757 linesWithCheckboxPlaceholders[position.line] = true; 758 } 759 } 760 761 var lineCount = this._codeMirror.lineCount(); 762 763 for (var i = 0; i < lineCount; ++i) { 764 if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders) 765 continue; 766 767 var position = {line: i, ch: 0}; 768 769 var placeholderElement = document.createElement("div"); 770 placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName; 771 772 var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement); 773 placeholderMark.__checkboxPlaceholder = true; 774 } 775 }, 776 777 _removeCheckboxPlaceholder: function(lineNumber) 778 { 779 var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0}); 780 for (var i = 0; i < marks.length; ++i) { 781 var mark = marks[i]; 782 if (!mark.__checkboxPlaceholder) 783 continue; 784 785 mark.clear(); 786 return; 787 } 788 }, 789 790 _resetContent: function() 791 { 792 if (this._commitChangesTimeout) { 793 clearTimeout(this._commitChangesTimeout); 794 delete this._commitChangesTimeout; 795 } 796 797 this._removeEditingLineClasses(); 798 799 // Only allow editing if we have a style, it is editable and we have text range in the stylesheet. 800 var readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange; 801 this._codeMirror.setOption("readOnly", readOnly); 802 803 if (readOnly) { 804 this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName); 805 this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties")); 806 } else { 807 this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName); 808 this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit")); 809 } 810 811 if (!this._style) { 812 this._ignoreCodeMirrorContentDidChangeEvent = true; 813 814 this._clearTextMarkers(false, true); 815 816 this._codeMirror.setValue(""); 817 this._codeMirror.clearHistory(); 818 this._codeMirror.markClean(); 819 820 delete this._ignoreCodeMirrorContentDidChangeEvent; 821 822 return; 823 } 824 825 function update() 826 { 827 // Remember the cursor position/selection. 828 var selectionAnchor = this._codeMirror.getCursor("anchor"); 829 var selectionHead = this._codeMirror.getCursor("head"); 830 831 function countNewLineCharacters(text) 832 { 833 var matches = text.match(/\n/g); 834 return matches ? matches.length : 0; 835 } 836 837 var styleText = this._style.text; 838 839 // Pretty print the content if there are more properties than there are lines. 840 // This could be an option exposed to the user; however, it is almost always 841 // desired in this case. 842 843 if (styleText && this._style.visibleProperties.length <= countNewLineCharacters(styleText.trim()) + 1) { 844 // This style has formatted text content, so use it for a high-fidelity experience. 845 846 var prefixWhitespaceMatch = styleText.match(/^[ \t]*\n/); 847 this._prefixWhitespace = prefixWhitespaceMatch ? prefixWhitespaceMatch[0] : ""; 848 849 var suffixWhitespaceMatch = styleText.match(/\n[ \t]*$/); 850 this._suffixWhitespace = suffixWhitespaceMatch ? suffixWhitespaceMatch[0] : ""; 851 852 this._codeMirror.setValue(styleText); 853 854 if (this._prefixWhitespace) 855 this._codeMirror.replaceRange("", {line: 0, ch: 0}, {line: 1, ch: 0}); 856 857 if (this._suffixWhitespace) { 858 var lineCount = this._codeMirror.lineCount(); 859 this._codeMirror.replaceRange("", {line: lineCount - 2}, {line: lineCount - 1}); 860 } 861 862 this._linePrefixWhitespace = ""; 863 864 var linesToStrip = []; 865 866 // Remember the whitespace so it can be restored on commit. 867 var lineCount = this._codeMirror.lineCount(); 868 for (var i = 0; i < lineCount; ++i) { 869 var lineContent = this._codeMirror.getLine(i); 870 var prefixWhitespaceMatch = lineContent.match(/^\s+/); 871 872 // If there is no prefix whitespace (except for empty lines) then the prefix 873 // whitespace of all other lines will be retained as is. Update markers and return. 874 if (!prefixWhitespaceMatch) { 875 if (!lineContent) 876 continue; 877 this._linePrefixWhitespace = ""; 878 this._updateTextMarkers(true); 879 return; 880 } 881 882 linesToStrip.push(i); 883 884 // Only remember the shortest whitespace so we don't loose any of the 885 // original author's whitespace if their indentation lengths differed. 886 // Using the shortest also makes the adjustment work in _updateTextMarkers. 887 888 // FIXME: This messes up if there is a mix of spaces and tabs. A tab 889 // is treated the same as a space when prefix whitespace is omitted, 890 // so if the shortest prefixed whitespace is, say, two tab characters, 891 // lines that begin with four spaces will only have a two space indent. 892 if (!this._linePrefixWhitespace || prefixWhitespaceMatch[0].length < this._linePrefixWhitespace.length) 893 this._linePrefixWhitespace = prefixWhitespaceMatch[0]; 894 } 895 896 // Strip the whitespace from the beginning of each line. 897 for (var i = 0; i < linesToStrip.length; ++i) { 898 var lineNumber = linesToStrip[i]; 899 var from = {line: lineNumber, ch: 0}; 900 var to = {line: lineNumber, ch: this._linePrefixWhitespace.length}; 901 this._codeMirror.replaceRange("", from, to); 902 } 903 904 // Update all the text markers. 905 this._updateTextMarkers(true); 906 } else { 907 // This style does not have text content or it is minified, so we want to synthesize the text content. 908 909 this._prefixWhitespace = ""; 910 this._suffixWhitespace = ""; 911 this._linePrefixWhitespace = ""; 912 913 this._codeMirror.setValue(""); 914 915 var lineNumber = 0; 916 917 // Iterate only visible properties if we have original style text. That way we known we only synthesize 918 // what was originaly in the style text. 919 this._iterateOverProperties(styleText ? true : false, function(property) { 920 // Some property text can have line breaks, so consider that in the ranges below. 921 var propertyText = property.synthesizedText; 922 var propertyLineCount = countNewLineCharacters(propertyText); 923 924 var from = {line: lineNumber, ch: 0}; 925 var to = {line: lineNumber + propertyLineCount}; 926 927 this._codeMirror.replaceRange((lineNumber ? "\n" : "") + propertyText, from); 928 this._createTextMarkerForPropertyIfNeeded(from, to, property); 929 930 lineNumber += propertyLineCount + 1; 931 }); 932 933 // Look for colors and make swatches. 934 this._createColorSwatches(true); 935 } 936 937 this._markLinesWithCheckboxPlaceholder(); 938 939 // Restore the cursor position/selection. 940 this._codeMirror.setSelection(selectionAnchor, selectionHead); 941 942 // Reset undo history since undo past the reset is wrong when the content was empty before 943 // or the content was representing a previous style object. 944 this._codeMirror.clearHistory(); 945 946 // Mark the editor as clean (unedited state). 947 this._codeMirror.markClean(); 948 } 949 950 // This needs to be done first and as a separate operation to avoid an exception in CodeMirror. 951 this._clearTextMarkers(false, true); 952 953 this._ignoreCodeMirrorContentDidChangeEvent = true; 954 this._codeMirror.operation(update.bind(this)); 955 delete this._ignoreCodeMirrorContentDidChangeEvent; 956 }, 957 958 _updateJumpToSymbolTrackingMode: function() 959 { 960 var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled; 961 962 if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation) 963 this._jumpToSymbolTrackingModeEnabled = false; 964 else 965 this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.shiftKey; 966 967 if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) { 968 if (this._jumpToSymbolTrackingModeEnabled) { 969 this._tokenTrackingController.highlightLastHoveredRange(); 970 this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly"); 971 } else { 972 this._tokenTrackingController.removeHighlightedRange(); 973 this._tokenTrackingController.enabled = false; 974 } 975 } 976 }, 977 978 tokenTrackingControllerHighlightedRangeWasClicked: function(tokenTrackingController) 979 { 980 console.assert(this._style.ownerRule.sourceCodeLocation); 981 if (!this._style.ownerRule.sourceCodeLocation) 982 return; 983 984 // Special case command clicking url(...) links. 985 var token = this._tokenTrackingController.candidate.hoveredToken; 986 if (/\blink\b/.test(token.type)) { 987 var url = token.string; 988 var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url; 989 WebInspector.openURL(absoluteURL(url, baseURL)); 990 return; 991 } 992 993 // Jump to the rule if we can't find a property. 994 // Find a better source code location from the property that was clicked. 995 var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation; 996 var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start); 997 for (var i = 0; i < marks.length; ++i) { 998 var mark = marks[i]; 999 var property = mark.__cssProperty; 1000 if (property) { 1001 var sourceCode = sourceCodeLocation.sourceCode; 1002 var styleSheetTextRange = property.styleSheetTextRange; 1003 sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn); 1004 } 1005 } 1006 1007 WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation); 1008 }, 1009 1010 tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate) 1011 { 1012 this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange); 1013 } 1014}; 1015 1016WebInspector.CSSStyleDeclarationTextEditor.prototype.__proto__ = WebInspector.Object.prototype; 1017