Bug 896181 - Autocomplete CSS properties and values in markup view, r=mratcliffe
This commit is contained in:
@@ -20,6 +20,10 @@ let promise = require("sdk/core/promise");
|
||||
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Templater.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:
|
||||
@@ -53,6 +57,14 @@ function MarkupView(aInspector, aFrame, aControllerWindow)
|
||||
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.installController(aControllerWindow);
|
||||
|
||||
@@ -676,6 +688,9 @@ MarkupView.prototype = {
|
||||
this.undo.destroy();
|
||||
delete this.undo;
|
||||
|
||||
this.popup.destroy();
|
||||
delete this.popup;
|
||||
|
||||
this._frame.removeEventListener("focus", this._boundFocus, false);
|
||||
delete this._boundFocus;
|
||||
|
||||
@@ -1130,6 +1145,8 @@ function ElementEditor(aContainer, aNode)
|
||||
element: this.newAttr,
|
||||
trigger: "dblclick",
|
||||
stopOnReturn: true,
|
||||
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
|
||||
popup: this.markup.popup,
|
||||
done: (aVal, aCommit) => {
|
||||
if (!aCommit) {
|
||||
return;
|
||||
@@ -1222,6 +1239,8 @@ ElementEditor.prototype = {
|
||||
trigger: "dblclick",
|
||||
stopOnReturn: true,
|
||||
selectAll: false,
|
||||
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
|
||||
popup: this.markup.popup,
|
||||
start: (aEditor, aEvent) => {
|
||||
// If the editing was started inside the name or value areas,
|
||||
// select accordingly.
|
||||
|
||||
@@ -19,6 +19,7 @@ MOCHITEST_BROWSER_FILES := \
|
||||
browser_inspector_markup_edit.js \
|
||||
browser_inspector_markup_subset.html \
|
||||
browser_inspector_markup_subset.js \
|
||||
browser_bug896181_css_mixed_completion_new_attribute.js \
|
||||
head.js \
|
||||
$(NULL)
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -759,7 +759,13 @@ InplaceEditor.prototype = {
|
||||
} else {
|
||||
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 emit is mainly for the purpose of making the test flow simpler.
|
||||
this.emit("after-suggest");
|
||||
@@ -898,6 +904,7 @@ InplaceEditor.prototype = {
|
||||
return;
|
||||
}
|
||||
let query = input.value.slice(0, input.selectionStart);
|
||||
let startCheckQuery = query;
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
@@ -906,28 +913,52 @@ InplaceEditor.prototype = {
|
||||
if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
|
||||
list = CSSPropertyList;
|
||||
} 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 => {
|
||||
if (item.startsWith(query)) {
|
||||
input.value = item;
|
||||
input.setSelectionRange(query.length, item.length);
|
||||
if (item.startsWith(startCheckQuery)) {
|
||||
input.value = query + item.slice(startCheckQuery.length) +
|
||||
input.value.slice(query.length);
|
||||
input.setSelectionRange(query.length, query.length + item.length -
|
||||
startCheckQuery.length);
|
||||
this._updateSize();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.popup) {
|
||||
// This emit is mainly to make the test flow simpler.
|
||||
this.emit("after-suggest", "no popup");
|
||||
return;
|
||||
}
|
||||
let finalList = [];
|
||||
let length = list.length;
|
||||
for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
|
||||
if (list[i].startsWith(query)) {
|
||||
if (list[i].startsWith(startCheckQuery)) {
|
||||
count++;
|
||||
finalList.push({
|
||||
preLabel: query,
|
||||
preLabel: startCheckQuery,
|
||||
label: list[i]
|
||||
});
|
||||
}
|
||||
@@ -936,7 +967,7 @@ InplaceEditor.prototype = {
|
||||
// which would have started with query, assuming that list is sorted.
|
||||
break;
|
||||
}
|
||||
else if (list[i][0] > query[0]) {
|
||||
else if (list[i][0] > startCheckQuery[0]) {
|
||||
// We have crossed all possible matches alphabetically.
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,6 @@ let testData = [
|
||||
["VK_UP", "direction", 0, 3],
|
||||
["VK_UP", "dominant-baseline", 2, 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],
|
||||
["i", "direction", 0, 2],
|
||||
["s", "display", -1, 0],
|
||||
|
||||
@@ -27,11 +27,6 @@ let testData = [
|
||||
["VK_UP", "direction", 0, 3],
|
||||
["VK_UP", "dominant-baseline", 2, 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],
|
||||
["i", "direction", 0, 2],
|
||||
["s", "display", -1, 0],
|
||||
|
||||
Reference in New Issue
Block a user