Web Inspector: Styles Redesign: support editing of rule selectors
authornvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 3 Oct 2017 19:17:23 +0000 (19:17 +0000)
committernvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 3 Oct 2017 19:17:23 +0000 (19:17 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177012

Reviewed by Matt Baker.

Clicking or focusing (by tabbing from another field) on a CSS selector should select the text and make the selector
field editable.

Keyboard behavior while editing:
- Enter should commit changes.
- Escape should discard changes.
- Tab should commit changes and navigate to the first property name.
- Shift-Tab should commit changes and navigate to the last rule's property value, if there's one.

* UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js:
(WI.SpreadsheetStyleProperty.prototype._update):
Add tabIndex so the keyboard navigation (Tab & Shift-Tab) to and from selectors works as expected.

* UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.css:
(.spreadsheet-css-declaration .selector:focus,):
(.spreadsheet-css-declaration .selector.spreadsheet-selector-field):
(.spreadsheet-css-declaration .selector.spreadsheet-selector-field.editing):
* UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js:
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.get selectorEditable):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.initialLayout):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.layout):
Split layout into _renderOrigin and _renderSelector, so selector field can be updated separately
from everything else.

(WI.SpreadsheetCSSStyleDeclarationSection.prototype.cssStyleDeclarationTextEditorFocused):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidChange):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidDiscard):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype._discardSelectorChange):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderSelector):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderOrigin):

(WI.SpreadsheetSelectorField):
(WI.SpreadsheetSelectorField.prototype.get editing):
(WI.SpreadsheetSelectorField.prototype.startEditing):
(WI.SpreadsheetSelectorField.prototype.stopEditing):
(WI.SpreadsheetSelectorField.prototype._handleClick):
(WI.SpreadsheetSelectorField.prototype._handleFocus):
(WI.SpreadsheetSelectorField.prototype._handleBlur):
(WI.SpreadsheetSelectorField.prototype._handleKeyDown):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@222799 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js
Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.css
Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js

index 0413076..ed5ec9a 100644 (file)
@@ -1,3 +1,50 @@
+2017-10-03  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Styles Redesign: support editing of rule selectors
+        https://bugs.webkit.org/show_bug.cgi?id=177012
+
+        Reviewed by Matt Baker.
+
+        Clicking or focusing (by tabbing from another field) on a CSS selector should select the text and make the selector
+        field editable.
+
+        Keyboard behavior while editing:
+        - Enter should commit changes.
+        - Escape should discard changes.
+        - Tab should commit changes and navigate to the first property name.
+        - Shift-Tab should commit changes and navigate to the last rule's property value, if there's one.
+
+        * UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js:
+        (WI.SpreadsheetStyleProperty.prototype._update):
+        Add tabIndex so the keyboard navigation (Tab & Shift-Tab) to and from selectors works as expected.
+
+        * UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.css:
+        (.spreadsheet-css-declaration .selector:focus,):
+        (.spreadsheet-css-declaration .selector.spreadsheet-selector-field):
+        (.spreadsheet-css-declaration .selector.spreadsheet-selector-field.editing):
+        * UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js:
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.get selectorEditable):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.initialLayout):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.layout):
+        Split layout into _renderOrigin and _renderSelector, so selector field can be updated separately
+        from everything else.
+
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.cssStyleDeclarationTextEditorFocused):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidChange):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidDiscard):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype._discardSelectorChange):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderSelector):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderOrigin):
+
+        (WI.SpreadsheetSelectorField):
+        (WI.SpreadsheetSelectorField.prototype.get editing):
+        (WI.SpreadsheetSelectorField.prototype.startEditing):
+        (WI.SpreadsheetSelectorField.prototype.stopEditing):
+        (WI.SpreadsheetSelectorField.prototype._handleClick):
+        (WI.SpreadsheetSelectorField.prototype._handleFocus):
+        (WI.SpreadsheetSelectorField.prototype._handleBlur):
+        (WI.SpreadsheetSelectorField.prototype._handleKeyDown):
+
 2017-10-03  Matt Baker  <mattbaker@apple.com>
 
         Web Inspector: Add View layout tests, make views more testable
