mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Underline links on hover
This commit is contained in:
+16
-6
@@ -5,6 +5,7 @@
|
||||
import { ILinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
||||
import { IColorSet } from './renderer/Interfaces';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
@@ -22,8 +23,15 @@ export interface IBufferAccessor {
|
||||
buffer: IBuffer;
|
||||
}
|
||||
|
||||
export interface ITerminal extends IBufferAccessor, IEventEmitter {
|
||||
export interface IElementAccessor {
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
linkifier: ILinkifier;
|
||||
}
|
||||
|
||||
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
@@ -197,9 +205,11 @@ export interface ICharMeasure {
|
||||
measure(options: ITerminalOptions): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
linkifyRow(rowIndex: number): void;
|
||||
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
@@ -243,11 +253,11 @@ export interface ILinkMatcherOptions {
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
*/
|
||||
hoverStartCallback?: LinkMatcherHandler;
|
||||
tooltipCallback?: LinkMatcherHandler;
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link that was hovered.
|
||||
*/
|
||||
hoverEndCallback?: () => void;
|
||||
leaveCallback?: () => void;
|
||||
/**
|
||||
* The priority of the link matcher, this defines the order in which the link
|
||||
* matcher is evaluated relative to others, from highest to lowest. The
|
||||
|
||||
+19
-12
@@ -2,12 +2,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkMatcherOptions, ITerminal, IBufferAccessor } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types';
|
||||
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
import { MouseZone } from './input/MouseZoneManager';
|
||||
|
||||
const INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
@@ -36,7 +35,7 @@ const HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier {
|
||||
export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
@@ -51,8 +50,9 @@ export class Linkifier {
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
|
||||
constructor(
|
||||
private _terminal: IBufferAccessor
|
||||
private _terminal: IBufferAccessor & IElementAccessor
|
||||
) {
|
||||
super();
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ export class Linkifier {
|
||||
handler,
|
||||
matchIndex: options.matchIndex,
|
||||
validationCallback: options.validationCallback,
|
||||
hoverStartCallback: options.hoverStartCallback,
|
||||
hoverEndCallback: options.hoverEndCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
@@ -260,13 +260,20 @@ export class Linkifier {
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
if (matcher.hoverStartCallback) {
|
||||
matcher.hoverStartCallback(e, uri);
|
||||
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = 'pointer';
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (matcher.hoverEndCallback) {
|
||||
matcher.hoverEndCallback();
|
||||
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = '';
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ import * as Mouse from './utils/Mouse';
|
||||
import { CHARSETS } from './Charsets';
|
||||
import { getRawByteCoords } from './utils/Mouse';
|
||||
import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
|
||||
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces';
|
||||
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces';
|
||||
import { BellSound } from './utils/Sounds';
|
||||
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
@@ -188,7 +188,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
private parser: Parser;
|
||||
private renderer: IRenderer;
|
||||
public selectionManager: SelectionManager;
|
||||
private linkifier: Linkifier;
|
||||
public linkifier: ILinkifier;
|
||||
public buffers: BufferSet;
|
||||
public buffer: Buffer;
|
||||
public viewport: IViewport;
|
||||
|
||||
+14
-2
@@ -6,8 +6,8 @@ export type LinkMatcher = {
|
||||
id: number,
|
||||
regex: RegExp,
|
||||
handler: LinkMatcherHandler,
|
||||
hoverStartCallback?: LinkMatcherHandler,
|
||||
hoverEndCallback?: () => void,
|
||||
hoverTooltipCallback?: LinkMatcherHandler,
|
||||
hoverLeaveCallback?: () => void,
|
||||
matchIndex?: number,
|
||||
validationCallback?: LinkMatcherValidationCallback,
|
||||
priority?: number
|
||||
@@ -20,3 +20,15 @@ export type Charset = {[key: string]: string};
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type LineData = CharData[];
|
||||
|
||||
export type LinkHoverEvent = {
|
||||
x: number,
|
||||
y: number,
|
||||
length: number
|
||||
};
|
||||
|
||||
export enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
LEAVE = 'linkleave'
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface IMouseZone {
|
||||
x2: number;
|
||||
y: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverStartCallback?: (e: MouseEvent) => any;
|
||||
hoverEndCallback?: () => any;
|
||||
hoverCallback?: (e: MouseEvent) => any;
|
||||
tooltipCallback?: (e: MouseEvent) => any;
|
||||
leaveCallback?: () => any;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
private _mouseDownListener: (e: MouseEvent) => any;
|
||||
private _clickListener: (e: MouseEvent) => any;
|
||||
|
||||
private _hoverTimeout: number = null;
|
||||
private _tooltipTimeout: number = null;
|
||||
private _currentZone: IMouseZone = null;
|
||||
private _lastHoverCoords: [number, number] = [null, null];
|
||||
|
||||
@@ -63,30 +63,49 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
private _onMouseMove(e: MouseEvent): void {
|
||||
// TODO: Ideally this would only clear the hover state when the mouse moves
|
||||
// outside of the mouse zone
|
||||
if (this._lastHoverCoords[0] !== e.pageX && this._lastHoverCoords[1] !== e.pageY) {
|
||||
// Restart the timeout
|
||||
if (this._hoverTimeout) {
|
||||
clearTimeout(this._hoverTimeout);
|
||||
}
|
||||
this._hoverTimeout = <number><any>setTimeout(() => this._onHover(e), HOVER_DURATION);
|
||||
|
||||
// Fire the hover end callback if a zone was being hovered
|
||||
if (this._currentZone) {
|
||||
this._currentZone.hoverEndCallback();
|
||||
this._currentZone = null;
|
||||
}
|
||||
|
||||
if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
|
||||
this._onHover(e);
|
||||
// Record the current coordinates
|
||||
this._lastHoverCoords = [e.pageX, e.pageY];
|
||||
}
|
||||
}
|
||||
|
||||
private _onHover(e: MouseEvent): void {
|
||||
const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.hoverStartCallback) {
|
||||
this._currentZone = zone;
|
||||
zone.hoverStartCallback(e);
|
||||
|
||||
// Do nothing if the zone is the same
|
||||
if (zone === this._currentZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire the hover end callback if a zone was being hovered
|
||||
if (this._currentZone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
}
|
||||
|
||||
// Exit if there is not zone
|
||||
if (!zone) {
|
||||
return;
|
||||
}
|
||||
this._currentZone = zone;
|
||||
|
||||
// Trigger the hover callback
|
||||
if (zone.hoverCallback) {
|
||||
zone.hoverCallback(e);
|
||||
}
|
||||
|
||||
// Restart the timeout
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
|
||||
}
|
||||
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.tooltipCallback) {
|
||||
zone.tooltipCallback(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +135,9 @@ export class MouseZone implements IMouseZone {
|
||||
public x2: number,
|
||||
public y: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverStartCallback?: (e: MouseEvent) => any,
|
||||
public hoverEndCallback?: () => void
|
||||
public hoverCallback?: (e: MouseEvent) => any,
|
||||
public tooltipCallback?: (e: MouseEvent) => any,
|
||||
public leaveCallback?: () => void
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +109,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected fillBottomLineAtCell(x: number, y: number): void {
|
||||
protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this.scaledCharWidth,
|
||||
(y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
this.scaledCharWidth,
|
||||
width * this.scaledCharWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this.colors.cursor;
|
||||
this.fillBottomLineAtCell(x, y);
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
} else {
|
||||
this._ctx.fillStyle = this.colors.foreground;
|
||||
}
|
||||
this.fillBottomLineAtCell(x, y);
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
}
|
||||
|
||||
this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD));
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
import { FLAGS } from './Types';
|
||||
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
||||
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: LinkHoverEvent = null;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||
super(container, 'link', zIndex, colors);
|
||||
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = null;
|
||||
}
|
||||
|
||||
public reset(terminal: ITerminal): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
|
||||
private _clearCurrentLink(): void {
|
||||
if (this._state) {
|
||||
this.clearCells(this._state.x, this._state.y, this._state.length, 1);
|
||||
this._state = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: LinkHoverEvent): void {
|
||||
this._ctx.fillStyle = this.colors.foreground;
|
||||
this.fillBottomLineAtCells(e.x, e.y, e.length);
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: LinkHoverEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IRenderLayer, IColorSet, IRenderer } from './Interfaces';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
|
||||
export class Renderer implements IRenderer {
|
||||
/** A queue of the rows to be refreshed */
|
||||
@@ -28,7 +29,8 @@ export class Renderer implements IRenderer {
|
||||
new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors),
|
||||
new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors),
|
||||
new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors),
|
||||
new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors)
|
||||
new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors)
|
||||
];
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme } from '../Interfaces';
|
||||
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier } from '../Interfaces';
|
||||
import { LineData } from '../Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from './Browser';
|
||||
import { IColorSet, IRenderer } from '../renderer/Interfaces';
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
linkifier: ILinkifier;
|
||||
isFocused: boolean;
|
||||
options: ITerminalOptions = {};
|
||||
element: HTMLElement;
|
||||
|
||||
+1
-1
@@ -120,5 +120,5 @@
|
||||
}
|
||||
|
||||
.terminal:not(.enable-mouse-events) {
|
||||
cursor: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
Vendored
+5
-4
@@ -141,14 +141,15 @@ interface ILinkMatcherOptions {
|
||||
validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
* A callback that fires when the mouse hovers over a link for a moment.
|
||||
*/
|
||||
hoverStartCallback?: (event: MouseEvent, uri: string) => boolean | void;
|
||||
tooltipCallback?: (event: MouseEvent, uri: string) => boolean | void;
|
||||
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link that was hovered.
|
||||
* A callback that fires when the mouse leaves a link. Note that this can
|
||||
* happen even when tooltipCallback hasn't fired for the link yet.
|
||||
*/
|
||||
hoverEndCallback?: (event: MouseEvent, uri: string) => boolean | void;
|
||||
leaveCallback?: (event: MouseEvent, uri: string) => boolean | void;
|
||||
|
||||
/**
|
||||
* The priority of the link matcher, this defines the order in which the link
|
||||
|
||||
Reference in New Issue
Block a user