Changeset 223283 in webkit


Ignore:
Timestamp:
Oct 13, 2017 10:14:22 AM (6 years ago)
Author:
Nikita Vasilyev
Message:

Web Inspector: Styles Redesign: hook up autocompletion to property names and values
https://bugs.webkit.org/show_bug.cgi?id=177313
<rdar://problem/34577057>

Reviewed by Joseph Pecoraro.

  • Arrow Right accept the current completion item and places the text caret after it.
  • Arrow Left hides the completion popover.
  • Arrow Up selects the previous completion item.
  • Arrow Down selects the next completion item.
  • Enter and Tab accept the current completion item and navigate to the next focusable item.
  • Escape hides the completion popover, if there is one.
  • UserInterface/Views/CompletionSuggestionsView.js:

(WI.CompletionSuggestionsView):
(WI.CompletionSuggestionsView.prototype._mouseDown):
Add a preventBlur option so clicking on an completion item doesn't change the focus and
doesn't cause "blur" event on the target text field.

  • UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css:

(.spreadsheet-style-declaration-editor .completion-hint):

  • UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js:

(WI.SpreadsheetCSSStyleDeclarationEditor):
(WI.SpreadsheetCSSStyleDeclarationEditor.prototype.layout):
(WI.SpreadsheetCSSStyleDeclarationEditor.prototype.detached):
Call detached on every SpreadsheetTextField to hide CompletionSuggestionsView once
SpreadsheetCSSStyleDeclarationEditor is removed from the DOM.

(WI.SpreadsheetCSSStyleDeclarationEditor.prototype._addBlankProperty):
Remove index argument since it is no longer used.

  • UserInterface/Views/SpreadsheetStyleProperty.js:

(WI.SpreadsheetStyleProperty):
(WI.SpreadsheetStyleProperty.prototype.detached):
(WI.SpreadsheetStyleProperty.prototype._remove):
(WI.SpreadsheetStyleProperty.prototype._update):
(WI.SpreadsheetStyleProperty.prototype._nameCompletionDataProvider):
(WI.SpreadsheetStyleProperty.prototype._valueCompletionDataProvider):
Add an extra parameter to SpreadsheetTextField to pass a completion data provider.

  • UserInterface/Views/SpreadsheetTextField.js:

(WI.SpreadsheetTextField):
(WI.SpreadsheetTextField.prototype.get suggestionHint):
(WI.SpreadsheetTextField.prototype.set suggestionHint):
(WI.SpreadsheetTextField.prototype.startEditing):
(WI.SpreadsheetTextField.prototype.stopEditing):
(WI.SpreadsheetTextField.prototype.detached):
(WI.SpreadsheetTextField.prototype.completionSuggestionsSelectedCompletion):
(WI.SpreadsheetTextField.prototype.completionSuggestionsClickedCompletion):
(WI.SpreadsheetTextField.prototype._getPrefix):
(WI.SpreadsheetTextField.prototype._handleBlur):
(WI.SpreadsheetTextField.prototype._handleKeyDown):
(WI.SpreadsheetTextField.prototype._handleKeyDownForSuggestionView):
(WI.SpreadsheetTextField.prototype._handleInput):
(WI.SpreadsheetTextField.prototype._updateCompletions):
(WI.SpreadsheetTextField.prototype._getCaretRect):
(WI.SpreadsheetTextField.prototype._getCompletionPrefix):
(WI.SpreadsheetTextField.prototype._applyCompletionHint):
(WI.SpreadsheetTextField.prototype._hideCompletions):
Provide text completion based on the existing CompletionSuggestionsView when completionProvider is passed to SpreadsheetTextField.