index 74ff7b9..368432c 100644 (file)
@@ -180,10 +180,14 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._valueElement.textContent = this._property.rawValue;
 
         if (this._property.editable && this._property.enabled) {
+            this._nameElement.tabIndex = 1;
             this._nameElement.contentEditable = "plaintext-only";
+            this._nameElement.spellcheck = false;
             this._nameElement.addEventListener("input", this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleNameChange);
 
+            this._valueElement.tabIndex = 1;
             this._valueElement.contentEditable = "plaintext-only";
+            this._valueElement.spellcheck = false;
             this._valueElement.addEventListener("input", this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleValueChange);
         }
 
index 262e7e7..74797d5 100644 (file)
@@ -79,6 +79,7 @@
     color: hsl(0, 0%, 50%);
 }
 
+.spreadsheet-css-declaration .selector:focus,
 .spreadsheet-css-declaration .selector > .matched {
     color: black;
 }
     height: 10px;
     content: url(../Images/Locked.svg);
 }
+
+.spreadsheet-css-declaration .selector.spreadsheet-selector-field {
+    outline-offset: -3px;
+}
+
+.spreadsheet-css-declaration .selector.spreadsheet-selector-field.editing {
+    box-shadow: hsla(0, 0%, 0%, 0.5) 0 1px 3px;
+    outline: none !important;
+}
index 86bc9e5..a5b912c 100644 (file)
@@ -42,6 +42,11 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
 
     get style() { return this._style; }
 
+    get selectorEditable()
+    {
+        return this._style.editable && this._style.ownerRule;
+    }
+
     initialLayout()
     {
         super.initialLayout();
@@ -57,6 +62,11 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
         this._selectorElement.classList.add("selector");
         this._headerElement.append(this._selectorElement);
 
+        if (this.selectorEditable) {
+            this._selectorElement.tabIndex = 1;
+            this._selectorTextField = new WI.SpreadsheetSelectorField(this, this._selectorElement);
+        }
+
         this._propertiesEditor = new WI.SpreadsheetCSSStyleDeclarationEditor(this, this._style);
         this._propertiesEditor.element.classList.add("properties");
 
@@ -83,9 +93,45 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
     {
         super.layout();
 
-        this._selectorElement.removeChildren();
-        this._originElement.removeChildren();
+        this._renderOrigin();
+        this._renderSelector();
+    }
+
+    cssStyleDeclarationTextEditorFocused()
+    {
+        if (this._delegate && typeof this._delegate.cssStyleDeclarationSectionEditorFocused === "function")
+            this._delegate.cssStyleDeclarationSectionEditorFocused(this);
+    }
 
+    spreadsheetSelectorFieldDidChange()
+    {
+        let selectorText = this._selectorElement.textContent.trim();
+        if (!selectorText || selectorText === this._style.ownerRule.selectorText) {
+            this._discardSelectorChange();
+            return;
+        }
+
+        this._style.ownerRule.singleFireEventListener(WI.CSSRule.Event.SelectorChanged, this._renderSelector, this);
+
+        this._style.ownerRule.selectorText = selectorText;
+    }
+
+    spreadsheetSelectorFieldDidDiscard()
+    {
+        this._discardSelectorChange();
+    }
+
+    // Private
+
+    _discardSelectorChange()
+    {
+        // Re-render selector for syntax highlighting.
+        this._renderSelector();
+    }
+
+    _renderSelector()
+    {
+        this._selectorElement.removeChildren();
         this._selectorElements = [];
 
         let appendSelector = (selector, matched) => {
@@ -145,6 +191,27 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
             } else
                 appendSelectorTextKnownToMatch(this._style.ownerRule.selectorText);
 
+            break;
+
+        case WI.CSSStyleDeclaration.Type.Inline:
+            this._selectorElement.textContent = WI.UIString("Style Attribute");
+            this._selectorElement.classList.add("style-attribute");
+            break;
+
+        case WI.CSSStyleDeclaration.Type.Attribute:
+            appendSelectorTextKnownToMatch(this._style.node.displayName);
+            break;
+        }
+    }
+
+    _renderOrigin()
+    {
+        this._originElement.removeChildren();
+
+        switch (this._style.type) {
+        case WI.CSSStyleDeclaration.Type.Rule:
+            console.assert(this._style.ownerRule);
+
             if (this._style.ownerRule.sourceCodeLocation) {
                 let options = {
                     dontFloat: true,
@@ -194,39 +261,14 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
                     this._originElement.title = WI.UIString("%s cannot be modified").format(styleTitle);
                 }
             }
-
-            break;
-
-        case WI.CSSStyleDeclaration.Type.Inline:
-            this._selectorElement.textContent = WI.UIString("Style Attribute");
-            this._selectorElement.classList.add("style-attribute");
             break;
 
         case WI.CSSStyleDeclaration.Type.Attribute:
-            appendSelectorTextKnownToMatch(this._style.node.displayName);
             this._originElement.append(WI.UIString("HTML Attributes"));
             break;
         }
     }
 
