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:///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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
} 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user