Location:
trunk/Source/WebInspectorUI
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebInspectorUI/ChangeLog

    r223268 r223283  
     12017-10-13  Nikita Vasilyev  <nvasilyev@apple.com>
     2
     3        Web Inspector: Styles Redesign: hook up autocompletion to property names and values
     4        https://bugs.webkit.org/show_bug.cgi?id=177313
     5        <rdar://problem/34577057>
     6
     7        Reviewed by Joseph Pecoraro.
     8
     9        - Arrow Right accept the current completion item and places the text caret after it.
     10        - Arrow Left hides the completion popover.
     11        - Arrow Up selects the previous completion item.
     12        - Arrow Down selects the next completion item.
     13        - Enter and Tab accept the current completion item and navigate to the next focusable item.
     14        - Escape hides the completion popover, if there is one.
     15
     16        * UserInterface/Views/CompletionSuggestionsView.js:
     17        (WI.CompletionSuggestionsView):
     18        (WI.CompletionSuggestionsView.prototype._mouseDown):
     19        Add a preventBlur option so clicking on an completion item doesn't change the focus and
     20        doesn't cause "blur" event on the target text field.
     21
     22        * UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css:
     23        (.spreadsheet-style-declaration-editor .completion-hint):
     24        * UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js:
     25        (WI.SpreadsheetCSSStyleDeclarationEditor):
     26        (WI.SpreadsheetCSSStyleDeclarationEditor.prototype.layout):
     27        (WI.SpreadsheetCSSStyleDeclarationEditor.prototype.detached):
     28        Call detached on every SpreadsheetTextField to hide CompletionSuggestionsView once
     29        SpreadsheetCSSStyleDeclarationEditor is removed from the DOM.
     30
     31        (WI.SpreadsheetCSSStyleDeclarationEditor.prototype._addBlankProperty):
     32        Remove index argument since it is no longer used.
     33
     34        * UserInterface/Views/SpreadsheetStyleProperty.js:
     35        (WI.SpreadsheetStyleProperty):
     36        (WI.SpreadsheetStyleProperty.prototype.detached):
     37        (WI.SpreadsheetStyleProperty.prototype._remove):
     38        (WI.SpreadsheetStyleProperty.prototype._update):
     39        (WI.SpreadsheetStyleProperty.prototype._nameCompletionDataProvider):
     40        (WI.SpreadsheetStyleProperty.prototype._valueCompletionDataProvider):
     41        Add an extra parameter to SpreadsheetTextField to pass a completion data provider.
     42
     43        * UserInterface/Views/SpreadsheetTextField.js:
     44        (WI.SpreadsheetTextField):
     45        (WI.SpreadsheetTextField.prototype.get suggestionHint):
     46        (WI.SpreadsheetTextField.prototype.set suggestionHint):
     47        (WI.SpreadsheetTextField.prototype.startEditing):
     48        (WI.SpreadsheetTextField.prototype.stopEditing):
     49        (WI.SpreadsheetTextField.prototype.detached):
     50        (WI.SpreadsheetTextField.prototype.completionSuggestionsSelectedCompletion):
     51        (WI.SpreadsheetTextField.prototype.completionSuggestionsClickedCompletion):
     52        (WI.SpreadsheetTextField.prototype._getPrefix):
     53        (WI.SpreadsheetTextField.prototype._handleBlur):
     54        (WI.SpreadsheetTextField.prototype._handleKeyDown):
     55        (WI.SpreadsheetTextField.prototype._handleKeyDownForSuggestionView):
     56        (WI.SpreadsheetTextField.prototype._handleInput):
     57        (WI.SpreadsheetTextField.prototype._updateCompletions):
     58        (WI.SpreadsheetTextField.prototype._getCaretRect):
     59        (WI.SpreadsheetTextField.prototype._getCompletionPrefix):
     60        (WI.SpreadsheetTextField.prototype._applyCompletionHint):
     61        (WI.SpreadsheetTextField.prototype._hideCompletions):
     62        Provide text completion based on the existing CompletionSuggestionsView when completionProvider is passed to SpreadsheetTextField.
     63
    1642017-10-12  Joseph Pecoraro  <pecoraro@apple.com>
    265
  • trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js

    r220119 r223283  
    2626WI.CompletionSuggestionsView = class CompletionSuggestionsView extends WI.Object
    2727{
    28     constructor(delegate)
     28    constructor(delegate, {preventBlur} = {})
    2929    {
    3030        super();
    3131
    3232        this._delegate = delegate || null;
     33        this._preventBlur = preventBlur || false;
    3334
    3435        this._selectedIndex = NaN;
     
    198199        if (event.button !== 0)
    199200            return;
     201
     202        if (this._preventBlur)
     203            event.preventDefault();
     204
    200205        this._mouseIsDown = true;
    201206    }
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css

    r222959 r223283  
    4545}
    4646
     47.spreadsheet-style-declaration-editor .value.editing {
     48    display: inline-block;
     49    margin-right: 3px;
     50}
     51
    4752.spreadsheet-style-declaration-editor.no-properties {
    4853    display: none;
     
    8085    opacity: 0.5;
    8186}
     87
     88.spreadsheet-style-declaration-editor .completion-hint {
     89    color: hsl(0, 0%, 50%) !important;
     90}
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js

    r222959 r223283  
    3434        this._delegate = delegate;
    3535        this.style = style;
     36        this._propertyViews = [];
    3637    }
    3738
     
    5051        for (let index = 0; index < properties.length; index++) {
    5152            let property = properties[index];
    52             let propertyView = new WI.SpreadsheetStyleProperty(this, property, index);
     53            let propertyView = new WI.SpreadsheetStyleProperty(this, property);
    5354            this.element.append(propertyView.element);
    5455            this._propertyViews.push(propertyView);
    5556        }
     57    }
     58
     59    detached()
     60    {
     61        for (let propertyView of this._propertyViews)
     62            propertyView.detached();
    5663    }
    5764
     
    156163        let blankProperty = this._style.newBlankProperty(afterIndex);
    157164        const newlyAdded = true;
    158         let propertyView = new WI.SpreadsheetStyleProperty(this, blankProperty, blankProperty.index, newlyAdded);
     165        let propertyView = new WI.SpreadsheetStyleProperty(this, blankProperty, newlyAdded);
    159166        this.element.append(propertyView.element);
    160167        this._propertyViews.push(propertyView);
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js

    r222959 r223283  
    2626WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
    2727{
    28     constructor(delegate, property, index, newlyAdded)
     28    constructor(delegate, property, newlyAdded = false)
    2929    {
    3030        super();
     31
     32        console.assert(property instanceof WI.CSSProperty);
    3133
    3234        this._delegate = delegate || null;
    3335        this._property = property;
    34         this._newlyAdded = newlyAdded || false;
     36        this._newlyAdded = newlyAdded;
    3537        this._element = document.createElement("div");
    3638
     
    5153    get valueTextField() { return this._valueTextField; }
    5254
     55    detached()
     56    {
     57        if (this._nameTextField)
     58            this._nameTextField.detached();
     59
     60        if (this._valueTextField)
     61            this._valueTextField.detached();
     62    }
     63
    5364    // Private
    5465
     
    5768        this.element.remove();
    5869        this._property.remove();
     70        this.detached();
    5971
    6072        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
     
    135147        if (this._property.editable && this._property.enabled) {
    136148            this._nameElement.tabIndex = 0;
    137             this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement);
     149            this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
    138150
    139151            this._valueElement.tabIndex = 0;
    140             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement);
     152            this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
    141153        }
    142154
     
    210222        this._property.rawValue = this._valueElement.textContent.trim();
    211223    }
     224
     225    _nameCompletionDataProvider(prefix)
     226    {
     227        return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
     228    }
     229
     230    _valueCompletionDataProvider(prefix)
     231    {
     232        return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(prefix);
     233    }
    212234};
    213235
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js

    r222959 r223283  
    2626WI.SpreadsheetTextField = class SpreadsheetTextField
    2727{
    28     constructor(delegate, element)
     28    constructor(delegate, element, completionProvider)
    2929    {
    3030        this._delegate = delegate;
    3131        this._element = element;
     32
     33        this._completionProvider = completionProvider || null;
     34        if (this._completionProvider) {
     35            this._suggestionHintElement = document.createElement("span");
     36            this._suggestionHintElement.contentEditable = false;
     37            this._suggestionHintElement.classList.add("completion-hint");
     38            this._suggestionsView = new WI.CompletionSuggestionsView(this, {preventBlur: true});
     39        }
     40
    3241        this._element.classList.add("spreadsheet-text-field");
    3342
     
    5059    set value(value) { this._element.textContent = value; }
    5160
     61    get suggestionHint()
     62    {
     63        return this._suggestionHintElement.textContent;
     64    }
     65
     66    set suggestionHint(value)
     67    {
     68        this._suggestionHintElement.textContent = value;
     69
     70        if (value) {
     71            if (this._suggestionHintElement.parentElement !== this._element)
     72                this._element.append(this._suggestionHintElement);
     73        } else
     74            this._suggestionHintElement.remove();
     75    }
     76
    5277    startEditing()
    5378    {
     
    6893        this._element.focus();
    6994        this._selectText();
     95
     96        this._updateCompletions();
    7097    }
    7198
     
    79106        this._element.classList.remove("editing");
    80107        this._element.contentEditable = false;
     108
     109        this._hideCompletions();
     110    }
     111
     112    detached()
     113    {
     114        this._hideCompletions();
     115        this._element.remove();
     116    }
     117
     118    // CompletionSuggestionsView delegate
     119
     120    completionSuggestionsSelectedCompletion(suggestionsView, selectedText = "")
     121    {
     122        let prefix = this._getPrefix();
     123        let completionPrefix = this._getCompletionPrefix(prefix);
     124
     125        this.suggestionHint = selectedText.slice(completionPrefix.length);
     126
     127        if (this._suggestionHintElement.parentElement !== this._element)
     128            this._element.append(this._suggestionHintElement);
     129
     130        if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     131            this._delegate.spreadsheetTextFieldDidChange(this);
     132    }
     133
     134    completionSuggestionsClickedCompletion(suggestionsView, selectedText)
     135    {
     136        // Consider the following example:
     137        //
     138        //   border: 1px solid ro|
     139        //                     rosybrown
     140        //                     royalblue
     141        //
     142        // Clicking on "rosybrown" should replace "ro" with "rosybrown".
     143        //
     144        //           prefix:  1px solid ro
     145        // completionPrefix:            ro
     146        //        newPrefix:  1px solid
     147        //     selectedText:            rosybrown
     148        let prefix = this._getPrefix();
     149        let completionPrefix = this._getCompletionPrefix(prefix);
     150        let newPrefix = prefix.slice(0, -completionPrefix.length);
     151
     152        this._element.textContent = newPrefix + selectedText;
     153
     154        // Place text caret at the end.
     155        window.getSelection().setBaseAndExtent(this._element, selectedText.length, this._element, selectedText.length);
     156
     157        this._hideCompletions();
     158
     159        if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     160            this._delegate.spreadsheetTextFieldDidChange(this);
    81161    }
    82162
     
    99179    }
    100180
     181    _getPrefix()
     182    {
     183        let value = this._element.textContent;
     184        return value.slice(0, value.length - this.suggestionHint.length);
     185    }
     186
    101187    _handleFocus(event)
    102188    {
     
    108194        if (!this._editing)
    109195            return;
     196
     197        this._applyCompletionHint();
     198        this._hideCompletions();
    110199
    111200        this._delegate.spreadsheetTextFieldDidBlur(this);
     
    118207            return;
    119208
     209        if (this._suggestionsView) {
     210            let consumed = this._handleKeyDownForSuggestionView(event);
     211            if (consumed)
     212                return;
     213        }
     214
    120215        if (event.key === "Enter" || event.key === "Tab") {
    121216            event.stop();
    122             this.stopEditing();
     217            this._applyCompletionHint();
    123218
    124219            let direction = (event.shiftKey && event.key === "Tab") ? "backward" : "forward";
     
    127222                this._delegate.spreadsheetTextFieldDidCommit(this, {direction});
    128223
     224            this.stopEditing();
    129225            return;
    130226        }
     
    136232    }
    137233
     234    _handleKeyDownForSuggestionView(event)
     235    {
     236        if ((event.key === "ArrowDown" || event.key === "ArrowUp") && this._suggestionsView.visible) {
     237            event.stop();
     238
     239            if (event.key === "ArrowDown")
     240                this._suggestionsView.selectNext();
     241            else
     242                this._suggestionsView.selectPrevious();
     243
     244            if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     245                this._delegate.spreadsheetTextFieldDidChange(this);
     246
     247            return true;
     248        }
     249
     250        if (event.key === "ArrowRight" && this.suggestionHint) {
     251            let selection = window.getSelection();
     252
     253            if (selection.isCollapsed && (selection.focusOffset === this._getPrefix().length || selection.focusNode === this._suggestionHintElement)) {
     254                event.stop();
     255                document.execCommand("insertText", false, this.suggestionHint);
     256
     257                // When completing "background", don't hide the completion popover.
     258                // Continue showing the popover with properties such as "background-color" and "background-image".
     259                this._updateCompletions();
     260
     261                if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     262                    this._delegate.spreadsheetTextFieldDidChange(this);
     263
     264                return true;
     265            }
     266        }
     267
     268        if (event.key === "Escape" && this._suggestionsView.visible) {
     269            event.stop();
     270
     271            let willChange = !!this.suggestionHint;
     272            this._hideCompletions();
     273
     274            if (willChange && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     275                this._delegate.spreadsheetTextFieldDidChange(this);
     276
     277            return true;
     278        }
     279
     280        if (event.key === "ArrowLeft" && (this.suggestionHint || this._suggestionsView.visible)) {
     281            this._hideCompletions();
     282
     283            if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     284                this._delegate.spreadsheetTextFieldDidChange(this);
     285        }
     286
     287        return false;
     288    }
     289
    138290    _handleInput(event)
    139291    {
    140292        if (!this._editing)
    141293            return;
     294
     295        this._updateCompletions();
    142296
    143297        if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
    144298            this._delegate.spreadsheetTextFieldDidChange(this);
    145299    }
     300
     301    _updateCompletions()
     302    {
     303        if (!this._completionProvider)
     304            return;
     305
     306        let prefix = this._getPrefix();
     307        let completionPrefix = this._getCompletionPrefix(prefix);
     308        let completions = this._completionProvider(completionPrefix);
     309
     310        if (!completions.length) {
     311            this._hideCompletions();
     312            return;
     313        }
     314
     315        // No need to show the completion popover with only one item that matches the entered value.
     316        if (completions.length === 1 && completions[0] === prefix) {
     317            this._hideCompletions();
     318            return;
     319        }
     320
     321        console.assert(this._element.parentNode, "_updateCompletions got called after SpreadsheetTextField was removed from the DOM");
     322        if (!this._element.parentNode) {
     323            this._suggestionsView.hide();
     324            return;
     325        }
     326
     327        this._suggestionsView.update(completions);
     328
     329        if (completions.length === 1) {
     330            // No need to show the completion popover that matches the suggestion hint.
     331            this._suggestionsView.hide();
     332        } else {
     333            let caretRect = this._getCaretRect(prefix, completionPrefix);
     334            this._suggestionsView.show(caretRect);
     335        }
     336
     337        // Select first item and call completionSuggestionsSelectedCompletion.
     338        this._suggestionsView.selectedIndex = NaN;
     339        this._suggestionsView.selectNext();
     340
     341        if (!completionPrefix)
     342            this.suggestionHint = "";
     343    }
     344
     345    _getCaretRect(prefix, completionPrefix)
     346    {
     347        let startOffset = prefix.length - completionPrefix.length;
     348        let selection = window.getSelection();
     349
     350        if (startOffset > 0 && selection.rangeCount) {
     351            let range = selection.getRangeAt(0).cloneRange();
     352            range.setStart(range.startContainer, startOffset);
     353            let clientRect = range.getBoundingClientRect();
     354            return WI.Rect.rectFromClientRect(clientRect);
     355        }
     356
     357        let clientRect = this._element.getBoundingClientRect();
     358        const leftPadding = parseInt(getComputedStyle(this._element).paddingLeft) || 0;
     359        return new WI.Rect(clientRect.left + leftPadding, clientRect.top, clientRect.width, clientRect.height);
     360    }
     361
     362    _getCompletionPrefix(prefix)
     363    {
     364        // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
     365        let match = prefix.match(/[a-z0-9()-]+$/i);
     366        if (match)
     367            return match[0];
     368
     369        return prefix;
     370    }
     371
     372    _applyCompletionHint()
     373    {
     374        if (!this._completionProvider || !this.suggestionHint)
     375            return;
     376
     377        this._element.textContent = this._element.textContent;
     378    }
     379
     380    _hideCompletions()
     381    {
     382        if (!this._completionProvider)
     383            return;
     384
     385        this._suggestionsView.hide();
     386        this.suggestionHint = "";
     387    }
    146388};
Note: See TracChangeset for help on using the changeset viewer.