diff --git a/ace-internal.d.ts b/ace-internal.d.ts index c0417ec25cd..ac9f46501e8 100644 --- a/ace-internal.d.ts +++ b/ace-internal.d.ts @@ -380,7 +380,6 @@ export namespace Ace { dragDelay: number; dragEnabled: boolean; focusTimeout: number; - tooltipFollowsMouse: boolean; } interface EditorOptions extends EditSessionOptions, @@ -1629,7 +1628,6 @@ declare module "./src/mouse/mouse_event" { declare module "./src/mouse/mouse_handler" { export interface MouseHandler { - $tooltipFollowsMouse?: boolean, cancelDrag?: boolean //from DefaultHandlers $clickSelection?: Ace.Range, @@ -1638,6 +1636,7 @@ declare module "./src/mouse/mouse_handler" { select?: () => void $lastScroll?: { t: number, vx: number, vy: number, allowed: number } selectEnd?: () => void + tooltip?: Ace.GutterTooltip } } diff --git a/ace.d.ts b/ace.d.ts index 04dadcec71a..8f5e42c3a8f 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -281,7 +281,6 @@ declare module "ace-code" { dragDelay: number; dragEnabled: boolean; focusTimeout: number; - tooltipFollowsMouse: boolean; } interface EditorOptions extends EditSessionOptions, MouseHandlerOptions, VirtualRendererOptions { selectionStyle: "fullLine" | "screenLine" | "text" | "line"; diff --git a/src/editor.js b/src/editor.js index 528f440ec36..3b5f5445f19 100644 --- a/src/editor.js +++ b/src/editor.js @@ -3121,7 +3121,6 @@ config.defineOptions(Editor.prototype, "editor", { dragDelay: "$mouseHandler", dragEnabled: "$mouseHandler", focusTimeout: "$mouseHandler", - tooltipFollowsMouse: "$mouseHandler", firstLineNumber: "session", overwrite: "session", diff --git a/src/ext/options.js b/src/ext/options.js index 6457003f45d..47a1685e49c 100644 --- a/src/ext/options.js +++ b/src/ext/options.js @@ -231,10 +231,6 @@ var optionGroups = { }, "Keyboard Accessibility Mode": { path: "enableKeyboardAccessibility" - }, - "Gutter tooltip follows mouse": { - path: "tooltipFollowsMouse", - defaultValue: true } } }; diff --git a/src/keyboard/gutter_handler.js b/src/keyboard/gutter_handler.js index d8f99dd07b6..2dab5f263f9 100644 --- a/src/keyboard/gutter_handler.js +++ b/src/keyboard/gutter_handler.js @@ -34,7 +34,7 @@ class GutterKeyboardHandler { e.preventDefault(); if (e.keyCode === keys["escape"]) - this.annotationTooltip.hideTooltip(); + this.annotationTooltip.hide(); return; } @@ -186,12 +186,8 @@ class GutterKeyboardHandler { return; case "annotation": - var gutterElement = this.lines.cells[this.activeRowIndex].element.childNodes[2]; - var rect = gutterElement.getBoundingClientRect(); - var style = this.annotationTooltip.getElement().style; - style.left = rect.right + "px"; - style.top = rect.bottom + "px"; - this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex)); + this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex)); + this.annotationTooltip.$fromKeyboard = true; break; } return; @@ -213,7 +209,7 @@ class GutterKeyboardHandler { } if (this.annotationTooltip.isOpen) - this.annotationTooltip.hideTooltip(); + this.annotationTooltip.hide(); return; } diff --git a/src/keyboard/gutter_handler_test.js b/src/keyboard/gutter_handler_test.js index c59d46738f5..3c641fe07ea 100644 --- a/src/keyboard/gutter_handler_test.js +++ b/src/keyboard/gutter_handler_test.js @@ -3,23 +3,24 @@ if (typeof process !== "undefined") { require("../test/mockdom"); } -var keys = require('../lib/keys'); - "use strict"; - + require("../multi_select"); require("../theme/textmate"); +var user = require("../test/user"); var Editor = require("../editor").Editor; var Mode = require("../mode/java").Mode; var VirtualRenderer = require("../virtual_renderer").VirtualRenderer; var assert = require("../test/assertions"); -function emit(keyCode) { - var data = {bubbles: true, keyCode}; - var event = new KeyboardEvent("keydown", data); - - var el = document.activeElement; - el.dispatchEvent(event); +function findVisibleTooltip() { + const tooltips = document.body.querySelectorAll(".ace_gutter-tooltip"); + for (let i = 0; i < tooltips.length; i++) { + if (window.getComputedStyle(tooltips[i]).display === "block") { + return tooltips[i]; + } + } + return null; } module.exports = { @@ -43,20 +44,20 @@ module.exports = { editor.renderer.$loop._flush(); var lines = editor.renderer.$gutterLayer.$lines; - var toggler = lines.cells[0].element.children[1]; + var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); // Set focus to the gutter div. editor.renderer.$gutter.focus(); assert.equal(document.activeElement, editor.renderer.$gutter); // Focus on the fold widget. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { assert.equal(document.activeElement, lines.cells[0].element.childNodes[1]); // Click the fold widget. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check that code is folded. @@ -65,7 +66,7 @@ module.exports = { assert.equal(lines.cells[1].element.textContent, "52"); // After escape focus should be back to the gutter. - emit(keys["escape"]); + user.type("Escape"); assert.equal(document.activeElement, editor.renderer.$gutter); done(); @@ -90,13 +91,13 @@ module.exports = { assert.equal(lines.cells[2].element.textContent, "3"); // Focus on the fold widgets. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]); // Click the first fold widget. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check that code is folded. @@ -104,19 +105,19 @@ module.exports = { assert.equal(lines.cells[2].element.textContent, "8"); // Move to the next fold widget. - emit(keys["down"]); + user.type("Down"); assert.equal(document.activeElement, lines.cells[3].element.childNodes[1]); assert.equal(lines.cells[4].element.textContent, "10"); // Click the fold widget. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check that code is folded. assert.equal(lines.cells[4].element.textContent, "15"); // Move back up one fold widget. - emit(keys["up"]); + user.type("Up"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]); done(); @@ -141,26 +142,26 @@ module.exports = { assert.equal(document.activeElement, editor.renderer.$gutter); // Focus on the annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { - emit(keys["left"]); + user.type("Left"); assert.equal(document.activeElement, lines.cells[0].element.childNodes[2]); // Click annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. - emit(keys["escape"]); + user.type("Escape"); // After escape again focus should be back to the gutter. - emit(keys["escape"]); + user.type("Escape"); assert.equal(document.activeElement, editor.renderer.$gutter); done(); @@ -186,46 +187,46 @@ module.exports = { assert.equal(document.activeElement, editor.renderer.$gutter); // Focus on the annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { - emit(keys["left"]); + user.type("Left"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]); // Click annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. - emit(keys["escape"]); + user.type("Escape"); // Press down to move to next annotation. - emit(keys["down"]); + user.type("Down"); assert.equal(document.activeElement, lines.cells[2].element.childNodes[2]); // Click annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. - emit(keys["escape"]); + user.type("Escape"); // Move back up one annotation. - emit(keys["up"]); + user.type("Up"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]); // Move back to the folds, focus should be on the fold on line 1. - emit(keys["right"]); + user.type("Right"); assert.equal(document.activeElement, lines.cells[0].element.childNodes[1]); done(); @@ -249,14 +250,15 @@ module.exports = { assert.equal(document.activeElement, editor.renderer.$gutter); // Focus on gutter interaction. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Focus should be on the annotation directly. assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]); done(); }, 20); - },"test: aria attributes mode with getFoldWidgetRange" : function() { + }, + "test: aria attributes mode with getFoldWidgetRange" : function() { var editor = this.editor; var value = "x {" + "\n".repeat(5) + "}"; editor.session.setMode(new Mode()); @@ -265,7 +267,7 @@ module.exports = { editor.renderer.$loop._flush(); var lines = editor.renderer.$gutterLayer.$lines; - var toggler = lines.cells[0].element.children[1]; + var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); assert.equal(toggler.getAttribute("aria-label"), "Toggle code folding, rows 1 through 6"); assert.equal(toggler.getAttribute("aria-expanded"), "true"); @@ -291,7 +293,7 @@ module.exports = { editor.renderer.$loop._flush(); var lines = editor.renderer.$gutterLayer.$lines; - var toggler = lines.cells[0].element.children[1]; + var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); assert.equal(toggler.getAttribute("aria-label"), "Toggle code folding, row 1"); assert.equal(toggler.getAttribute("aria-expanded"), "true"); @@ -323,10 +325,10 @@ module.exports = { assert.equal(document.activeElement, editor.renderer.$gutter); // Focus on the annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { - emit(keys["left"]); + user.type("Left"); assert.equal(document.activeElement, lines.cells[0].element.childNodes[2]); setTimeout(function() { @@ -364,30 +366,30 @@ module.exports = { }); // Focus on the annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { - emit(keys["left"]); + user.type("Left"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]); // Click annotation. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. - emit(keys["escape"]); + user.type("Escape"); // Switch lane move to custom widget - emit(keys["right"]); + user.type("Right"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[3]); // Move back to the annotations, focus should be on the annotation on line 1. - emit(keys["left"]); + user.type("Left"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]); done(); }, 20); @@ -426,20 +428,20 @@ module.exports = { }); // Focus on the fold widgets. - emit(keys["enter"]); + user.type("Enter"); setTimeout(function() { assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]); // Move down to the custom widget. - emit(keys["down"]); + user.type("Down"); assert.equal(document.activeElement, lines.cells[2].element.childNodes[3]); - emit(keys["enter"]); + user.type("Enter"); assert.equal(firstCallbackCalledCount,1); // Move up to the previous fold widget. - emit(keys["up"]); + user.type("Up"); assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]); done(); }, 20); diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 0c1735d6d97..d36ad2db8e4 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -3,15 +3,10 @@ * @typedef {import("./mouse_handler").MouseHandler} MouseHandler */ var dom = require("../lib/dom"); -var event = require("../lib/event"); -var Tooltip = require("../tooltip").Tooltip; +var MouseEvent = require("./mouse_event").MouseEvent; +var HoverTooltip = require("../tooltip").HoverTooltip; var nls = require("../config").nls; - -const GUTTER_TOOLTIP_LEFT_OFFSET = 5; -const GUTTER_TOOLTIP_TOP_OFFSET = 3; -exports.GUTTER_TOOLTIP_LEFT_OFFSET = GUTTER_TOOLTIP_LEFT_OFFSET; -exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; - +var Range = require("../range").Range; /** * @param {MouseHandler} mouseHandler @@ -20,7 +15,13 @@ exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; function GutterHandler(mouseHandler) { var editor = mouseHandler.editor; var gutter = editor.renderer.$gutterLayer; - var tooltip = new GutterTooltip(editor, true); + mouseHandler.tooltip = new GutterTooltip(editor); + mouseHandler.tooltip.addToEditor(editor); + + mouseHandler.tooltip.setDataProvider(function(e, editor) { + var row = e.getDocumentPosition().row; + mouseHandler.tooltip.showTooltip(row); + }); mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) { if (!editor.isFocused() || e.getButton() != 0) @@ -46,101 +47,16 @@ function GutterHandler(mouseHandler) { mouseHandler.captureMouse(e); return e.preventDefault(); }); - - var tooltipTimeout, mouseEvent; - - function showTooltip() { - var row = mouseEvent.getDocumentPosition().row; - - var maxRow = editor.session.getLength(); - if (row == maxRow) { - var screenRow = editor.renderer.pixelToScreenCoordinates(0, mouseEvent.y).row; - var pos = mouseEvent.$pos; - if (screenRow > editor.session.documentToScreenRow(pos.row, pos.column)) - return hideTooltip(); - } - - tooltip.showTooltip(row); - - if (!tooltip.isOpen) - return; - - editor.on("mousewheel", hideTooltip); - editor.on("changeSession", hideTooltip); - window.addEventListener("keydown", hideTooltip, true); - - if (mouseHandler.$tooltipFollowsMouse) { - moveTooltip(mouseEvent); - } else { - var gutterRow = mouseEvent.getGutterRow(); - var gutterCell = gutter.$lines.get(gutterRow); - if (gutterCell) { - var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation"); - var rect = gutterElement.getBoundingClientRect(); - var style = tooltip.getElement().style; - style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; - style.top = (rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET) + "px"; - } else { - moveTooltip(mouseEvent); - } - } - } - - function hideTooltip(e) { - // dont close tooltip in case the user wants to copy text from it (Ctrl/Meta + C) - if (e && e.type === "keydown" && (e.ctrlKey || e.metaKey)) - return; - // in case mouse moved but is still on the tooltip, dont close it - if (e && e.type === "mouseout" && (!e.relatedTarget || tooltip.getElement().contains(e.relatedTarget))) - return; - if (tooltipTimeout) - tooltipTimeout = clearTimeout(tooltipTimeout); - if (tooltip.isOpen) { - tooltip.hideTooltip(); - editor.off("mousewheel", hideTooltip); - editor.off("changeSession", hideTooltip); - window.removeEventListener("keydown", hideTooltip, true); - } - } - - function moveTooltip(e) { - tooltip.setPosition(e.x, e.y); - } - - mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) { - var target = e.domEvent.target || e.domEvent.srcElement; - if (dom.hasCssClass(target, "ace_fold-widget") || dom.hasCssClass(target, "ace_custom-widget")) - return hideTooltip(); - - if (tooltip.isOpen && mouseHandler.$tooltipFollowsMouse) - moveTooltip(e); - - mouseEvent = e; - if (tooltipTimeout) - return; - tooltipTimeout = setTimeout(function() { - tooltipTimeout = null; - if (mouseEvent && !mouseHandler.isMousePressed) - showTooltip(); - }, 50); - }); - - event.addListener(editor.renderer.$gutter, "mouseout", function(e) { - mouseEvent = null; - if (!tooltip.isOpen) - return; - - tooltipTimeout = setTimeout(function() { - tooltipTimeout = null; - hideTooltip(e); - }, 50); - }, editor); } exports.GutterHandler = GutterHandler; -class GutterTooltip extends Tooltip { - constructor(editor, isHover = false) { + +class GutterTooltip extends HoverTooltip { + /** + * @param {import("../editor").Editor} editor + */ + constructor(editor) { super(editor.container); this.id = "gt" + (++GutterTooltip.$uid); this.editor = editor; @@ -150,38 +66,46 @@ class GutterTooltip extends Tooltip { el.setAttribute("role", "tooltip"); el.setAttribute("id", this.id); el.style.pointerEvents = "auto"; - if (isHover) { - this.onMouseOut = this.onMouseOut.bind(this); - el.addEventListener("mouseout", this.onMouseOut); - } - } + el.style.position = "absolute"; + this.idleTime = 90; - // handler needed to hide tooltip after mouse hovers from tooltip to editor - onMouseOut(e) { - if (!this.isOpen) return; + this.onDomMouseMove = this.onDomMouseMove.bind(this); + this.onDomMouseOut = this.onDomMouseOut.bind(this); - if (!e.relatedTarget || this.getElement().contains(e.relatedTarget)) return; + this.setClassName("ace_gutter-tooltip"); + } + + onDomMouseMove(domEvent) { + var aceEvent = new MouseEvent(domEvent, this.editor); + this.onMouseMove(aceEvent, this.editor); + } + + onDomMouseOut(domEvent) { + var aceEvent = new MouseEvent(domEvent, this.editor); + this.onMouseOut(aceEvent); + } - if (e && e.currentTarget.contains(e.relatedTarget)) return; - this.hideTooltip(); + addToEditor(editor) { + var gutter = editor.renderer.$gutter; + gutter.addEventListener("mousemove", this.onDomMouseMove); + gutter.addEventListener("mouseout", this.onDomMouseOut); + super.addToEditor(editor); } - setPosition(x, y) { - var windowWidth = window.innerWidth || document.documentElement.clientWidth; - var windowHeight = window.innerHeight || document.documentElement.clientHeight; - var width = this.getWidth(); - var height = this.getHeight(); - x += 15; - y += 15; - if (x + width > windowWidth) { - x -= (x + width) - windowWidth; - } - if (y + height > windowHeight) { - y -= 20 + height; + removeFromEditor(editor) { + var gutter = editor.renderer.$gutter; + gutter.removeEventListener("mousemove", this.onDomMouseMove); + gutter.removeEventListener("mouseout", this.onDomMouseOut); + super.removeFromEditor(editor); + } + + destroy() { + if (this.editor) { + this.removeFromEditor(this.editor); } - Tooltip.prototype.setPosition.call(this, x, y); + super.destroy(); } - + static get annotationLabels() { return { error: { @@ -207,6 +131,9 @@ class GutterTooltip extends Tooltip { }; } + /** + * @param {number} row + */ showTooltip(row) { var gutter = this.editor.renderer.$gutterLayer; var annotationsInRow = gutter.$annotations[row]; @@ -227,7 +154,7 @@ class GutterTooltip extends Tooltip { var severityRank = {error: 1, security: 2, warning: 3, info: 4, hint: 5}; var mostSevereAnnotationTypeInFold; - for (let i = row + 1; i <= fold.end.row; i++) { + for (var i = row + 1; i <= fold.end.row; i++) { if (!gutter.$annotations[i]) continue; for (var j = 0; j < gutter.$annotations[i].text.length; j++) { @@ -253,13 +180,13 @@ class GutterTooltip extends Tooltip { } } - if (annotation.displayText.length === 0) return this.hideTooltip(); + if (annotation.displayText.length === 0) return this.hide(); var annotationMessages = {error: [], security: [], warning: [], info: [], hint: []}; var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon"; // Construct the contents of the tooltip. - for (let i = 0; i < annotation.displayText.length; i++) { + for (var i = 0; i < annotation.displayText.length; i++) { var lineElement = dom.createElement("span"); var iconElement = dom.createElement("span"); @@ -279,9 +206,7 @@ class GutterTooltip extends Tooltip { annotationMessages[annotation.type[i].replace("_fold", "")].push(lineElement); } - // Clear the current tooltip content - var tooltipElement = this.getElement(); - dom.removeChildren(tooltipElement); + var tooltipElement = dom.createElement("span"); // Update the tooltip content annotationMessages.error.forEach((el) => tooltipElement.appendChild(el)); @@ -292,25 +217,43 @@ class GutterTooltip extends Tooltip { tooltipElement.setAttribute("aria-live", "polite"); - if (!this.isOpen) { - this.setTheme(this.editor.renderer.theme); - this.setClassName("ace_gutter-tooltip"); - } - - const annotationNode = this.$findLinkedAnnotationNode(row); + var annotationNode = this.$findLinkedAnnotationNode(row); if (annotationNode) { annotationNode.setAttribute("aria-describedby", this.id); } - this.show(); + var range = Range.fromPoints({row, column: 0}, {row, column: 0}); + this.showForRange(this.editor, range, tooltipElement); this.visibleTooltipRow = row; this.editor._signal("showGutterTooltip", this); } + $setPosition(editor, _ignoredPosition, range) { + var gutterCell = this.$findCellByRow(range.start.row); + if (!gutterCell) return; + var el = gutterCell && gutterCell.element; + var anchorEl = el && (el.querySelector(".ace_gutter_annotation")); + if (!anchorEl) return; + var r = anchorEl.getBoundingClientRect(); + if (!r) return; + var point = { + pageX: r.right, + pageY: r.top + }; + return super.$setPosition(editor, { + point, + rect: r + }, range); + } + + $shouldPlaceAbove(metrics) { + return metrics.spaceBelow < metrics.labelHeight; + } + $findLinkedAnnotationNode(row) { - const cell = this.$findCellByRow(row); + var cell = this.$findCellByRow(row); if (cell) { - const element = cell.element; + var element = cell.element; if (element.childNodes.length > 2) { return element.childNodes[2]; } @@ -321,34 +264,45 @@ class GutterTooltip extends Tooltip { return this.editor.renderer.$gutterLayer.$lines.cells.find((el) => el.row === row); } - hideTooltip() { + hide(e) { if(!this.isOpen){ return; } this.$element.removeAttribute("aria-live"); - this.hide(); if (this.visibleTooltipRow != undefined) { - const annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow); + var annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow); if (annotationNode) { annotationNode.removeAttribute("aria-describedby"); } } - this.visibleTooltipRow = undefined; this.editor._signal("hideGutterTooltip", this); + super.hide(e); } static annotationsToSummaryString(annotations) { - const summary = []; - const annotationTypes = ["error", "security", "warning", "info", "hint"]; - for (const annotationType of annotationTypes) { + var summary = []; + var annotationTypes = ["error", "security", "warning", "info", "hint"]; + for (var annotationType of annotationTypes) { if (!annotations[annotationType].length) continue; - const label = annotations[annotationType].length === 1 ? GutterTooltip.annotationLabels[annotationType].singular : GutterTooltip.annotationLabels[annotationType].plural; + var label = annotations[annotationType].length === 1 ? GutterTooltip.annotationLabels[annotationType].singular : GutterTooltip.annotationLabels[annotationType].plural; summary.push(`${annotations[annotationType].length} ${label}`); } return summary.join(", "); } + + /** + * Check if cursor is outside gutter + * @param e + * @return {boolean} + */ + isOutsideOfText(e) { + var editor = e.editor; + var rect = editor.renderer.$gutter.getBoundingClientRect(); + return !(e.clientX >= rect.left && e.clientX <= rect.right && + e.clientY >= rect.top && e.clientY <= rect.bottom); + } } GutterTooltip.$uid = 0; diff --git a/src/mouse/default_gutter_handler_test.js b/src/mouse/default_gutter_handler_test.js index e3a0b3af6b4..25fba232678 100644 --- a/src/mouse/default_gutter_handler_test.js +++ b/src/mouse/default_gutter_handler_test.js @@ -12,7 +12,7 @@ var Mode = require("../mode/java").Mode; var VirtualRenderer = require("../virtual_renderer").VirtualRenderer; var assert = require("../test/assertions"); var user = require("../test/user"); -const {GUTTER_TOOLTIP_LEFT_OFFSET, GUTTER_TOOLTIP_TOP_OFFSET} = require("./default_gutter_handler"); + var MouseEvent = function(type, opts){ var e = document.createEvent("MouseEvents"); e.initMouseEvent(/click|wheel/.test(type) ? type : "mouse" + type, @@ -26,6 +26,16 @@ var MouseEvent = function(type, opts){ var editor; +function findVisibleTooltip() { + const tooltips = document.body.querySelectorAll(".ace_gutter-tooltip"); + for (let i = 0; i < tooltips.length; i++) { + if (window.getComputedStyle(tooltips[i]).display === "block") { + return tooltips[i]; + } + } + return null; +} + module.exports = { setUp : function(next) { this.editor = new Editor(new VirtualRenderer()); @@ -52,13 +62,15 @@ module.exports = { assert.ok(/ace_error/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); + assert.ok(/error test/.test(tooltip.textContent)); + annotation.dispatchEvent(new MouseEvent("move", {x: 0, y: 0})); done(); }, 100); }, @@ -76,12 +88,12 @@ module.exports = { assert.ok(/ace_security/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/security finding test/.test(tooltip.textContent)); done(); }, 100); @@ -100,12 +112,12 @@ module.exports = { assert.ok(/ace_warning/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning test/.test(tooltip.textContent)); done(); }, 100); @@ -124,12 +136,12 @@ module.exports = { assert.ok(/ace_info/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/info test/.test(tooltip.textContent)); done(); }, 100); @@ -148,12 +160,12 @@ module.exports = { assert.ok(/ace_hint/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/suggestion test/.test(tooltip.textContent)); done(); }, 100); @@ -200,16 +212,16 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_error_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error in folded/.test(tooltip.textContent)); done(); - }, 50); + }, 100); }, "test: security show up in fold" : function(done) { var editor = this.editor; @@ -236,13 +248,13 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_security_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/security finding in folded/.test(tooltip.textContent)); done(); }, 100); @@ -272,13 +284,13 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_warning_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning in folded/.test(tooltip.textContent)); done(); }, 100); @@ -384,34 +396,6 @@ module.exports = { assert.notOk(/ace_security_fold/.test(firstLineGutterElement.className)); assert.ok(/ace_warning_fold/.test(firstLineGutterElement.className)); }, - "test: sets position correctly when tooltipFollowsMouse false" : function(done) { - var editor = this.editor; - var value = ""; - - editor.session.setMode(new Mode()); - editor.setValue(value, -1); - editor.session.setAnnotations([{row: 0, column: 0, text: "error test", type: "error"}]); - editor.setOption("tooltipFollowsMouse", false); - editor.setOption("useSvgGutterIcons", true); - editor.renderer.$loop._flush(); - - var lines = editor.renderer.$gutterLayer.$lines; - var annotation = lines.cells[0].element.childNodes[2].firstChild; - assert.ok(/ace_error/.test(annotation.className)); - - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); - - // Wait for the tooltip to appear after its timeout. - setTimeout(function() { - editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); - assert.ok(/error test/.test(tooltip.textContent)); - assert.equal(tooltip.style.left, `${rect.right - GUTTER_TOOLTIP_LEFT_OFFSET}px`); - assert.equal(tooltip.style.top, `${rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET}px`); - done(); - }, 100); - }, "test: gutter tooltip should properly display special characters (\" ' & <)" : function(done) { var editor = this.editor; var value = ""; @@ -426,12 +410,12 @@ module.exports = { assert.ok(/ace_error/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/special characters " ' & = 0; i--) { + var node = parents[i]; + if (call(node, true)) return; + } + e.eventPhase = 2; + if (call(this, true)) return; + if (call(this, false)) return; + e.eventPhase = 3; + for (var i = 0; i < parents.length; i++) { + var node = parents[i]; + if (call(node, false)) return; + } + + function call(node, capturing) { + e.currentTarget = node; + if (!capturing && node["on" + e.type]) + node["on" + e.type](e); + var id = capturing ? "__c__:" + e.type : e.type; + var events = node._events && node._events[id]; + events && events.slice().forEach(function(listener) { + listener.call(node, e); + }); + if (e.stopped) return true; + if (!capturing && !e.bubbles) return true; + } }; this.contains = function(node) { while (node) { diff --git a/src/tooltip.js b/src/tooltip.js index 6454863bd37..78e7109f933 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -73,8 +73,20 @@ class Tooltip { * @param {import("../ace-internal").Ace.Theme} theme */ setTheme(theme) { - this.$element.className = CLASSNAME + " " + - (theme.isDark? "ace_dark " : "") + (theme.cssClass || ""); + if (this.theme) { + this.theme.isDark && dom.removeCssClass(this.getElement(), "ace_dark"); + this.theme.cssClass && dom.removeCssClass(this.getElement(), this.theme.cssClass); + } + if (theme.isDark) { + dom.addCssClass(this.getElement(), "ace_dark"); + } + if (theme.cssClass) { + dom.addCssClass(this.getElement(), theme.cssClass); + } + this.theme = { + isDark: theme.isDark, + cssClass: theme.cssClass + }; } /** @@ -83,10 +95,8 @@ class Tooltip { * @param {Number} [y] **/ show(text, x, y) { - if (text != null) - this.setText(text); - if (x != null && y != null) - this.setPosition(x, y); + if (text != null) this.setText(text); + if (x != null && y != null) this.setPosition(x, y); if (!this.isOpen) { this.getElement().style.display = "block"; this.isOpen = true; @@ -125,7 +135,7 @@ class Tooltip { } class PopupManager { - constructor () { + constructor() { /**@type{Tooltip[]} */ this.popups = []; } @@ -142,7 +152,7 @@ class PopupManager { * @param {Tooltip} popup */ removePopup(popup) { - const index = this.popups.indexOf(popup); + var index = this.popups.indexOf(popup); if (index !== -1) { this.popups.splice(index, 1); this.updatePopups(); @@ -165,7 +175,8 @@ class PopupManager { if (shouldDisplay) { visiblepopups.push(popup); - } else { + } + else { popup.hide(); } } @@ -176,9 +187,9 @@ class PopupManager { * @param {Tooltip} popupB * @return {boolean} */ - doPopupsOverlap (popupA, popupB) { - const rectA = popupA.getElement().getBoundingClientRect(); - const rectB = popupB.getElement().getBoundingClientRect(); + doPopupsOverlap(popupA, popupB) { + var rectA = popupA.getElement().getBoundingClientRect(); + var rectB = popupB.getElement().getBoundingClientRect(); return (rectA.left < rectB.right && rectA.right > rectB.left && rectA.top < rectB.bottom && rectA.bottom > rectB.top); @@ -192,7 +203,7 @@ exports.Tooltip = Tooltip; class HoverTooltip extends Tooltip { - constructor(parentNode=document.body) { + constructor(parentNode = document.body) { super(parentNode); /**@type{ReturnType | undefined}*/ @@ -212,7 +223,7 @@ class HoverTooltip extends Tooltip { el.addEventListener("mouseout", this.onMouseOut); el.tabIndex = -1; - el.addEventListener("blur", function() { + el.addEventListener("blur", function () { if (!el.contains(document.activeElement)) this.hide(); }.bind(this)); @@ -225,7 +236,11 @@ class HoverTooltip extends Tooltip { addToEditor(editor) { editor.on("mousemove", this.onMouseMove); editor.on("mousedown", this.hide); - editor.renderer.getMouseEventTarget().addEventListener("mouseout", this.onMouseOut, true); + var target = editor.renderer.getMouseEventTarget(); + if (target && typeof target.removeEventListener === "function") { + target.addEventListener("mouseout", this.onMouseOut, true); + } + } /** @@ -234,7 +249,10 @@ class HoverTooltip extends Tooltip { removeFromEditor(editor) { editor.off("mousemove", this.onMouseMove); editor.off("mousedown", this.hide); - editor.renderer.getMouseEventTarget().removeEventListener("mouseout", this.onMouseOut, true); + var target = editor.renderer.getMouseEventTarget(); + if (target && typeof target.removeEventListener === "function") { + target.removeEventListener("mouseout", this.onMouseOut, true); + } if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; @@ -252,12 +270,8 @@ class HoverTooltip extends Tooltip { var isMousePressed = editor.$mouseHandler.isMousePressed; if (this.isOpen) { var pos = this.lastEvent && this.lastEvent.getDocumentPosition(); - if ( - !this.range - || !this.range.contains(pos.row, pos.column) - || isMousePressed - || this.isOutsideOfText(this.lastEvent) - ) { + if (!this.range || !this.range.contains(pos.row, pos.column) || isMousePressed || this.isOutsideOfText( + this.lastEvent)) { this.hide(); } } @@ -265,6 +279,7 @@ class HoverTooltip extends Tooltip { this.lastEvent = e; this.timeout = setTimeout(this.waitForHover, this.idleTime); } + waitForHover() { if (this.timeout) clearTimeout(this.timeout); var dt = Date.now() - this.lastT; @@ -289,10 +304,7 @@ class HoverTooltip extends Tooltip { if (docPos.column == line.length) { var screenPos = editor.renderer.pixelToScreenCoordinates(e.clientX, e.clientY); var clippedPos = editor.session.documentToScreenPosition(docPos.row, docPos.column); - if ( - clippedPos.column != screenPos.column - || clippedPos.row != screenPos.row - ) { + if (clippedPos.column != screenPos.column || clippedPos.row != screenPos.row) { return true; } } @@ -313,7 +325,6 @@ class HoverTooltip extends Tooltip { * @param {MouseEvent} [startingEvent] */ showForRange(editor, range, domNode, startingEvent) { - var MARGIN = 10; if (startingEvent && startingEvent != this.lastEvent) return; if (this.isOpen && document.activeElement == this.getElement()) return; @@ -325,14 +336,12 @@ class HoverTooltip extends Tooltip { } this.isOpen = true; - this.addMarker(range, editor.session); this.range = Range.fromPoints(range.start, range.end); var position = renderer.textToScreenCoordinates(range.start.row, range.start.column); var rect = renderer.scroller.getBoundingClientRect(); // clip position to visible area of the editor - if (position.pageX < rect.left) - position.pageX = rect.left; + if (position.pageX < rect.left) position.pageX = rect.left; var element = this.getElement(); element.innerHTML = ""; @@ -341,23 +350,58 @@ class HoverTooltip extends Tooltip { element.style.maxHeight = ""; element.style.display = "block"; + this.$setPosition(editor, {point: position}, range); + } + + /** + * + * @param {Editor} editor + * @param {{point:{pageX:number,pageY:number},rect?:{top:number,right:number,bottom:number,left:number,width:number,height:number}}} anchor + * @param {Range} [range] + */ + $setPosition(editor, anchor, range) { + var position = anchor && anchor.point ? anchor.point : { + pageX: 0, + pageY: 0 + }; + var MARGIN = 10; + this.addMarker(range, editor.session); + + var renderer = editor.renderer; + var element = this.getElement(); + // measure the size of tooltip, without constraints on its height var labelHeight = element.clientHeight; var labelWidth = element.clientWidth; - var spaceBelow = window.innerHeight - position.pageY - renderer.lineHeight; + var anchorTop = position.pageY; + var anchorLeft = position.pageX; + if (anchor && anchor.rect) { + anchorTop = anchor.rect.top; + anchorLeft = anchor.rect.right; + } + var spaceBelow = window.innerHeight - anchorTop - renderer.lineHeight; // if tooltip fits above the line, or space below the line is smaller, show tooltip above - let isAbove = true; - if (position.pageY - labelHeight < 0 && position.pageY < spaceBelow) { - isAbove = false; - } + var metrics = { labelHeight, anchorTop, spaceBelow}; + var isAbove = this.$shouldPlaceAbove(metrics); - element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px"; - element.style.top = isAbove ? "" : position.pageY + renderer.lineHeight + "px"; - element.style.bottom = isAbove ? window.innerHeight - position.pageY + "px" : ""; + var rootRect = element.offsetParent && element.offsetParent.getBoundingClientRect(); + + element.style.maxHeight = (isAbove ? anchorTop : spaceBelow) - MARGIN + "px"; + element.style.top = isAbove ? "" : anchorTop + renderer.lineHeight - (rootRect ? rootRect.top : 0) + "px"; + element.style.bottom = isAbove ? window.innerHeight - anchorTop + (rootRect ? rootRect.bottom + - window.innerHeight : 0) + "px" : ""; // try to align tooltip left with the range, but keep it on screen - element.style.left = Math.min(position.pageX, window.innerWidth - labelWidth - MARGIN) + "px"; + element.style.left = Math.min(anchorLeft, window.innerWidth - labelWidth - MARGIN) - (rootRect + ? rootRect.left : 0) + "px"; + } + + /** + * @param {{ labelHeight: number; anchorTop: number; spaceBelow: number; }} metrics + */ + $shouldPlaceAbove(metrics) { + return !(metrics.anchorTop - metrics.labelHeight < 0 && metrics.anchorTop < metrics.spaceBelow); } /** @@ -373,15 +417,24 @@ class HoverTooltip extends Tooltip { } hide(e) { - if (!e && document.activeElement == this.getElement()) - return; - if (e && e.target && (e.type != "keydown" || e.ctrlKey || e.metaKey) && this.$element.contains(e.target)) - return; + if (e && this.$fromKeyboard && e.type == "keydown") { + if (e.code == "Escape") { + return; + } + // else if (/Control|Alt|Shift|Command/.test(e.code)) { + // return; + // } + } + + if (!e && document.activeElement == this.getElement()) return; + if (e && e.target && (e.type != "keydown" || e.ctrlKey || e.metaKey) && this.$element.contains( + e.target)) return; this.lastEvent = null; if (this.timeout) clearTimeout(this.timeout); this.timeout = null; this.addMarker(null); if (this.isOpen) { + this.$fromKeyboard = false; this.$removeCloseEvents(); this.getElement().style.display = "none"; this.isOpen = false; diff --git a/types/ace-ext.d.ts b/types/ace-ext.d.ts index e87d48c3a82..55bc6de1be6 100644 --- a/types/ace-ext.d.ts +++ b/types/ace-ext.d.ts @@ -826,10 +826,6 @@ declare module "ace-code/src/ext/options" { "Keyboard Accessibility Mode": { path: string; }; - "Gutter tooltip follows mouse": { - path: string; - defaultValue: boolean; - }; }; } namespace Ace { diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts index 4bfc1caa28e..2e4141bf16e 100644 --- a/types/ace-modules.d.ts +++ b/types/ace-modules.d.ts @@ -1763,6 +1763,10 @@ declare module "ace-code/src/tooltip" { setPosition(x: number, y: number): void; setClassName(className: string): void; setTheme(theme: import("ace-code").Ace.Theme): void; + theme: { + isDark: boolean; + cssClass: string; + }; show(text?: string, x?: number, y?: number): void; hide(e: any): void; getHeight(): number; @@ -1786,9 +1790,7 @@ declare module "ace-code/src/mouse/default_gutter_handler" { export interface GutterHandler { } export type MouseHandler = import("ace-code/src/mouse/mouse_handler").MouseHandler; - export const GUTTER_TOOLTIP_LEFT_OFFSET: 5; - export const GUTTER_TOOLTIP_TOP_OFFSET: 3; - export class GutterTooltip extends Tooltip { + export class GutterTooltip extends HoverTooltip { static get annotationLabels(): { error: { singular: any; @@ -1812,19 +1814,24 @@ declare module "ace-code/src/mouse/default_gutter_handler" { }; }; static annotationsToSummaryString(annotations: any): string; - constructor(editor: any, isHover?: boolean); + constructor(editor: import("ace-code/src/editor").Editor); id: string; - editor: any; + editor: import("ace-code/src/editor").Editor; visibleTooltipRow: number | undefined; - onMouseOut(e: any): void; - setPosition(x: any, y: any): void; - showTooltip(row: any): void; - hideTooltip(): void; + onDomMouseMove(domEvent: any): void; + onDomMouseOut(domEvent: any): void; + addToEditor(editor: any): void; + removeFromEditor(editor: any): void; + showTooltip(row: number): void; + /** + * Check if cursor is outside gutter + */ + isOutsideOfText(e: any): boolean; } export namespace GutterTooltip { let $uid: number; } - import { Tooltip } from "ace-code/src/tooltip"; + import { HoverTooltip } from "ace-code/src/tooltip"; export interface GutterHandler { } } @@ -1869,6 +1876,7 @@ declare module "ace-code/src/mouse/mouse_handler" { startSelect?: (pos?: import("ace-code").Ace.Point, waitForClickSelection?: boolean) => void; select?: () => void; selectEnd?: () => void; + tooltip?: import("ace-code").Ace.GutterTooltip; } export type Editor = import("ace-code/src/editor").Editor; import { MouseEvent } from "ace-code/src/mouse/mouse_event"; @@ -1876,6 +1884,7 @@ declare module "ace-code/src/mouse/mouse_handler" { type Range = import("ace-code").Ace.Range; type MouseEvent = import("ace-code").Ace.MouseEvent; type Point = import("ace-code").Ace.Point; + type GutterTooltip = import("ace-code").Ace.GutterTooltip; } export interface MouseHandler { cancelDrag?: boolean; @@ -1883,6 +1892,7 @@ declare module "ace-code/src/mouse/mouse_handler" { startSelect?: (pos?: Ace.Point, waitForClickSelection?: boolean) => void; select?: () => void; selectEnd?: () => void; + tooltip?: Ace.GutterTooltip; } } declare module "ace-code/src/mouse/fold_handler" {