Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit 994daeb

Browse files
committed
Merge pull request #6916 from adobe/randy/display-inline-editor-errors
Display Inline Editor Errors
2 parents 9151c63 + c1f7ebd commit 994daeb

11 files changed

Lines changed: 494 additions & 48 deletions

File tree

src/editor/CSSInlineEditor.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ define(function (require, exports, module) {
5454
* Given a position in an HTML editor, returns the relevant selector for the attribute/tag
5555
* surrounding that position, or "" if none is found.
5656
* @param {!Editor} editor
57+
* @param {!{line:Number, ch:Number}} pos
58+
* @return {selectorName: {string}, reason: {string}}
5759
* @private
5860
*/
5961
function _getSelectorName(editor, pos) {
6062
var tagInfo = HTMLUtils.getTagInfo(editor, pos),
61-
selectorName = "";
63+
selectorName = "",
64+
reason;
6265

6366
if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME || tagInfo.position.tokenType === HTMLUtils.CLOSING_TAG) {
6467
// Type selector
@@ -85,16 +88,27 @@ define(function (require, exports, module) {
8588
if (selectorName === ".") {
8689
selectorName = "";
8790
}
91+
92+
if (selectorName === "") {
93+
reason = Strings.ERROR_CSSQUICKEDIT_CLASSNOTFOUND;
94+
}
8895
} else if (tagInfo.attr.name === "id") {
8996
// ID selector
9097
var trimmedVal = tagInfo.attr.value.trim();
9198
if (trimmedVal) {
9299
selectorName = "#" + trimmedVal;
100+
} else {
101+
reason = Strings.ERROR_CSSQUICKEDIT_IDNOTFOUND;
93102
}
103+
} else {
104+
reason = Strings.ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR;
94105
}
95106
}
96107

97-
return selectorName;
108+
return {
109+
selectorName: selectorName,
110+
reason: reason
111+
};
98112
}
99113

100114
/**
@@ -146,8 +160,9 @@ define(function (require, exports, module) {
146160
*
147161
* @param {!Editor} editor
148162
* @param {!{line:Number, ch:Number}} pos
149-
* @return {$.Promise} a promise that will be resolved with an InlineWidget
150-
* or null if we're not going to provide anything.
163+
* @return {?$.Promise} synchronously resolved with an InlineWidget, or
164+
* {string} if pos is in tag but not in tag name, class attr, or id attr, or
165+
* null if we're not going to provide anything.
151166
*/
152167
function htmlToCSSProvider(hostEditor, pos) {
153168

@@ -164,10 +179,12 @@ define(function (require, exports, module) {
164179

165180
// Always use the selection start for determining selector name. The pos
166181
// parameter is usually the selection end.
167-
var selectorName = _getSelectorName(hostEditor, sel.start);
168-
if (selectorName === "") {
169-
return null;
182+
var selectorResult = _getSelectorName(hostEditor, sel.start);
183+
if (selectorResult.selectorName === "") {
184+
return selectorResult.reason || null;
170185
}
186+
187+
var selectorName = selectorResult.selectorName;
171188

172189
var result = new $.Deferred(),
173190
cssInlineEditor,

src/editor/Editor.js

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ define(function (require, exports, module) {
6969
CodeMirror = require("thirdparty/CodeMirror2/lib/codemirror"),
7070
Menus = require("command/Menus"),
7171
PerfUtils = require("utils/PerfUtils"),
72+
PopUpManager = require("widgets/PopUpManager"),
7273
PreferencesManager = require("preferences/PreferencesManager"),
7374
Strings = require("strings"),
7475
TextRange = require("document/TextRange").TextRange,
@@ -198,6 +199,7 @@ define(function (require, exports, module) {
198199
// (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized)
199200

200201
this._inlineWidgets = [];
202+
this._$messagePopover = null;
201203

202204
// Editor supplies some standard keyboard behavior extensions of its own
203205
var codeMirrorKeyMap = {
@@ -811,7 +813,7 @@ define(function (require, exports, module) {
811813

812814
this._codeMirror.on("blur", function () {
813815
self._focused = false;
814-
// EditorManager only cares about other Editors gaining focus, so we don't notify it of anything here
816+
$(self).triggerHandler("blur", [self]);
815817
});
816818

817819
this._codeMirror.on("update", function (instance) {
@@ -1518,6 +1520,126 @@ define(function (require, exports, module) {
15181520
return this._inlineWidgets;
15191521
};
15201522

1523+
/**
1524+
* Display temporary popover message at current cursor position. Display message above
1525+
* cursor if space allows, otherwise below.
1526+
*
1527+
* @param {string} errorMsg Error message to display
1528+
*/
1529+
Editor.prototype.displayErrorMessageAtCursor = function (errorMsg) {
1530+
var arrowBelow, cursorPos, cursorCoord, popoverRect,
1531+
top, left, clip, arrowLeft,
1532+
self = this,
1533+
$editorHolder = $("#editor-holder"),
1534+
POPOVER_MARGIN = 10,
1535+
POPOVER_ARROW_HALF_WIDTH = 10;
1536+
1537+
function _removeListeners() {
1538+
$(self).off(".msgbox");
1539+
}
1540+
1541+
// PopUpManager.removePopUp() callback
1542+
function _clearMessagePopover() {
1543+
if (self._$messagePopover && self._$messagePopover.length > 0) {
1544+
// self._$messagePopover.remove() is done by PopUpManager
1545+
self._$messagePopover = null;
1546+
}
1547+
_removeListeners();
1548+
}
1549+
1550+
// PopUpManager.removePopUp() is called either directly by this closure, or by
1551+
// PopUpManager as a result of another popup being invoked.
1552+
function _removeMessagePopover() {
1553+
PopUpManager.removePopUp(self._$messagePopover);
1554+
}
1555+
1556+
function _addListeners() {
1557+
$(self)
1558+
.on("blur.msgbox", _removeMessagePopover)
1559+
.on("change.msgbox", _removeMessagePopover)
1560+
.on("cursorActivity.msgbox", _removeMessagePopover)
1561+
.on("update.msgbox", _removeMessagePopover);
1562+
}
1563+
1564+
// Only 1 message at a time
1565+
if (this._$messagePopover) {
1566+
_removeMessagePopover();
1567+
}
1568+
1569+
// Make sure cursor is in view
1570+
cursorPos = this.getCursorPos();
1571+
this._codeMirror.scrollIntoView(cursorPos);
1572+
1573+
// Determine if arrow is above or below
1574+
cursorCoord = this._codeMirror.charCoords(cursorPos);
1575+
1576+
// Assume popover height is max of 2 lines
1577+
arrowBelow = (cursorCoord.top > 100);
1578+
1579+
// Text is dynamic, so build popover first so we can measure final width
1580+
this._$messagePopover = $("<div/>").addClass("popover-message").appendTo($("body"));
1581+
if (!arrowBelow) {
1582+
$("<div/>").addClass("arrowAbove").appendTo(this._$messagePopover);
1583+
}
1584+
$("<div/>").addClass("text").appendTo(this._$messagePopover).html(errorMsg);
1585+
if (arrowBelow) {
1586+
$("<div/>").addClass("arrowBelow").appendTo(this._$messagePopover);
1587+
}
1588+
1589+
// Estimate where to position popover.
1590+
top = (arrowBelow) ? cursorCoord.top - this._$messagePopover.height() - POPOVER_MARGIN
1591+
: cursorCoord.bottom + POPOVER_MARGIN;
1592+
left = cursorCoord.left - (this._$messagePopover.width() / 2);
1593+
1594+
popoverRect = {
1595+
top: top,
1596+
left: left,
1597+
height: this._$messagePopover.height(),
1598+
width: this._$messagePopover.width()
1599+
};
1600+
1601+
// See if popover is clipped on any side
1602+
clip = ViewUtils.getElementClipSize($editorHolder, popoverRect);
1603+
1604+
// Prevent horizontal clipping
1605+
if (clip.left > 0) {
1606+
left += clip.left;
1607+
} else if (clip.right > 0) {
1608+
left -= clip.right;
1609+
}
1610+
1611+
// Popover text and arrow are positioned individually
1612+
this._$messagePopover.css({"top": top, "left": left});
1613+
1614+
// Position popover arrow exactly centered over/under cursor
1615+
arrowLeft = cursorCoord.left - left - POPOVER_ARROW_HALF_WIDTH;
1616+
if (arrowBelow) {
1617+
this._$messagePopover.find(".arrowBelow").css({"margin-left": arrowLeft});
1618+
} else {
1619+
this._$messagePopover.find(".arrowAbove").css({"margin-left": arrowLeft});
1620+
}
1621+
1622+
// Add listeners
1623+
PopUpManager.addPopUp(this._$messagePopover, _clearMessagePopover, true);
1624+
_addListeners();
1625+
1626+
// Animate open
1627+
AnimationUtils.animateUsingClass(this._$messagePopover[0], "animateOpen").done(function () {
1628+
// Make sure we still have a popover
1629+
if (self._$messagePopover && self._$messagePopover.length > 0) {
1630+
self._$messagePopover.addClass("open");
1631+
1632+
// Don't add scroll listeners until open so we don't get event
1633+
// from scrolling cursor into view
1634+
$(self).on("scroll.msgbox", _removeMessagePopover);
1635+
1636+
// Animate closed -- which includes delay to show message
1637+
AnimationUtils.animateUsingClass(self._$messagePopover[0], "animateClose")
1638+
.done(_removeMessagePopover);
1639+
}
1640+
});
1641+
};
1642+
15211643
/**
15221644
* Returns the offset of the top of the virtual scroll area relative to the browser window (not the editor
15231645
* itself). Mainly useful for calculations related to scrollIntoView(), where you're starting with the

src/editor/EditorManager.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,24 +160,48 @@ define(function (require, exports, module) {
160160
* @private
161161
* Finds an inline widget provider from the given list that can offer a widget for the current cursor
162162
* position, and once the widget has been created inserts it into the editor.
163+
*
163164
* @param {!Editor} editor The host editor
164-
* @param {!Array.<{priority:number, provider:function(!Editor, !{line:number, ch:number}):?$.Promise}>} prioritized providers
165+
* @param {Array.<{priority:number, provider:function(...)}>} providers
166+
* prioritized list of providers
167+
* @param {string=} defaultErrorMsg Default error message to display if no initial provider found
165168
* @return {$.Promise} a promise that will be resolved when an InlineWidget
166169
* is created or rejected if no inline providers have offered one.
167170
*/
168-
function _openInlineWidget(editor, providers) {
171+
function _openInlineWidget(editor, providers, defaultErrorMsg) {
169172
PerfUtils.markStart(PerfUtils.INLINE_WIDGET_OPEN);
170173

171174
// Run through inline-editor providers until one responds
172175
var pos = editor.getCursorPos(),
173176
inlinePromise,
174177
i,
175-
result = new $.Deferred();
178+
result = new $.Deferred(),
179+
errorMsg,
180+
providerRet;
176181

182+
// Query each provider in priority order. Provider may return:
183+
// 1. `null` to indicate it does not apply to current cursor position
184+
// 2. promise that should resolve to an InlineWidget
185+
// 3. string which indicates provider does apply to current cursor position,
186+
// but reason it could not create InlineWidget
187+
//
188+
// Keep looping until a provider is found. If a provider is not found,
189+
// display highest priority error message that was found, otherwise display
190+
// default error message
177191
for (i = 0; i < providers.length && !inlinePromise; i++) {
178192
var provider = providers[i].provider;
179-
inlinePromise = provider(editor, pos);
193+
providerRet = provider(editor, pos);
194+
if (providerRet) {
195+
if (providerRet.hasOwnProperty("done")) {
196+
inlinePromise = providerRet;
197+
} else if (!errorMsg && typeof (providerRet) === "string") {
198+
errorMsg = providerRet;
199+
}
200+
}
180201
}
202+
203+
// Use default error message if none other provided
204+
errorMsg = errorMsg || defaultErrorMsg;
181205

182206
// If one of them will provide a widget, show it inline once ready
183207
if (inlinePromise) {
@@ -189,11 +213,13 @@ define(function (require, exports, module) {
189213
}).fail(function () {
190214
// terminate timer that was started above
191215
PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
216+
editor.displayErrorMessageAtCursor(errorMsg);
192217
result.reject();
193218
});
194219
} else {
195220
// terminate timer that was started above
196221
PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
222+
editor.displayErrorMessageAtCursor(errorMsg);
197223
result.reject();
198224
}
199225

@@ -918,12 +944,14 @@ define(function (require, exports, module) {
918944
/**
919945
* Closes any focused inline widget. Else, asynchronously asks providers to create one.
920946
*
921-
* @param {Array.<{priority:number, provider:function(...)}>} prioritized providers
947+
* @param {Array.<{priority:number, provider:function(...)}>} providers
948+
* prioritized list of providers
949+
* @param {string=} errorMsg Error message to display if no initial provider found
922950
* @return {!Promise} A promise resolved with true if an inline widget is opened or false
923951
* when closed. Rejected if there is neither an existing widget to close nor a provider
924952
* willing to create a widget (or if no editor is open).
925953
*/
926-
function _toggleInlineWidget(providers) {
954+
function _toggleInlineWidget(providers, errorMsg) {
927955
var result = new $.Deferred();
928956

929957
if (_currentEditor) {
@@ -939,7 +967,7 @@ define(function (require, exports, module) {
939967
});
940968
} else {
941969
// main editor has focus, so create an inline editor
942-
_openInlineWidget(_currentEditor, providers).done(function () {
970+
_openInlineWidget(_currentEditor, providers, errorMsg).done(function () {
943971
result.resolve(true);
944972
}).fail(function () {
945973
result.reject();
@@ -1008,10 +1036,10 @@ define(function (require, exports, module) {
10081036

10091037
// Initialize: command handlers
10101038
CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, function () {
1011-
return _toggleInlineWidget(_inlineEditProviders);
1039+
return _toggleInlineWidget(_inlineEditProviders, Strings.ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND);
10121040
});
10131041
CommandManager.register(Strings.CMD_TOGGLE_QUICK_DOCS, Commands.TOGGLE_QUICK_DOCS, function () {
1014-
return _toggleInlineWidget(_inlineDocsProviders);
1042+
return _toggleInlineWidget(_inlineDocsProviders, Strings.ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND);
10151043
});
10161044
CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, Commands.NAVIGATE_JUMPTO_DEFINITION, _doJumpToDef);
10171045

src/extensions/default/InlineTimingFunctionEditor/main.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,28 @@ define(function (require, exports, module) {
6565
*
6666
* @param {Editor} hostEditor
6767
* @param {{line:Number, ch:Number}} pos
68-
* @return {?{color:String, start:TextMarker, end:TextMarker}}
68+
* @return {timingFunction:{?string}, reason:{?string}, start:{?TextMarker}, end:{?TextMarker}}
6969
*/
7070
function prepareEditorForProvider(hostEditor, pos) {
7171
var cursorLine, sel, startPos, endPos, startBookmark, endBookmark, currentMatch,
7272
cm = hostEditor._codeMirror;
7373

7474
sel = hostEditor.getSelection();
7575
if (sel.start.line !== sel.end.line) {
76-
return null;
76+
return {timingFunction: null, reason: null};
7777
}
7878

7979
cursorLine = hostEditor.document.getLine(pos.line);
8080

8181
// code runs several matches complicated patterns, multiple times, so
8282
// first do a quick, simple check to see make sure we may have a match
8383
if (!cursorLine.match(/cubic-bezier|linear|ease|step/)) {
84-
return null;
84+
return {timingFunction: null, reason: null};
8585
}
8686

8787
currentMatch = TimingFunctionUtils.timingFunctionMatch(cursorLine, false);
8888
if (!currentMatch) {
89-
return null;
89+
return {timingFunction: null, reason: Strings.ERROR_TIMINGQUICKEDIT_INVALIDSYNTAX};
9090
}
9191

9292
// check for subsequent matches, and use first match after pos
@@ -129,16 +129,17 @@ define(function (require, exports, module) {
129129
*
130130
* @param {!Editor} hostEditor
131131
* @param {!{line:Number, ch:Number}} pos
132-
* @return {?$.Promise} synchronously resolved with an InlineWidget, or null if there's
133-
* no color at pos.
132+
* @return {?$.Promise} synchronously resolved with an InlineWidget, or
133+
* {string} if timing function with invalid syntax is detected at pos, or
134+
* null if there's no timing function at pos.
134135
*/
135136
function inlineTimingFunctionEditorProvider(hostEditor, pos) {
136137
var context = prepareEditorForProvider(hostEditor, pos),
137138
inlineTimingFunctionEditor,
138139
result;
139140

140-
if (!context) {
141-
return null;
141+
if (!context.timingFunction) {
142+
return context.reason || null;
142143
} else {
143144
inlineTimingFunctionEditor = new InlineTimingFunctionEditor(context.timingFunction, context.start, context.end);
144145
inlineTimingFunctionEditor.load(hostEditor);
@@ -162,8 +163,6 @@ define(function (require, exports, module) {
162163

163164
init();
164165

165-
// for use by other InlineColorEditors
166-
exports.prepareEditorForProvider = prepareEditorForProvider;
167166

168167
// for unit tests only
169168
exports.inlineTimingFunctionEditorProvider = inlineTimingFunctionEditorProvider;

0 commit comments

Comments
 (0)