Diff
Modified: trunk/Source/WebInspectorUI/ChangeLog (223282 => 223283)
--- trunk/Source/WebInspectorUI/ChangeLog 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/ChangeLog 2017-10-13 17:14:22 UTC (rev 223283)
@@ -1,3 +1,66 @@
+2017-10-13 Nikita Vasilyev <nvasil...@apple.com>
+
+ 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.
+
2017-10-12 Joseph Pecoraro <pecor...@apple.com>
Web Inspector: Switch Clear navigation item back to the Trash icon (Console, Timelines, Network)
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js (223282 => 223283)
--- trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js 2017-10-13 17:14:22 UTC (rev 223283)
@@ -25,11 +25,12 @@
WI.CompletionSuggestionsView = class CompletionSuggestionsView extends WI.Object
{
- constructor(delegate)
+ constructor(delegate, {preventBlur} = {})
{
super();
this._delegate = delegate || null;
+ this._preventBlur = preventBlur || false;
this._selectedIndex = NaN;
@@ -197,6 +198,10 @@
{
if (event.button !== 0)
return;
+
+ if (this._preventBlur)
+ event.preventDefault();
+
this._mouseIsDown = true;
}
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css (223282 => 223283)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css 2017-10-13 17:14:22 UTC (rev 223283)
@@ -44,6 +44,11 @@
padding-bottom: 0 !important;
}
+.spreadsheet-style-declaration-editor .value.editing {
+ display: inline-block;
+ margin-right: 3px;
+}
+
.spreadsheet-style-declaration-editor.no-properties {
display: none;
}
@@ -79,3 +84,7 @@
.spreadsheet-style-declaration-editor .property.not-inherited {
opacity: 0.5;
}
+
+.spreadsheet-style-declaration-editor .completion-hint {
+ color: hsl(0, 0%, 50%) !important;
+}
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js (223282 => 223283)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.js 2017-10-13 17:14:22 UTC (rev 223283)
@@ -33,6 +33,7 @@
this._delegate = delegate;
this.style = style;
+ this._propertyViews = [];
}
// Public
@@ -49,12 +50,18 @@
this._propertyViews = [];
for (let index = 0; index < properties.length; index++) {
let property = properties[index];
- let propertyView = new WI.SpreadsheetStyleProperty(this, property, index);
+ let propertyView = new WI.SpreadsheetStyleProperty(this, property);
this.element.append(propertyView.element);
this._propertyViews.push(propertyView);
}
}
+ detached()
+ {
+ for (let propertyView of this._propertyViews)
+ propertyView.detached();
+ }
+
get style()
{
return this._style;
@@ -155,7 +162,7 @@
{
let blankProperty = this._style.newBlankProperty(afterIndex);
const newlyAdded = true;
- let propertyView = new WI.SpreadsheetStyleProperty(this, blankProperty, blankProperty.index, newlyAdded);
+ let propertyView = new WI.SpreadsheetStyleProperty(this, blankProperty, newlyAdded);
this.element.append(propertyView.element);
this._propertyViews.push(propertyView);
propertyView.nameTextField.startEditing();
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js (223282 => 223283)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js 2017-10-13 17:14:22 UTC (rev 223283)
@@ -25,13 +25,15 @@
WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
{
- constructor(delegate, property, index, newlyAdded)
+ constructor(delegate, property, newlyAdded = false)
{
super();
+ console.assert(property instanceof WI.CSSProperty);
+
this._delegate = delegate || null;
this._property = property;
- this._newlyAdded = newlyAdded || false;
+ this._newlyAdded = newlyAdded;
this._element = document.createElement("div");
this._nameElement = null;
@@ -50,6 +52,15 @@
get nameTextField() { return this._nameTextField; }
get valueTextField() { return this._valueTextField; }
+ detached()
+ {
+ if (this._nameTextField)
+ this._nameTextField.detached();
+
+ if (this._valueTextField)
+ this._valueTextField.detached();
+ }
+
// Private
_remove()
@@ -56,6 +67,7 @@
{
this.element.remove();
this._property.remove();
+ this.detached();
if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
this._delegate.spreadsheetStylePropertyRemoved(this);
@@ -134,10 +146,10 @@
if (this._property.editable && this._property.enabled) {
this._nameElement.tabIndex = 0;
- this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement);
+ this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
this._valueElement.tabIndex = 0;
- this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement);
+ this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
}
this.element.append(";");
@@ -209,6 +221,16 @@
{
this._property.rawValue = this._valueElement.textContent.trim();
}
+
+ _nameCompletionDataProvider(prefix)
+ {
+ return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+ }
+
+ _valueCompletionDataProvider(prefix)
+ {
+ return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(prefix);
+ }
};
WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js (223282 => 223283)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js 2017-10-13 17:07:32 UTC (rev 223282)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js 2017-10-13 17:14:22 UTC (rev 223283)
@@ -25,10 +25,19 @@
WI.SpreadsheetTextField = class SpreadsheetTextField
{
- constructor(delegate, element)
+ constructor(delegate, element, completionProvider)
{
this._delegate = delegate;
this._element = element;
+
+ this._completionProvider = completionProvider || null;
+ if (this._completionProvider) {
+ this._suggestionHintElement = document.createElement("span");
+ this._suggestionHintElement.contentEditable = false;
+ this._suggestionHintElement.classList.add("completion-hint");
+ this._suggestionsView = new WI.CompletionSuggestionsView(this, {preventBlur: true});
+ }
+
this._element.classList.add("spreadsheet-text-field");
this._element.addEventListener("focus", this._handleFocus.bind(this));
@@ -49,6 +58,22 @@
get value() { return this._element.textContent; }
set value(value) { this._element.textContent = value; }
+ get suggestionHint()
+ {
+ return this._suggestionHintElement.textContent;
+ }
+
+ set suggestionHint(value)
+ {
+ this._suggestionHintElement.textContent = value;
+
+ if (value) {
+ if (this._suggestionHintElement.parentElement !== this._element)
+ this._element.append(this._suggestionHintElement);
+ } else
+ this._suggestionHintElement.remove();
+ }
+
startEditing()
{
if (this._editing)
@@ -67,6 +92,8 @@
this._element.focus();
this._selectText();
+
+ this._updateCompletions();
}
stopEditing()
@@ -78,8 +105,61 @@
this._startEditingValue = "";
this._element.classList.remove("editing");
this._element.contentEditable = false;
+
+ this._hideCompletions();
}
+ detached()
+ {
+ this._hideCompletions();
+ this._element.remove();
+ }
+
+ // CompletionSuggestionsView delegate
+
+ completionSuggestionsSelectedCompletion(suggestionsView, selectedText = "")
+ {
+ let prefix = this._getPrefix();
+ let completionPrefix = this._getCompletionPrefix(prefix);
+
+ this.suggestionHint = selectedText.slice(completionPrefix.length);
+
+ if (this._suggestionHintElement.parentElement !== this._element)
+ this._element.append(this._suggestionHintElement);
+
+ if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+ }
+
+ completionSuggestionsClickedCompletion(suggestionsView, selectedText)
+ {
+ // Consider the following example:
+ //
+ // border: 1px solid ro|
+ // rosybrown
+ // royalblue
+ //
+ // Clicking on "rosybrown" should replace "ro" with "rosybrown".
+ //
+ // prefix: 1px solid ro
+ // completionPrefix: ro
+ // newPrefix: 1px solid
+ // selectedText: rosybrown
+ let prefix = this._getPrefix();
+ let completionPrefix = this._getCompletionPrefix(prefix);
+ let newPrefix = prefix.slice(0, -completionPrefix.length);
+
+ this._element.textContent = newPrefix + selectedText;
+
+ // Place text caret at the end.
+ window.getSelection().setBaseAndExtent(this._element, selectedText.length, this._element, selectedText.length);
+
+ this._hideCompletions();
+
+ if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+ }
+
// Private
_selectText()
@@ -98,6 +178,12 @@
}
}
+ _getPrefix()
+ {
+ let value = this._element.textContent;
+ return value.slice(0, value.length - this.suggestionHint.length);
+ }
+
_handleFocus(event)
{
this.startEditing();
@@ -108,6 +194,9 @@
if (!this._editing)
return;
+ this._applyCompletionHint();
+ this._hideCompletions();
+
this._delegate.spreadsheetTextFieldDidBlur(this);
this.stopEditing();
}
@@ -117,9 +206,15 @@
if (!this._editing)
return;
+ if (this._suggestionsView) {
+ let consumed = this._handleKeyDownForSuggestionView(event);
+ if (consumed)
+ return;
+ }
+
if (event.key === "Enter" || event.key === "Tab") {
event.stop();
- this.stopEditing();
+ this._applyCompletionHint();
let direction = (event.shiftKey && event.key === "Tab") ? "backward" : "forward";
@@ -126,6 +221,7 @@
if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidCommit === "function")
this._delegate.spreadsheetTextFieldDidCommit(this, {direction});
+ this.stopEditing();
return;
}
@@ -135,12 +231,158 @@
}
}
+ _handleKeyDownForSuggestionView(event)
+ {
+ if ((event.key === "ArrowDown" || event.key === "ArrowUp") && this._suggestionsView.visible) {
+ event.stop();
+
+ if (event.key === "ArrowDown")
+ this._suggestionsView.selectNext();
+ else
+ this._suggestionsView.selectPrevious();
+
+ if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+
+ return true;
+ }
+
+ if (event.key === "ArrowRight" && this.suggestionHint) {
+ let selection = window.getSelection();
+
+ if (selection.isCollapsed && (selection.focusOffset === this._getPrefix().length || selection.focusNode === this._suggestionHintElement)) {
+ event.stop();
+ document.execCommand("insertText", false, this.suggestionHint);
+
+ // When completing "background", don't hide the completion popover.
+ // Continue showing the popover with properties such as "background-color" and "background-image".
+ this._updateCompletions();
+
+ if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+
+ return true;
+ }
+ }
+
+ if (event.key === "Escape" && this._suggestionsView.visible) {
+ event.stop();
+
+ let willChange = !!this.suggestionHint;
+ this._hideCompletions();
+
+ if (willChange && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+
+ return true;
+ }
+
+ if (event.key === "ArrowLeft" && (this.suggestionHint || this._suggestionsView.visible)) {
+ this._hideCompletions();
+
+ if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
+ this._delegate.spreadsheetTextFieldDidChange(this);
+ }
+
+ return false;
+ }
+
_handleInput(event)
{
if (!this._editing)
return;
+ this._updateCompletions();
+
if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
this._delegate.spreadsheetTextFieldDidChange(this);
}
+
+ _updateCompletions()
+ {
+ if (!this._completionProvider)
+ return;
+
+ let prefix = this._getPrefix();
+ let completionPrefix = this._getCompletionPrefix(prefix);
+ let completions = this._completionProvider(completionPrefix);
+
+ if (!completions.length) {
+ this._hideCompletions();
+ return;
+ }
+
+ // No need to show the completion popover with only one item that matches the entered value.
+ if (completions.length === 1 && completions[0] === prefix) {
+ this._hideCompletions();
+ return;
+ }
+
+ console.assert(this._element.parentNode, "_updateCompletions got called after SpreadsheetTextField was removed from the DOM");
+ if (!this._element.parentNode) {
+ this._suggestionsView.hide();
+ return;
+ }
+
+ this._suggestionsView.update(completions);
+
+ if (completions.length === 1) {
+ // No need to show the completion popover that matches the suggestion hint.
+ this._suggestionsView.hide();
+ } else {
+ let caretRect = this._getCaretRect(prefix, completionPrefix);
+ this._suggestionsView.show(caretRect);
+ }
+
+ // Select first item and call completionSuggestionsSelectedCompletion.
+ this._suggestionsView.selectedIndex = NaN;
+ this._suggestionsView.selectNext();
+
+ if (!completionPrefix)
+ this.suggestionHint = "";
+ }
+
+ _getCaretRect(prefix, completionPrefix)
+ {
+ let startOffset = prefix.length - completionPrefix.length;
+ let selection = window.getSelection();
+
+ if (startOffset > 0 && selection.rangeCount) {
+ let range = selection.getRangeAt(0).cloneRange();
+ range.setStart(range.startContainer, startOffset);
+ let clientRect = range.getBoundingClientRect();
+ return WI.Rect.rectFromClientRect(clientRect);
+ }
+
+ let clientRect = this._element.getBoundingClientRect();
+ const leftPadding = parseInt(getComputedStyle(this._element).paddingLeft) || 0;
+ return new WI.Rect(clientRect.left + leftPadding, clientRect.top, clientRect.width, clientRect.height);
+ }
+
+ _getCompletionPrefix(prefix)
+ {
+ // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
+ let match = prefix.match(/[a-z0-9()-]+$/i);
+ if (match)
+ return match[0];
+
+ return prefix;
+ }
+
+ _applyCompletionHint()
+ {
+ if (!this._completionProvider || !this.suggestionHint)
+ return;
+
+ this._element.textContent = this._element.textContent;
+ }
+
+ _hideCompletions()
+ {
+ if (!this._completionProvider)
+ return;
+
+ this._suggestionsView.hide();
+ this.suggestionHint = "";
+ }
};