-    cssStyleDeclarationTextEditorFocused()
-    {
-        if (this._delegate && typeof this._delegate.cssStyleDeclarationSectionEditorFocused === "function")
-            this._delegate.cssStyleDeclarationSectionEditorFocused(this);
-    }
-
-    get locked()
-    {
-        return !this._style.editable;
-    }
-
-    get selectorEditable()
-    {
-        return this._style.editable && this._style.ownerRule;
-    }
-
-    // Private
-
     _createMediaHeader()
     {
         if (!this._style.ownerRule)
@@ -252,3 +294,112 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
 };
 
 WI.SpreadsheetCSSStyleDeclarationSection.MatchedSelectorElementStyleClassName = "matched";
+
+WI.SpreadsheetSelectorField = class SpreadsheetSelectorField
+{
+    constructor(delegate, element)
+    {
+        this._delegate = delegate;
+        this._element = element;
+        this._element.classList.add("spreadsheet-selector-field");
+
+        this._element.addEventListener("click", this._handleClick.bind(this));
+        this._element.addEventListener("focus", this._handleFocus.bind(this));
+        this._element.addEventListener("blur", this._handleBlur.bind(this));
+        this._element.addEventListener("keydown", this._handleKeyDown.bind(this));
+
+        this._editing = false;
+    }
+
+    // Public
+
+    get editing() { return this._editing; }
+
+    startEditing()
+    {
+        if (this._editing)
+            return;
+
+        this._editing = true;
+
+        let element = this._element;
+        element.classList.add("editing");
+        element.contentEditable = "plaintext-only";
+        element.spellcheck = false;
+        element.scrollIntoViewIfNeeded(false);
+
+        // Disable syntax highlighting.
+        element.textContent = element.textContent;
+
+        let selection = window.getSelection();
+        let range = document.createRange();
+        range.selectNodeContents(element);
+        selection.removeAllRanges();
+        selection.addRange(range);
+    }
+
+    stopEditing()
+    {
+        if (!this._editing)
+            return;
+
+        this._editing = false;
+        this._element.classList.remove("editing");
+        this._element.contentEditable = false;
+    }
+
+    // Private
+
+    _handleClick(event)
+    {
+        this.startEditing();
+    }
+
+    _handleFocus(event)
+    {
+        this.startEditing();
+    }
+
+    _handleBlur(event)
+    {
+        this.stopEditing();
+
+        if (this._delegate && typeof this._delegate.spreadsheetSelectorFieldDidChange === "function")
+            this._delegate.spreadsheetSelectorFieldDidChange();
+    }
+
+    _handleKeyDown(event)
+    {
+        if (event.key === "Enter" && !this._editing) {
+            event.stopImmediatePropagation();
+            event.preventDefault();
+
+            this.startEditing();
+            return;
+        }
+
+        if (event.key === "Enter" || event.key === "Tab") {
+            if (event.key === "Enter") {
+                event.stopImmediatePropagation();
+                event.preventDefault();
+            }
+
+            this.stopEditing();
+
+            if (this._delegate && typeof this._delegate.spreadsheetSelectorFieldDidChange === "function")
+                this._delegate.spreadsheetSelectorFieldDidChange();
+
+            return;
+        }
+
+        if (event.key === "Escape") {
+            event.stopImmediatePropagation();
+            event.preventDefault();
+
+            this.stopEditing();
+
+            if (this._delegate && typeof this._delegate.spreadsheetSelectorFieldDidDiscard === "function")
+                this._delegate.spreadsheetSelectorFieldDidDiscard();
+        }
+    }
+};