Bug 896181 - Autocomplete CSS properties and values in markup view, r=mratcliffe

This commit is contained in:
Girish Sharma
2013-08-02 16:05:50 +05:30
parent 92365be89f
commit deea7e8e42
6 changed files with 226 additions and 18 deletions

View File

@@ -20,6 +20,10 @@ let promise = require("sdk/core/promise");
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/devtools/Templater.jsm"); Cu.import("resource://gre/modules/devtools/Templater.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
});
/** /**
* Vocabulary for the purposes of this file: * Vocabulary for the purposes of this file:
@@ -53,6 +57,14 @@ function MarkupView(aInspector, aFrame, aControllerWindow)
this.maxChildren = DEFAULT_MAX_CHILDREN; this.maxChildren = DEFAULT_MAX_CHILDREN;
} }
// Creating the popup to be used to show CSS suggestions.
let options = {
fixedWidth: true,
autoSelect: true,
theme: "auto"
};
this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
this.undo = new UndoStack(); this.undo = new UndoStack();
this.undo.installController(aControllerWindow); this.undo.installController(aControllerWindow);
@@ -676,6 +688,9 @@ MarkupView.prototype = {
this.undo.destroy(); this.undo.destroy();
delete this.undo; delete this.undo;
this.popup.destroy();
delete this.popup;
this._frame.removeEventListener("focus", this._boundFocus, false); this._frame.removeEventListener("focus", this._boundFocus, false);
delete this._boundFocus; delete this._boundFocus;
@@ -1130,6 +1145,8 @@ function ElementEditor(aContainer, aNode)
element: this.newAttr, element: this.newAttr,
trigger: "dblclick", trigger: "dblclick",
stopOnReturn: true, stopOnReturn: true,
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
popup: this.markup.popup,
done: (aVal, aCommit) => { done: (aVal, aCommit) => {
if (!aCommit) { if (!aCommit) {
return; return;
@@ -1222,6 +1239,8 @@ ElementEditor.prototype = {
trigger: "dblclick", trigger: "dblclick",
stopOnReturn: true, stopOnReturn: true,
selectAll: false, selectAll: false,
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
popup: this.markup.popup,
start: (aEditor, aEvent) => { start: (aEditor, aEvent) => {
// If the editing was started inside the name or value areas, // If the editing was started inside the name or value areas,
// select accordingly. // select accordingly.

View File

@@ -19,6 +19,7 @@ MOCHITEST_BROWSER_FILES := \
browser_inspector_markup_edit.js \ browser_inspector_markup_edit.js \
browser_inspector_markup_subset.html \ browser_inspector_markup_subset.html \
browser_inspector_markup_subset.js \ browser_inspector_markup_subset.js \
browser_bug896181_css_mixed_completion_new_attribute.js \
head.js \ head.js \
$(NULL) $(NULL)

View File

@@ -0,0 +1,167 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test CSS state is correctly determined and the corresponding suggestions are
// displayed. i.e. CSS property suggestions are shown when cursor is like:
// ```style="di|"``` where | is teh cursor; And CSS value suggestion is
// displayed when the cursor is like: ```style="display:n|"``` properly. No
// suggestions should ever appear when the attribute is not a style attribute.
// The correctness and cycling of the suggestions is covered in the ruleview
// tests.
function test() {
let inspector;
let {
getInplaceEditorForSpan: inplaceEditor
} = devtools.require("devtools/shared/inplace-editor");
waitForExplicitFinish();
// Will hold the doc we're viewing
let doc;
// Holds the MarkupTool object we're testing.
let markup;
let editor;
let state;
// format :
// [
// what key to press,
// expected input box value after keypress,
// expected input.selectionStart,
// expected input.selectionEnd,
// is popup expected to be open ?
// ]
let testData = [
['s', 's', 1, 1, false],
['t', 'st', 2, 2, false],
['y', 'sty', 3, 3, false],
['l', 'styl', 4, 4, false],
['e', 'style', 5, 5, false],
['=', 'style=', 6, 6, false],
['"', 'style="', 7, 7, false],
['d', 'style="direction', 8, 16, true],
['VK_DOWN', 'style="display', 8, 14, true],
['VK_RIGHT', 'style="display', 14, 14, false],
[':', 'style="display:', 15, 15, false],
['n', 'style="display:none', 16, 19, false],
['VK_BACK_SPACE', 'style="display:n', 16, 16, false],
['VK_BACK_SPACE', 'style="display:', 15, 15, false],
[' ', 'style="display: ', 16, 16, false],
[' ', 'style="display: ', 17, 17, false],
['i', 'style="display: inherit', 18, 24, true],
['VK_RIGHT', 'style="display: inherit', 24, 24, false],
[';', 'style="display: inherit;', 25, 25, false],
[' ', 'style="display: inherit; ', 26, 26, false],
[' ', 'style="display: inherit; ', 27, 27, false],
['VK_LEFT', 'style="display: inherit; ', 26, 26, false],
['c', 'style="display: inherit; caption-side ', 27, 38, true],
['o', 'style="display: inherit; color ', 28, 31, true],
['VK_RIGHT', 'style="display: inherit; color ', 31, 31, false],
[' ', 'style="display: inherit; color ', 32, 32, false],
['c', 'style="display: inherit; color c ', 33, 33, false],
['VK_BACK_SPACE', 'style="display: inherit; color ', 32, 32, false],
[':', 'style="display: inherit; color : ', 33, 33, false],
['c', 'style="display: inherit; color :cadetblue ', 34, 42, true],
['VK_DOWN', 'style="display: inherit; color :chartreuse ', 34, 43, true],
['VK_RETURN', 'style="display: inherit; color :chartreuse"', -1, -1, false]
];
function startTests() {
markup = inspector.markup;
markup.expandAll().then(() => {
let node = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
let attr = node.newAttr;
attr.focus();
EventUtils.sendKey("return", inspector.panelWin);
editor = inplaceEditor(attr);
checkStateAndMoveOn(0);
});
}
function checkStateAndMoveOn(index) {
if (index == testData.length) {
finishUp();
return;
}
let [key] = testData[index];
state = index;
info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
"] for state " + state);
if (/(down|left|right|back_space|return)/ig.test(key)) {
info("added event listener for down|left|right|back_space|return keys");
editor.input.addEventListener("keypress", function onKeypress() {
if (editor.input) {
editor.input.removeEventListener("keypress", onKeypress);
}
info("inside event listener");
checkState();
})
}
else {
editor.once("after-suggest", checkState);
}
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
}
function checkState() {
executeSoon(() => {
info("After keypress for state " + state);
let [key, completion, selStart, selEnd, popupOpen] = testData[state];
if (selEnd != -1) {
is(editor.input.value, completion,
"Correct value is autocompleted for state " + state);
is(editor.input.selectionStart, selStart,
"Selection is starting at the right location for state " + state);
is(editor.input.selectionEnd, selEnd,
"Selection is ending at the right location for state " + state);
if (popupOpen) {
ok(editor.popup._panel.state == "open" ||
editor.popup._panel.state == "showing",
"Popup is open for state " + state);
}
else {
ok(editor.popup._panel.state != "open" &&
editor.popup._panel.state != "showing",
"Popup is closed for state " + state);
}
}
else {
let editor = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
let attr = editor.attrs["style"].querySelector(".editable");
is(attr.textContent, completion,
"Correct value is persisted after pressing Enter for state " + state);
}
checkStateAndMoveOn(state + 1);
});
}
// Create the helper tab for parsing...
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
doc = content.document;
waitForFocus(setupTest, content);
}, true);
content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
function setupTest() {
var target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
inspector = toolbox.getCurrentPanel();
startTests();
});
}
function finishUp() {
while (markup.undo.canUndo()) {
markup.undo.undo();
}
doc = inspector = null;
gBrowser.removeCurrentTab();
finish();
}
}

View File

@@ -759,7 +759,13 @@ InplaceEditor.prototype = {
} else { } else {
this.popup.selectNextItem(); this.popup.selectNextItem();
} }
this.input.value = this.popup.selectedItem.label; let input = this.input;
let pre = input.value.slice(0, input.selectionStart);
let post = input.value.slice(input.selectionEnd, input.value.length);
let item = this.popup.selectedItem;
let toComplete = item.label.slice(item.preLabel.length);
input.value = pre + toComplete + post;
input.setSelectionRange(pre.length, pre.length + toComplete.length);
this._updateSize(); this._updateSize();
// This emit is mainly for the purpose of making the test flow simpler. // This emit is mainly for the purpose of making the test flow simpler.
this.emit("after-suggest"); this.emit("after-suggest");
@@ -898,6 +904,7 @@ InplaceEditor.prototype = {
return; return;
} }
let query = input.value.slice(0, input.selectionStart); let query = input.value.slice(0, input.selectionStart);
let startCheckQuery = query;
if (!query) { if (!query) {
return; return;
} }
@@ -906,28 +913,52 @@ InplaceEditor.prototype = {
if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) { if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
list = CSSPropertyList; list = CSSPropertyList;
} else if (this.contentType == CONTENT_TYPES.CSS_VALUE) { } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
list = domUtils.getCSSValuesForProperty(this.property.name).sort(); list = domUtils.getCSSValuesForProperty(this.property.name);
} else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
/^\s*style\s*=/.test(query)) {
// Detecting if cursor is at property or value;
let match = query.match(/([:;"'=]?)\s*([^"';:= ]+)$/);
if (match && match.length == 3) {
if (match[1] == ":") { // We are in CSS value completion
let propertyName =
query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:= ]+$/)[1];
list = domUtils.getCSSValuesForProperty(propertyName);
startCheckQuery = match[2];
} else if (match[1]) { // We are in CSS property name completion
list = CSSPropertyList;
startCheckQuery = match[2];
}
if (!startCheckQuery) {
// This emit is mainly to make the test flow simpler.
this.emit("after-suggest", "nothing to autocomplete");
return;
}
}
} }
list.some(item => { list.some(item => {
if (item.startsWith(query)) { if (item.startsWith(startCheckQuery)) {
input.value = item; input.value = query + item.slice(startCheckQuery.length) +
input.setSelectionRange(query.length, item.length); input.value.slice(query.length);
input.setSelectionRange(query.length, query.length + item.length -
startCheckQuery.length);
this._updateSize(); this._updateSize();
return true; return true;
} }
}); });
if (!this.popup) { if (!this.popup) {
// This emit is mainly to make the test flow simpler.
this.emit("after-suggest", "no popup");
return; return;
} }
let finalList = []; let finalList = [];
let length = list.length; let length = list.length;
for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) { for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
if (list[i].startsWith(query)) { if (list[i].startsWith(startCheckQuery)) {
count++; count++;
finalList.push({ finalList.push({
preLabel: query, preLabel: startCheckQuery,
label: list[i] label: list[i]
}); });
} }
@@ -936,7 +967,7 @@ InplaceEditor.prototype = {
// which would have started with query, assuming that list is sorted. // which would have started with query, assuming that list is sorted.
break; break;
} }
else if (list[i][0] > query[0]) { else if (list[i][0] > startCheckQuery[0]) {
// We have crossed all possible matches alphabetically. // We have crossed all possible matches alphabetically.
break; break;
} }

View File

@@ -39,11 +39,6 @@ let testData = [
["VK_UP", "direction", 0, 3], ["VK_UP", "direction", 0, 3],
["VK_UP", "dominant-baseline", 2, 3], ["VK_UP", "dominant-baseline", 2, 3],
["VK_UP", "display", 1, 3], ["VK_UP", "display", 1, 3],
["VK_BACK_SPACE", "displa", -1, 0],
["VK_BACK_SPACE", "displ", -1, 0],
["VK_BACK_SPACE", "disp", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0], ["VK_BACK_SPACE", "d", -1, 0],
["i", "direction", 0, 2], ["i", "direction", 0, 2],
["s", "display", -1, 0], ["s", "display", -1, 0],

View File

@@ -27,11 +27,6 @@ let testData = [
["VK_UP", "direction", 0, 3], ["VK_UP", "direction", 0, 3],
["VK_UP", "dominant-baseline", 2, 3], ["VK_UP", "dominant-baseline", 2, 3],
["VK_UP", "display", 1, 3], ["VK_UP", "display", 1, 3],
["VK_BACK_SPACE", "displa", -1, 0],
["VK_BACK_SPACE", "displ", -1, 0],
["VK_BACK_SPACE", "disp", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0], ["VK_BACK_SPACE", "d", -1, 0],
["i", "direction", 0, 2], ["i", "direction", 0, 2],
["s", "display", -1, 0], ["s", "display", -1, 0],