diff --git a/src/editor/CSSInlineEditor.js b/src/editor/CSSInlineEditor.js index 22bfe1af8f5..bc8411da2fb 100644 --- a/src/editor/CSSInlineEditor.js +++ b/src/editor/CSSInlineEditor.js @@ -54,11 +54,14 @@ define(function (require, exports, module) { * Given a position in an HTML editor, returns the relevant selector for the attribute/tag * surrounding that position, or "" if none is found. * @param {!Editor} editor + * @param {!{line:Number, ch:Number}} pos + * @return {selectorName: {string}, reason: {string}} * @private */ function _getSelectorName(editor, pos) { var tagInfo = HTMLUtils.getTagInfo(editor, pos), - selectorName = ""; + selectorName = "", + reason; if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME || tagInfo.position.tokenType === HTMLUtils.CLOSING_TAG) { // Type selector @@ -85,16 +88,27 @@ define(function (require, exports, module) { if (selectorName === ".") { selectorName = ""; } + + if (selectorName === "") { + reason = Strings.ERROR_CSSQUICKEDIT_CLASSNOTFOUND; + } } else if (tagInfo.attr.name === "id") { // ID selector var trimmedVal = tagInfo.attr.value.trim(); if (trimmedVal) { selectorName = "#" + trimmedVal; + } else { + reason = Strings.ERROR_CSSQUICKEDIT_IDNOTFOUND; } + } else { + reason = Strings.ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR; } } - return selectorName; + return { + selectorName: selectorName, + reason: reason + }; } /** @@ -146,8 +160,9 @@ define(function (require, exports, module) { * * @param {!Editor} editor * @param {!{line:Number, ch:Number}} pos - * @return {$.Promise} a promise that will be resolved with an InlineWidget - * or null if we're not going to provide anything. + * @return {?$.Promise} synchronously resolved with an InlineWidget, or + * {string} if pos is in tag but not in tag name, class attr, or id attr, or + * null if we're not going to provide anything. */ function htmlToCSSProvider(hostEditor, pos) { @@ -164,10 +179,12 @@ define(function (require, exports, module) { // Always use the selection start for determining selector name. The pos // parameter is usually the selection end. - var selectorName = _getSelectorName(hostEditor, sel.start); - if (selectorName === "") { - return null; + var selectorResult = _getSelectorName(hostEditor, sel.start); + if (selectorResult.selectorName === "") { + return selectorResult.reason || null; } + + var selectorName = selectorResult.selectorName; var result = new $.Deferred(), cssInlineEditor, diff --git a/src/editor/Editor.js b/src/editor/Editor.js index c1fdd9f1694..362f78815d9 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -67,6 +67,7 @@ define(function (require, exports, module) { var CodeMirror = require("thirdparty/CodeMirror2/lib/codemirror"), Menus = require("command/Menus"), PerfUtils = require("utils/PerfUtils"), + PopUpManager = require("widgets/PopUpManager"), PreferencesManager = require("preferences/PreferencesManager"), Strings = require("strings"), TextRange = require("document/TextRange").TextRange, @@ -193,6 +194,7 @@ define(function (require, exports, module) { // (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized) this._inlineWidgets = []; + this._$messagePopover = null; // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { @@ -805,7 +807,7 @@ define(function (require, exports, module) { this._codeMirror.on("blur", function () { self._focused = false; - // EditorManager only cares about other Editors gaining focus, so we don't notify it of anything here + $(self).triggerHandler("blur", [self]); }); this._codeMirror.on("update", function (instance) { @@ -1500,6 +1502,126 @@ define(function (require, exports, module) { return this._inlineWidgets; }; + /** + * Display temporary popover message at current cursor position. Display message above + * cursor if space allows, otherwise below. + * + * @param {string} errorMsg Error message to display + */ + Editor.prototype.displayErrorMessageAtCursor = function (errorMsg) { + var arrowBelow, cursorPos, cursorCoord, popoverRect, + top, left, clip, arrowLeft, + self = this, + $editorHolder = $("#editor-holder"), + POPOVER_MARGIN = 10, + POPOVER_ARROW_HALF_WIDTH = 10; + + function _removeListeners() { + $(self).off(".msgbox"); + } + + // PopUpManager.removePopUp() callback + function _clearMessagePopover() { + if (self._$messagePopover && self._$messagePopover.length > 0) { + // self._$messagePopover.remove() is done by PopUpManager + self._$messagePopover = null; + } + _removeListeners(); + } + + // PopUpManager.removePopUp() is called either directly by this closure, or by + // PopUpManager as a result of another popup being invoked. + function _removeMessagePopover() { + PopUpManager.removePopUp(self._$messagePopover); + } + + function _addListeners() { + $(self) + .on("blur.msgbox", _removeMessagePopover) + .on("change.msgbox", _removeMessagePopover) + .on("cursorActivity.msgbox", _removeMessagePopover) + .on("update.msgbox", _removeMessagePopover); + } + + // Only 1 message at a time + if (this._$messagePopover) { + _removeMessagePopover(); + } + + // Make sure cursor is in view + cursorPos = this.getCursorPos(); + this._codeMirror.scrollIntoView(cursorPos); + + // Determine if arrow is above or below + cursorCoord = this._codeMirror.charCoords(cursorPos); + + // Assume popover height is max of 2 lines + arrowBelow = (cursorCoord.top > 100); + + // Text is dynamic, so build popover first so we can measure final width + this._$messagePopover = $("
").addClass("popover-message").appendTo($("body")); + if (!arrowBelow) { + $("
").addClass("arrowAbove").appendTo(this._$messagePopover); + } + $("
").addClass("text").appendTo(this._$messagePopover).html(errorMsg); + if (arrowBelow) { + $("
").addClass("arrowBelow").appendTo(this._$messagePopover); + } + + // Estimate where to position popover. + top = (arrowBelow) ? cursorCoord.top - this._$messagePopover.height() - POPOVER_MARGIN + : cursorCoord.bottom + POPOVER_MARGIN; + left = cursorCoord.left - (this._$messagePopover.width() / 2); + + popoverRect = { + top: top, + left: left, + height: this._$messagePopover.height(), + width: this._$messagePopover.width() + }; + + // See if popover is clipped on any side + clip = ViewUtils.getElementClipSize($editorHolder, popoverRect); + + // Prevent horizontal clipping + if (clip.left > 0) { + left += clip.left; + } else if (clip.right > 0) { + left -= clip.right; + } + + // Popover text and arrow are positioned individually + this._$messagePopover.css({"top": top, "left": left}); + + // Position popover arrow exactly centered over/under cursor + arrowLeft = cursorCoord.left - left - POPOVER_ARROW_HALF_WIDTH; + if (arrowBelow) { + this._$messagePopover.find(".arrowBelow").css({"margin-left": arrowLeft}); + } else { + this._$messagePopover.find(".arrowAbove").css({"margin-left": arrowLeft}); + } + + // Add listeners + PopUpManager.addPopUp(this._$messagePopover, _clearMessagePopover, true); + _addListeners(); + + // Animate open + AnimationUtils.animateUsingClass(this._$messagePopover[0], "animateOpen").done(function () { + // Make sure we still have a popover + if (self._$messagePopover && self._$messagePopover.length > 0) { + self._$messagePopover.addClass("open"); + + // Don't add scroll listeners until open so we don't get event + // from scrolling cursor into view + $(self).on("scroll.msgbox", _removeMessagePopover); + + // Animate closed -- which includes delay to show message + AnimationUtils.animateUsingClass(self._$messagePopover[0], "animateClose") + .done(_removeMessagePopover); + } + }); + }; + /** * Returns the offset of the top of the virtual scroll area relative to the browser window (not the editor * itself). Mainly useful for calculations related to scrollIntoView(), where you're starting with the diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index a95c0ec1695..707ada93bc0 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -160,24 +160,48 @@ define(function (require, exports, module) { * @private * Finds an inline widget provider from the given list that can offer a widget for the current cursor * position, and once the widget has been created inserts it into the editor. + * * @param {!Editor} editor The host editor - * @param {!Array.<{priority:number, provider:function(!Editor, !{line:number, ch:number}):?$.Promise}>} prioritized providers + * @param {Array.<{priority:number, provider:function(...)}>} providers + * prioritized list of providers + * @param {string=} defaultErrorMsg Default error message to display if no initial provider found * @return {$.Promise} a promise that will be resolved when an InlineWidget * is created or rejected if no inline providers have offered one. */ - function _openInlineWidget(editor, providers) { + function _openInlineWidget(editor, providers, defaultErrorMsg) { PerfUtils.markStart(PerfUtils.INLINE_WIDGET_OPEN); // Run through inline-editor providers until one responds var pos = editor.getCursorPos(), inlinePromise, i, - result = new $.Deferred(); + result = new $.Deferred(), + errorMsg, + providerRet; + // Query each provider in priority order. Provider may return: + // 1. `null` to indicate it does not apply to current cursor position + // 2. promise that should resolve to an InlineWidget + // 3. string which indicates provider does apply to current cursor position, + // but reason it could not create InlineWidget + // + // Keep looping until a provider is found. If a provider is not found, + // display highest priority error message that was found, otherwise display + // default error message for (i = 0; i < providers.length && !inlinePromise; i++) { var provider = providers[i].provider; - inlinePromise = provider(editor, pos); + providerRet = provider(editor, pos); + if (providerRet) { + if (providerRet.hasOwnProperty("done")) { + inlinePromise = providerRet; + } else if (!errorMsg && typeof (providerRet) === "string") { + errorMsg = providerRet; + } + } } + + // Use default error message if none other provided + errorMsg = errorMsg || defaultErrorMsg; // If one of them will provide a widget, show it inline once ready if (inlinePromise) { @@ -189,11 +213,13 @@ define(function (require, exports, module) { }).fail(function () { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN); + editor.displayErrorMessageAtCursor(errorMsg); result.reject(); }); } else { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN); + editor.displayErrorMessageAtCursor(errorMsg); result.reject(); } @@ -916,12 +942,14 @@ define(function (require, exports, module) { /** * Closes any focused inline widget. Else, asynchronously asks providers to create one. * - * @param {Array.<{priority:number, provider:function(...)}>} prioritized providers + * @param {Array.<{priority:number, provider:function(...)}>} providers + * prioritized list of providers + * @param {string=} errorMsg Error message to display if no initial provider found * @return {!Promise} A promise resolved with true if an inline widget is opened or false * when closed. Rejected if there is neither an existing widget to close nor a provider * willing to create a widget (or if no editor is open). */ - function _toggleInlineWidget(providers) { + function _toggleInlineWidget(providers, errorMsg) { var result = new $.Deferred(); if (_currentEditor) { @@ -937,7 +965,7 @@ define(function (require, exports, module) { }); } else { // main editor has focus, so create an inline editor - _openInlineWidget(_currentEditor, providers).done(function () { + _openInlineWidget(_currentEditor, providers, errorMsg).done(function () { result.resolve(true); }).fail(function () { result.reject(); @@ -1005,10 +1033,10 @@ define(function (require, exports, module) { // Initialize: command handlers CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, function () { - return _toggleInlineWidget(_inlineEditProviders); + return _toggleInlineWidget(_inlineEditProviders, Strings.ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND); }); CommandManager.register(Strings.CMD_TOGGLE_QUICK_DOCS, Commands.TOGGLE_QUICK_DOCS, function () { - return _toggleInlineWidget(_inlineDocsProviders); + return _toggleInlineWidget(_inlineDocsProviders, Strings.ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND); }); CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, Commands.NAVIGATE_JUMPTO_DEFINITION, _doJumpToDef); diff --git a/src/extensions/default/InlineTimingFunctionEditor/main.js b/src/extensions/default/InlineTimingFunctionEditor/main.js index f57ba4a0919..ab43654fdff 100644 --- a/src/extensions/default/InlineTimingFunctionEditor/main.js +++ b/src/extensions/default/InlineTimingFunctionEditor/main.js @@ -65,7 +65,7 @@ define(function (require, exports, module) { * * @param {Editor} hostEditor * @param {{line:Number, ch:Number}} pos - * @return {?{color:String, start:TextMarker, end:TextMarker}} + * @return {timingFunction:{?string}, reason:{?string}, start:{?TextMarker}, end:{?TextMarker}} */ function prepareEditorForProvider(hostEditor, pos) { var cursorLine, sel, startPos, endPos, startBookmark, endBookmark, currentMatch, @@ -73,7 +73,7 @@ define(function (require, exports, module) { sel = hostEditor.getSelection(); if (sel.start.line !== sel.end.line) { - return null; + return {timingFunction: null, reason: null}; } cursorLine = hostEditor.document.getLine(pos.line); @@ -81,12 +81,12 @@ define(function (require, exports, module) { // code runs several matches complicated patterns, multiple times, so // first do a quick, simple check to see make sure we may have a match if (!cursorLine.match(/cubic-bezier|linear|ease|step/)) { - return null; + return {timingFunction: null, reason: null}; } currentMatch = TimingFunctionUtils.timingFunctionMatch(cursorLine, false); if (!currentMatch) { - return null; + return {timingFunction: null, reason: Strings.ERROR_TIMINGQUICKEDIT_INVALIDSYNTAX}; } // check for subsequent matches, and use first match after pos @@ -129,16 +129,17 @@ define(function (require, exports, module) { * * @param {!Editor} hostEditor * @param {!{line:Number, ch:Number}} pos - * @return {?$.Promise} synchronously resolved with an InlineWidget, or null if there's - * no color at pos. + * @return {?$.Promise} synchronously resolved with an InlineWidget, or + * {string} if timing function with invalid syntax is detected at pos, or + * null if there's no timing function at pos. */ function inlineTimingFunctionEditorProvider(hostEditor, pos) { var context = prepareEditorForProvider(hostEditor, pos), inlineTimingFunctionEditor, result; - if (!context) { - return null; + if (!context.timingFunction) { + return context.reason || null; } else { inlineTimingFunctionEditor = new InlineTimingFunctionEditor(context.timingFunction, context.start, context.end); inlineTimingFunctionEditor.load(hostEditor); @@ -162,8 +163,6 @@ define(function (require, exports, module) { init(); - // for use by other InlineColorEditors - exports.prepareEditorForProvider = prepareEditorForProvider; // for unit tests only exports.inlineTimingFunctionEditorProvider = inlineTimingFunctionEditorProvider; diff --git a/src/extensions/default/JavaScriptQuickEdit/main.js b/src/extensions/default/JavaScriptQuickEdit/main.js index 0634c8bc6c5..f93bd45e0dd 100644 --- a/src/extensions/default/JavaScriptQuickEdit/main.js +++ b/src/extensions/default/JavaScriptQuickEdit/main.js @@ -34,14 +34,15 @@ define(function (require, exports, module) { DocumentManager = brackets.getModule("document/DocumentManager"), JSUtils = brackets.getModule("language/JSUtils"), PerfUtils = brackets.getModule("utils/PerfUtils"), - ProjectManager = brackets.getModule("project/ProjectManager"); + ProjectManager = brackets.getModule("project/ProjectManager"), + Strings = brackets.getModule("strings"); /** * Return the token string that is at the specified position. * * @param hostEditor {!Editor} editor * @param {!{line:Number, ch:Number}} pos - * @return {String} token string at the specified position + * @return {functionName: {string}, reason: {string}} */ function _getFunctionName(hostEditor, pos) { var token = hostEditor._codeMirror.getTokenAt(pos, true); @@ -56,10 +57,16 @@ define(function (require, exports, module) { if (!((token.type === "variable") || (token.type === "variable-2") || (token.type === "property"))) { - return null; + return { + functionName: null, + reason: Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND + }; } - return token.string; + return { + functionName: token.string, + reason: null + }; } /** @@ -100,8 +107,9 @@ define(function (require, exports, module) { * * @param {!Editor} hostEditor * @param {!string} functionName - * @return {$.Promise} a promise that will be resolved with an InlineWidget - * or null if we're not going to provide anything. + * @return {?$.Promise} synchronously resolved with an InlineWidget, or + * {string} if js other than function is detected at pos, or + * null if we're not going to provide anything. */ function _createInlineEditor(hostEditor, functionName) { // Use Tern jump-to-definition helper, if it's available, to find InlineEditor target. @@ -196,12 +204,12 @@ define(function (require, exports, module) { // Always use the selection start for determining the function name. The pos // parameter is usually the selection end. - var functionName = _getFunctionName(hostEditor, sel.start); - if (!functionName) { - return null; + var functionResult = _getFunctionName(hostEditor, sel.start); + if (!functionResult.functionName) { + return functionResult.reason || null; } - return _createInlineEditor(hostEditor, functionName); + return _createInlineEditor(hostEditor, functionResult.functionName); } // init diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index ef6d3213d29..9d750437909 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -180,6 +180,17 @@ define({ "FILE_FILTER_CLIPPED_SUFFIX" : "and {0} more", + // Quick Edit + "ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND" : "No Quick Edit provider found for current cursor position", + "ERROR_CSSQUICKEDIT_CLASSNOTFOUND" : "CSS Quick Edit: place cursor in class name", + "ERROR_CSSQUICKEDIT_IDNOTFOUND" : "CSS Quick Edit: place cursor in id name", + "ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR" : "CSS Quick Edit: place cursor in tag name, class name, or id name", + "ERROR_TIMINGQUICKEDIT_INVALIDSYNTAX" : "CSS Timing Function Quick Edit: invalid syntax", + "ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND" : "JS Quick Edit: place cursor in function name", + + // Quick Docs + "ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND" : "No Quick Docs provider found for current cursor position", + /** * ProjectManager */ diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 41ae3cc550c..4893cd9d647 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -1098,7 +1098,7 @@ a, img { top: 24px; min-width: 291px + 2px; // to align with search field above it - background-color: #f74687; + background-color: @bc-error; color: @bc-white; font-size: 12px; padding: 3px 8px; @@ -1437,3 +1437,56 @@ label input { .install-extension-dialog .spinner { margin-left: 10px; } + +/* Quick Edit, Quick Docs */ +.popover-message { + position: absolute; + top: 0; + left: 0; + z-index: @z-index-brackets-inline-editor-error; + opacity: 1.0; + + -webkit-transform: scale(0); + transform: scale(0); + + .text { + padding: 5px 10px; + background-color: @bc-error; + border-radius: 3px; + color: @bc-white; + box-shadow: @tc-dropdown-shadow; + white-space: nowrap; + } + .arrowAbove { + height: 0; + width: 0; + border-width: 0 10px 10px 10px; + border-color: transparent transparent @bc-error transparent; + border-style: solid; + } + .arrowBelow { + height: 0; + width: 0; + border-width: 10px 10px 0 10px; + border-color: @bc-error transparent transparent transparent; + border-style: solid; + } + &.animateOpen { + -webkit-transition: -webkit-transform 125ms; + transition: transform 125ms; + -webkit-transform: scale(1); + transform: scale(1); + } + &.animateClose { + // Make the animation use the GPU--especially important for retina. + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-transition: opacity 500ms 5s ease-in, -webkit-transform 500ms 5s; + transition: opacity 500ms 5s ease-in, transform 500ms 5s; + opacity: 0.0; + } + &.open { + -webkit-transform: scale(1); + transform: scale(1); + } +} diff --git a/src/styles/brackets_colors.less b/src/styles/brackets_colors.less index 194c4a7c8f4..f4cc78253e9 100644 --- a/src/styles/brackets_colors.less +++ b/src/styles/brackets_colors.less @@ -65,6 +65,8 @@ @bc-cyan: #2aa198; @bc-green: #859900; +@bc-error: #f74687; + // TopCoat colors and styles, putting them here for now; let me know if there's a better place for these. @tc-icon-down: 0.5; diff --git a/src/styles/brackets_variables.less b/src/styles/brackets_variables.less index 42776246aec..724e83ec38d 100644 --- a/src/styles/brackets_variables.less +++ b/src/styles/brackets_variables.less @@ -58,3 +58,4 @@ @z-index-brackets-context-menu-base: 1000; @z-index-brackets-stylesheet-menu: 1000; +@z-index-brackets-inline-editor-error: 1000; diff --git a/test/spec/InlineEditorProviders-test-files/test1.html b/test/spec/InlineEditorProviders-test-files/test1.html index 3fdad04e694..3a6d3370c24 100644 --- a/test/spec/InlineEditorProviders-test-files/test1.html +++ b/test/spec/InlineEditorProviders-test-files/test1.html @@ -14,6 +14,7 @@
-
+
+
This is a long line of text so that cursor position at end of line will have to be scrolled into view
{{13}} diff --git a/test/spec/InlineEditorProviders-test.js b/test/spec/InlineEditorProviders-test.js index ecb7a2fcff9..b7e5aed434a 100644 --- a/test/spec/InlineEditorProviders-test.js +++ b/test/spec/InlineEditorProviders-test.js @@ -28,7 +28,8 @@ define(function (require, exports, module) { 'use strict'; - var Commands, // loaded from brackets.test + var CommandManager, // loaded from brackets.test + Commands, // loaded from brackets.test EditorManager, // loaded from brackets.test FileSyncManager, // loaded from brackets.test DocumentManager, // loaded from brackets.test @@ -37,7 +38,8 @@ define(function (require, exports, module) { Dialogs = require("widgets/Dialogs"), KeyEvent = require("utils/KeyEvent"), FileUtils = require("file/FileUtils"), - SpecRunnerUtils = require("spec/SpecRunnerUtils"); + SpecRunnerUtils = require("spec/SpecRunnerUtils"), + Strings = require("strings"); describe("InlineEditorProviders", function () { @@ -78,13 +80,11 @@ define(function (require, exports, module) { * * @param {string} openFile Project relative file path to open in a main editor. * @param {number} openOffset The offset index location within openFile to open an inline editor. - * @param {?boolean} expectInline Use false to verify that an inline editor should not be opened. Omit otherwise. + * @param {boolean=} expectInline Use false to verify that an inline editor should not be opened. Omit otherwise. + * @param {Array<{string}>=} workingSet Optional array of files to open in working set */ function initInlineTest(openFile, openOffset, expectInline, workingSet) { - var allFiles, - editor, - hostOpened = false, - err = false; + var editor; workingSet = workingSet || []; @@ -145,6 +145,45 @@ define(function (require, exports, module) { return false; } + function expectPopoverMessageWithText(text) { + var $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(1); + + var popoverText = $(".text", $popover).html(); + expect(popoverText).toEqual(text); + } + + function getBounds(object, useOffset) { + var left = (useOffset ? object.offset().left : parseInt(object.css("left"), 10)), + top = (useOffset ? object.offset().top : parseInt(object.css("top"), 10)); + return { + left: left, + top: top, + right: left + object.outerWidth(), + bottom: top + object.outerHeight() + }; + } + + function boundsInsideWindow(object) { + // For the popover, we can't use offset(), because jQuery gets confused by the + // scale factor and transform origin that the animation uses. Instead, we rely on + // the fact that its offset parent is body, and just test its explicit left/top values. + var bounds = getBounds(object, false), + editorBounds = getBounds(testWindow.$("#editor-holder"), true); + + return bounds.left >= editorBounds.left && + bounds.right <= editorBounds.right && + bounds.top >= editorBounds.top && + bounds.bottom <= editorBounds.bottom; + } + + function toggleOption(commandID, text) { + runs(function () { + var promise = CommandManager.execute(commandID); + waitsForDone(promise, text); + }); + } + /* * Note that the bulk of selector matching tests are in CSSutils-test.js. * These tests are primarily focused on the InlineEditorProvider module. @@ -164,6 +203,7 @@ define(function (require, exports, module) { testWindow = w; // Load module instances from brackets.test + CommandManager = testWindow.brackets.test.CommandManager; Commands = testWindow.brackets.test.Commands; EditorManager = testWindow.brackets.test.EditorManager; FileSyncManager = testWindow.brackets.test.FileSyncManager; @@ -405,8 +445,33 @@ define(function (require, exports, module) { initInlineTest("test1.html", 3, false); runs(function () { - // verify no inline open + // verify no inline editor open expect(EditorManager.getCurrentFullEditor().getInlineWidgets().length).toBe(0); + + // verify popover message is displayed with correct string + expectPopoverMessageWithText(Strings.ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND); + }); + }); + + it("should not open an inline editor when positioned on title attribute", function () { + initInlineTest("test1.html", 12, false); + + runs(function () { + // verify no inline editor open + expect(EditorManager.getCurrentFullEditor().getInlineWidgets().length).toBe(0); + + // verify popover message is displayed with correct string + expectPopoverMessageWithText(Strings.ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR); + + // verify popover message is automatically dismissed after short wait + // current delay is 5 sec + 0.5 sec fade-out transition + waits(6000); + }); + + runs(function () { + // verify no popover message + var $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(0); }); }); @@ -414,11 +479,150 @@ define(function (require, exports, module) { initInlineTest("test1.html", 4, false); runs(function () { - // verify no inline open + // verify no inline editor open expect(EditorManager.getCurrentFullEditor().getInlineWidgets().length).toBe(0); }); }); + it("should close first popover message before opening another one", function () { + var editor, + openFile = "test1.html"; + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([openFile]), "FILE_OPEN timeout", 1000); + }); + + runs(function () { + editor = EditorManager.getCurrentFullEditor(); + + // attempt to open inline editor at specified offset index + var inlineEditorResult = SpecRunnerUtils.toggleQuickEditAtOffset( + editor, + infos[openFile].offsets[3] + ); + + waitsForFail(inlineEditorResult, "inline editor not opened", 1000); + }); + + runs(function () { + // attempt to open another inline editor at a different offset index + var inlineEditorResult = SpecRunnerUtils.toggleQuickEditAtOffset( + editor, + infos[openFile].offsets[12] + ); + + waitsForFail(inlineEditorResult, "inline editor not opened", 1000); + }); + + runs(function () { + // verify only 1 popover message + var $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(1); + }); + }); + + it("should close popover message on selection change", function () { + var editor, + openFile = "test1.html"; + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([openFile]), "FILE_OPEN timeout", 1000); + }); + + runs(function () { + editor = EditorManager.getCurrentFullEditor(); + + // attempt to open inline editor at specified offset index + var inlineEditorResult = SpecRunnerUtils.toggleQuickEditAtOffset( + editor, + infos[openFile].offsets[3] + ); + + waitsForFail(inlineEditorResult, "inline editor not opened", 1000); + }); + + runs(function () { + // change selection + var offset = infos[openFile].offsets[12]; + editor.setCursorPos(offset.line, offset.ch); + + // verify no popover message + var $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(0); + }); + }); + + it("should position message popover inside left edge of window", function () { + var $popover; + initInlineTest("test1.html", 11, false); + + runs(function () { + $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(1); + }); + + runs(function () { + expect(boundsInsideWindow($popover)).toBeTruthy(); + }); + }); + + it("should position message popover inside top edge of window", function () { + var $popover; + initInlineTest("test1.html", 3, false); + + runs(function () { + $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(1); + }); + + runs(function () { + expect(boundsInsideWindow($popover)).toBeTruthy(); + }); + }); + + it("should scroll cursor into view and position message popover inside right edge of window", function () { + var $popover, scrollPos, editor, + openFile = "test1.html"; + + runs(function () { + // Turn off word wrap for next tests + toggleOption(Commands.TOGGLE_WORD_WRAP, "Toggle word-wrap"); + }); + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([openFile]), "FILE_OPEN timeout", 1000); + }); + + runs(function () { + editor = EditorManager.getCurrentFullEditor(); + expect(editor.getScrollPos().x).toEqual(0); + + // attempt to open inline editor at specified offset index + var inlineEditorResult = SpecRunnerUtils.toggleQuickEditAtOffset( + editor, + infos[openFile].offsets[13] + ); + + waitsForFail(inlineEditorResult, "inline editor not opened", 1000); + }); + + runs(function () { + $popover = testWindow.$(".popover-message"); + expect($popover.length).toEqual(1); + }); + + runs(function () { + // Popover should be inside right edge + expect(boundsInsideWindow($popover)).toBeTruthy(); + + // verify that page scrolled left + expect(editor.getScrollPos().x).toBeGreaterThan(0); + + // restore word wrap + toggleOption(Commands.TOGGLE_WORD_WRAP, "Toggle word-wrap"); + }); + }); + it("should increase size based on content", function () { initInlineTest("test1.html", 1);