UNPKG

monaco-editor

Version:
753 lines (752 loc) • 37.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); import * as browser from '../../../base/browser/browser.js'; import { PageCoordinates } from '../editorDom.js'; import { PartFingerprints } from '../view/viewPart.js'; import { ViewLine } from '../viewParts/lines/viewLine.js'; import { Position } from '../../common/core/position.js'; import { Range as EditorRange } from '../../common/core/range.js'; var MouseTarget = /** @class */ (function () { function MouseTarget(element, type, mouseColumn, position, range, detail) { if (mouseColumn === void 0) { mouseColumn = 0; } if (position === void 0) { position = null; } if (range === void 0) { range = null; } if (detail === void 0) { detail = null; } this.element = element; this.type = type; this.mouseColumn = mouseColumn; this.position = position; if (!range && position) { range = new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column); } this.range = range; this.detail = detail; } MouseTarget._typeToString = function (type) { if (type === 1 /* TEXTAREA */) { return 'TEXTAREA'; } if (type === 2 /* GUTTER_GLYPH_MARGIN */) { return 'GUTTER_GLYPH_MARGIN'; } if (type === 3 /* GUTTER_LINE_NUMBERS */) { return 'GUTTER_LINE_NUMBERS'; } if (type === 4 /* GUTTER_LINE_DECORATIONS */) { return 'GUTTER_LINE_DECORATIONS'; } if (type === 5 /* GUTTER_VIEW_ZONE */) { return 'GUTTER_VIEW_ZONE'; } if (type === 6 /* CONTENT_TEXT */) { return 'CONTENT_TEXT'; } if (type === 7 /* CONTENT_EMPTY */) { return 'CONTENT_EMPTY'; } if (type === 8 /* CONTENT_VIEW_ZONE */) { return 'CONTENT_VIEW_ZONE'; } if (type === 9 /* CONTENT_WIDGET */) { return 'CONTENT_WIDGET'; } if (type === 10 /* OVERVIEW_RULER */) { return 'OVERVIEW_RULER'; } if (type === 11 /* SCROLLBAR */) { return 'SCROLLBAR'; } if (type === 12 /* OVERLAY_WIDGET */) { return 'OVERLAY_WIDGET'; } return 'UNKNOWN'; }; MouseTarget.toString = function (target) { return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + target.detail; }; MouseTarget.prototype.toString = function () { return MouseTarget.toString(this); }; return MouseTarget; }()); export { MouseTarget }; var ElementPath = /** @class */ (function () { function ElementPath() { } ElementPath.isTextArea = function (path) { return (path.length === 2 && path[0] === 3 /* OverflowGuard */ && path[1] === 6 /* TextArea */); }; ElementPath.isChildOfViewLines = function (path) { return (path.length >= 4 && path[0] === 3 /* OverflowGuard */ && path[3] === 7 /* ViewLines */); }; ElementPath.isStrictChildOfViewLines = function (path) { return (path.length > 4 && path[0] === 3 /* OverflowGuard */ && path[3] === 7 /* ViewLines */); }; ElementPath.isChildOfScrollableElement = function (path) { return (path.length >= 2 && path[0] === 3 /* OverflowGuard */ && path[1] === 5 /* ScrollableElement */); }; ElementPath.isChildOfMinimap = function (path) { return (path.length >= 2 && path[0] === 3 /* OverflowGuard */ && path[1] === 8 /* Minimap */); }; ElementPath.isChildOfContentWidgets = function (path) { return (path.length >= 4 && path[0] === 3 /* OverflowGuard */ && path[3] === 1 /* ContentWidgets */); }; ElementPath.isChildOfOverflowingContentWidgets = function (path) { return (path.length >= 1 && path[0] === 2 /* OverflowingContentWidgets */); }; ElementPath.isChildOfOverlayWidgets = function (path) { return (path.length >= 2 && path[0] === 3 /* OverflowGuard */ && path[1] === 4 /* OverlayWidgets */); }; return ElementPath; }()); var HitTestContext = /** @class */ (function () { function HitTestContext(context, viewHelper, lastViewCursorsRenderData) { this.model = context.model; this.layoutInfo = context.configuration.editor.layoutInfo; this.viewDomNode = viewHelper.viewDomNode; this.lineHeight = context.configuration.editor.lineHeight; this.typicalHalfwidthCharacterWidth = context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth; this.lastViewCursorsRenderData = lastViewCursorsRenderData; this._context = context; this._viewHelper = viewHelper; } HitTestContext.prototype.getZoneAtCoord = function (mouseVerticalOffset) { return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset); }; HitTestContext.getZoneAtCoord = function (context, mouseVerticalOffset) { // The target is either a view zone or the empty space after the last view-line var viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); if (viewZoneWhitespace) { var viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2, lineCount = context.model.getLineCount(), positionBefore = null, position = void 0, positionAfter = null; if (viewZoneWhitespace.afterLineNumber !== lineCount) { // There are more lines after this view zone positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1); } if (viewZoneWhitespace.afterLineNumber > 0) { // There are more lines above this view zone positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber)); } if (positionAfter === null) { position = positionBefore; } else if (positionBefore === null) { position = positionAfter; } else if (mouseVerticalOffset < viewZoneMiddle) { position = positionBefore; } else { position = positionAfter; } return { viewZoneId: viewZoneWhitespace.id, afterLineNumber: viewZoneWhitespace.afterLineNumber, positionBefore: positionBefore, positionAfter: positionAfter, position: position }; } return null; }; HitTestContext.prototype.getFullLineRangeAtCoord = function (mouseVerticalOffset) { if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) { // Below the last line var lineNumber_1 = this._context.model.getLineCount(); var maxLineColumn_1 = this._context.model.getLineMaxColumn(lineNumber_1); return { range: new EditorRange(lineNumber_1, maxLineColumn_1, lineNumber_1, maxLineColumn_1), isAfterLines: true }; } var lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset); var maxLineColumn = this._context.model.getLineMaxColumn(lineNumber); return { range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn), isAfterLines: false }; }; HitTestContext.prototype.getLineNumberAtVerticalOffset = function (mouseVerticalOffset) { return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset); }; HitTestContext.prototype.isAfterLines = function (mouseVerticalOffset) { return this._context.viewLayout.isAfterLines(mouseVerticalOffset); }; HitTestContext.prototype.getVerticalOffsetForLineNumber = function (lineNumber) { return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber); }; HitTestContext.prototype.findAttribute = function (element, attr) { return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode); }; HitTestContext._findAttribute = function (element, attr, stopAt) { while (element && element !== document.body) { if (element.hasAttribute && element.hasAttribute(attr)) { return element.getAttribute(attr); } if (element === stopAt) { return null; } element = element.parentNode; } return null; }; HitTestContext.prototype.getLineWidth = function (lineNumber) { return this._viewHelper.getLineWidth(lineNumber); }; HitTestContext.prototype.visibleRangeForPosition2 = function (lineNumber, column) { return this._viewHelper.visibleRangeForPosition2(lineNumber, column); }; HitTestContext.prototype.getPositionFromDOMInfo = function (spanNode, offset) { return this._viewHelper.getPositionFromDOMInfo(spanNode, offset); }; HitTestContext.prototype.getCurrentScrollTop = function () { return this._context.viewLayout.getCurrentScrollTop(); }; HitTestContext.prototype.getCurrentScrollLeft = function () { return this._context.viewLayout.getCurrentScrollLeft(); }; return HitTestContext; }()); export { HitTestContext }; var BareHitTestRequest = /** @class */ (function () { function BareHitTestRequest(ctx, editorPos, pos) { this.editorPos = editorPos; this.pos = pos; this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + pos.y - editorPos.y); this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft; this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft && pos.x - editorPos.x >= ctx.layoutInfo.glyphMarginLeft); this.isInContentArea = !this.isInMarginArea; this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth)); } return BareHitTestRequest; }()); var HitTestRequest = /** @class */ (function (_super) { __extends(HitTestRequest, _super); function HitTestRequest(ctx, editorPos, pos, target) { var _this = _super.call(this, ctx, editorPos, pos) || this; _this._ctx = ctx; if (target) { _this.target = target; _this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode); } else { _this.target = null; _this.targetPath = new Uint8Array(0); } return _this; } HitTestRequest.prototype.toString = function () { return "pos(" + this.pos.x + "," + this.pos.y + "), editorPos(" + this.editorPos.x + "," + this.editorPos.y + "), mouseVerticalOffset: " + this.mouseVerticalOffset + ", mouseContentHorizontalOffset: " + this.mouseContentHorizontalOffset + "\n\ttarget: " + (this.target ? this.target.outerHTML : null); }; HitTestRequest.prototype.fulfill = function (type, position, range, detail) { if (position === void 0) { position = null; } if (range === void 0) { range = null; } if (detail === void 0) { detail = null; } return new MouseTarget(this.target, type, this.mouseColumn, position, range, detail); }; HitTestRequest.prototype.withTarget = function (target) { return new HitTestRequest(this._ctx, this.editorPos, this.pos, target); }; return HitTestRequest; }(BareHitTestRequest)); var EMPTY_CONTENT_AFTER_LINES = { isAfterLines: true }; function createEmptyContentDataInLines(horizontalDistanceToText) { return { isAfterLines: false, horizontalDistanceToText: horizontalDistanceToText }; } var MouseTargetFactory = /** @class */ (function () { function MouseTargetFactory(context, viewHelper) { this._context = context; this._viewHelper = viewHelper; } MouseTargetFactory.prototype.mouseTargetIsWidget = function (e) { var t = e.target; var path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); // Is it a content widget? if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { return true; } // Is it an overlay widget? if (ElementPath.isChildOfOverlayWidgets(path)) { return true; } return false; }; MouseTargetFactory.prototype.createMouseTarget = function (lastViewCursorsRenderData, editorPos, pos, target) { var ctx = new HitTestContext(this._context, this._viewHelper, lastViewCursorsRenderData); var request = new HitTestRequest(ctx, editorPos, pos, target); try { var r = MouseTargetFactory._createMouseTarget(ctx, request, false); // console.log(r.toString()); return r; } catch (err) { // console.log(err); return request.fulfill(0 /* UNKNOWN */); } }; MouseTargetFactory._createMouseTarget = function (ctx, request, domHitTestExecuted) { // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`); // First ensure the request has a target if (request.target === null) { if (domHitTestExecuted) { // Still no target... and we have already executed hit test... return request.fulfill(0 /* UNKNOWN */); } var hitTestResult = MouseTargetFactory._doHitTest(ctx, request); if (hitTestResult.position) { return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); } // we know for a fact that request.target is not null var resolvedRequest = request; var result = null; result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted); result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest); return (result || request.fulfill(0 /* UNKNOWN */)); }; MouseTargetFactory._hitTestContentWidget = function (ctx, request) { // Is it a content widget? if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) { var widgetId = ctx.findAttribute(request.target, 'widgetId'); if (widgetId) { return request.fulfill(9 /* CONTENT_WIDGET */, null, null, widgetId); } else { return request.fulfill(0 /* UNKNOWN */); } } return null; }; MouseTargetFactory._hitTestOverlayWidget = function (ctx, request) { // Is it an overlay widget? if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) { var widgetId = ctx.findAttribute(request.target, 'widgetId'); if (widgetId) { return request.fulfill(12 /* OVERLAY_WIDGET */, null, null, widgetId); } else { return request.fulfill(0 /* UNKNOWN */); } } return null; }; MouseTargetFactory._hitTestViewCursor = function (ctx, request) { if (request.target) { // Check if we've hit a painted cursor var lastViewCursorsRenderData = ctx.lastViewCursorsRenderData; for (var i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { var d = lastViewCursorsRenderData[i]; if (request.target === d.domNode) { return request.fulfill(6 /* CONTENT_TEXT */, d.position); } } } if (request.isInContentArea) { // Edge has a bug when hit-testing the exact position of a cursor, // instead of returning the correct dom node, it returns the // first or last rendered view line dom node, therefore help it out // and first check if we are on top of a cursor var lastViewCursorsRenderData = ctx.lastViewCursorsRenderData; var mouseContentHorizontalOffset = request.mouseContentHorizontalOffset; var mouseVerticalOffset = request.mouseVerticalOffset; for (var i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { var d = lastViewCursorsRenderData[i]; if (mouseContentHorizontalOffset < d.contentLeft) { // mouse position is to the left of the cursor continue; } if (mouseContentHorizontalOffset > d.contentLeft + d.width) { // mouse position is to the right of the cursor continue; } var cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber); if (cursorVerticalOffset <= mouseVerticalOffset && mouseVerticalOffset <= cursorVerticalOffset + d.height) { return request.fulfill(6 /* CONTENT_TEXT */, d.position); } } } return null; }; MouseTargetFactory._hitTestViewZone = function (ctx, request) { var viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset); if (viewZoneData) { var mouseTargetType = (request.isInContentArea ? 8 /* CONTENT_VIEW_ZONE */ : 5 /* GUTTER_VIEW_ZONE */); return request.fulfill(mouseTargetType, viewZoneData.position, null, viewZoneData); } return null; }; MouseTargetFactory._hitTestTextArea = function (ctx, request) { // Is it the textarea? if (ElementPath.isTextArea(request.targetPath)) { return request.fulfill(1 /* TEXTAREA */); } return null; }; MouseTargetFactory._hitTestMargin = function (ctx, request) { if (request.isInMarginArea) { var res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset); var pos = res.range.getStartPosition(); var offset = Math.abs(request.pos.x - request.editorPos.x); var detail = { isAfterLines: res.isAfterLines, glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft, glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth, lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth, offsetX: offset }; offset -= ctx.layoutInfo.glyphMarginLeft; if (offset <= ctx.layoutInfo.glyphMarginWidth) { // On the glyph margin return request.fulfill(2 /* GUTTER_GLYPH_MARGIN */, pos, res.range, detail); } offset -= ctx.layoutInfo.glyphMarginWidth; if (offset <= ctx.layoutInfo.lineNumbersWidth) { // On the line numbers return request.fulfill(3 /* GUTTER_LINE_NUMBERS */, pos, res.range, detail); } offset -= ctx.layoutInfo.lineNumbersWidth; // On the line decorations return request.fulfill(4 /* GUTTER_LINE_DECORATIONS */, pos, res.range, detail); } return null; }; MouseTargetFactory._hitTestViewLines = function (ctx, request, domHitTestExecuted) { if (!ElementPath.isChildOfViewLines(request.targetPath)) { return null; } // Check if it is below any lines and any view zones if (ctx.isAfterLines(request.mouseVerticalOffset)) { // This most likely indicates it happened after the last view-line var lineCount = ctx.model.getLineCount(); var maxLineColumn = ctx.model.getLineMaxColumn(lineCount); return request.fulfill(7 /* CONTENT_EMPTY */, new Position(lineCount, maxLineColumn), void 0, EMPTY_CONTENT_AFTER_LINES); } if (domHitTestExecuted) { // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines) // See https://github.com/Microsoft/vscode/issues/46942 if (ElementPath.isStrictChildOfViewLines(request.targetPath)) { var lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); if (ctx.model.getLineLength(lineNumber) === 0) { var lineWidth = ctx.getLineWidth(lineNumber); var detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(7 /* CONTENT_EMPTY */, new Position(lineNumber, 1), void 0, detail); } } // We have already executed hit test... return request.fulfill(0 /* UNKNOWN */); } var hitTestResult = MouseTargetFactory._doHitTest(ctx, request); if (hitTestResult.position) { return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); }; MouseTargetFactory._hitTestMinimap = function (ctx, request) { if (ElementPath.isChildOfMinimap(request.targetPath)) { var possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); var maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn)); } return null; }; MouseTargetFactory._hitTestScrollbarSlider = function (ctx, request) { if (ElementPath.isChildOfScrollableElement(request.targetPath)) { if (request.target && request.target.nodeType === 1) { var className = request.target.className; if (className && /\b(slider|scrollbar)\b/.test(className)) { var possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); var maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn)); } } } return null; }; MouseTargetFactory._hitTestScrollbar = function (ctx, request) { // Is it the overview ruler? // Is it a child of the scrollable element? if (ElementPath.isChildOfScrollableElement(request.targetPath)) { var possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); var maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn)); } return null; }; MouseTargetFactory.prototype.getMouseColumn = function (editorPos, pos) { var layoutInfo = this._context.configuration.editor.layoutInfo; var mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft; return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth); }; MouseTargetFactory._getMouseColumn = function (mouseContentHorizontalOffset, typicalHalfwidthCharacterWidth) { if (mouseContentHorizontalOffset < 0) { return 1; } var chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth); return (chars + 1); }; MouseTargetFactory.createMouseTargetFromHitTestPosition = function (ctx, request, lineNumber, column) { var pos = new Position(lineNumber, column); var lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { if (browser.isEdge && pos.column === 1) { // See https://github.com/Microsoft/vscode/issues/10875 var detail_1 = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(7 /* CONTENT_EMPTY */, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)), void 0, detail_1); } var detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(7 /* CONTENT_EMPTY */, pos, void 0, detail); } var visibleRange = ctx.visibleRangeForPosition2(lineNumber, column); if (!visibleRange) { return request.fulfill(0 /* UNKNOWN */, pos); } var columnHorizontalOffset = visibleRange.left; if (request.mouseContentHorizontalOffset === columnHorizontalOffset) { return request.fulfill(6 /* CONTENT_TEXT */, pos); } var points = []; points.push({ offset: visibleRange.left, column: column }); if (column > 1) { var visibleRange_1 = ctx.visibleRangeForPosition2(lineNumber, column - 1); if (visibleRange_1) { points.push({ offset: visibleRange_1.left, column: column - 1 }); } } var lineMaxColumn = ctx.model.getLineMaxColumn(lineNumber); if (column < lineMaxColumn) { var visibleRange_2 = ctx.visibleRangeForPosition2(lineNumber, column + 1); if (visibleRange_2) { points.push({ offset: visibleRange_2.left, column: column + 1 }); } } points.sort(function (a, b) { return a.offset - b.offset; }); for (var i = 1; i < points.length; i++) { var prev = points[i - 1]; var curr = points[i]; if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) { var rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column); return request.fulfill(6 /* CONTENT_TEXT */, pos, rng); } } return request.fulfill(6 /* CONTENT_TEXT */, pos); }; /** * Most probably WebKit browsers and Edge */ MouseTargetFactory._doHitTestWithCaretRangeFromPoint = function (ctx, request) { // In Chrome, especially on Linux it is possible to click between lines, // so try to adjust the `hity` below so that it lands in the center of a line var lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); var lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber); var lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2); var adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset); if (adjustedPageY <= request.editorPos.y) { adjustedPageY = request.editorPos.y + 1; } if (adjustedPageY >= request.editorPos.y + ctx.layoutInfo.height) { adjustedPageY = request.editorPos.y + ctx.layoutInfo.height - 1; } var adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY); var r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates()); if (r.position) { return r; } // Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom) return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates()); }; MouseTargetFactory._actualDoHitTestWithCaretRangeFromPoint = function (ctx, coords) { var range = document.caretRangeFromPoint(coords.clientX, coords.clientY); if (!range || !range.startContainer) { return { position: null, hitTarget: null }; } // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span var startContainer = range.startContainer; var hitTarget = null; if (startContainer.nodeType === startContainer.TEXT_NODE) { // startContainer is expected to be the token text var parent1 = startContainer.parentNode; // expected to be the token span var parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span var parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div var parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { var p = ctx.getPositionFromDOMInfo(parent1, range.startOffset); return { position: p, hitTarget: null }; } else { hitTarget = startContainer.parentNode; } } else if (startContainer.nodeType === startContainer.ELEMENT_NODE) { // startContainer is expected to be the token span var parent1 = startContainer.parentNode; // expected to be the view line container span var parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div var parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null; if (parent2ClassName === ViewLine.CLASS_NAME) { var p = ctx.getPositionFromDOMInfo(startContainer, startContainer.textContent.length); return { position: p, hitTarget: null }; } else { hitTarget = startContainer; } } return { position: null, hitTarget: hitTarget }; }; /** * Most probably Gecko */ MouseTargetFactory._doHitTestWithCaretPositionFromPoint = function (ctx, coords) { var hitResult = document.caretPositionFromPoint(coords.clientX, coords.clientY); if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) { // offsetNode is expected to be the token text var parent1 = hitResult.offsetNode.parentNode; // expected to be the token span var parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span var parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div var parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { var p = ctx.getPositionFromDOMInfo(hitResult.offsetNode.parentNode, hitResult.offset); return { position: p, hitTarget: null }; } else { return { position: null, hitTarget: hitResult.offsetNode.parentNode }; } } return { position: null, hitTarget: hitResult.offsetNode }; }; /** * Most probably IE */ MouseTargetFactory._doHitTestWithMoveToPoint = function (ctx, coords) { var resultPosition = null; var resultHitTarget = null; var textRange = document.body.createTextRange(); try { textRange.moveToPoint(coords.clientX, coords.clientY); } catch (err) { return { position: null, hitTarget: null }; } textRange.collapse(true); // Now, let's do our best to figure out what we hit :) var parentElement = textRange ? textRange.parentElement() : null; var parent1 = parentElement ? parentElement.parentNode : null; var parent2 = parent1 ? parent1.parentNode : null; var parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : ''; if (parent2ClassName === ViewLine.CLASS_NAME) { var rangeToContainEntireSpan = textRange.duplicate(); rangeToContainEntireSpan.moveToElementText(parentElement); rangeToContainEntireSpan.setEndPoint('EndToStart', textRange); resultPosition = ctx.getPositionFromDOMInfo(parentElement, rangeToContainEntireSpan.text.length); // Move range out of the span node, IE doesn't like having many ranges in // the same spot and will act badly for lines containing dashes ('-') rangeToContainEntireSpan.moveToElementText(ctx.viewDomNode); } else { // Looks like we've hit the hover or something foreign resultHitTarget = parentElement; } // Move range out of the span node, IE doesn't like having many ranges in // the same spot and will act badly for lines containing dashes ('-') textRange.moveToElementText(ctx.viewDomNode); return { position: resultPosition, hitTarget: resultHitTarget }; }; MouseTargetFactory._doHitTest = function (ctx, request) { // State of the art (18.10.2012): // The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/) // Gecko: // - they tried to implement it once, but failed: https://bugzilla.mozilla.org/show_bug.cgi?id=654352 // - however, they do give out rangeParent/rangeOffset properties on mouse events // Webkit: // - they have implemented a previous version of the spec which was using document.caretRangeFromPoint // IE: // - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx // 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy // - when hit testing the cursor it returns the first or the last line in the viewport // - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes // - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line // Thank you browsers for making this so 'easy' :) if (document.caretRangeFromPoint) { return this._doHitTestWithCaretRangeFromPoint(ctx, request); } else if (document.caretPositionFromPoint) { return this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); } else if (document.body.createTextRange) { return this._doHitTestWithMoveToPoint(ctx, request.pos.toClientCoordinates()); } return { position: null, hitTarget: null }; }; return MouseTargetFactory; }()); export { MouseTargetFactory };