mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #2855 from Tyriar/link_underline
Polish link decorations and better support embedder hovers
This commit is contained in:
@@ -16,11 +16,11 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||
super(container, 'link', zIndex, true, colors);
|
||||
terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
|
||||
terminal.linkifier2.onLinkHover(e => this._onLinkHover(e));
|
||||
terminal.linkifier2.onLinkLeave(e => this._onLinkLeave(e));
|
||||
terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -45,7 +45,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||
@@ -69,7 +69,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ export class Linkifier implements ILinkifier {
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number | undefined, end: number | undefined };
|
||||
|
||||
private _onLinkHover = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
private _onLinkLeave = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
|
||||
private _onShowLinkUnderline = new EventEmitter<ILinkifierEvent>();
|
||||
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
|
||||
private _onHideLinkUnderline = new EventEmitter<ILinkifierEvent>();
|
||||
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
|
||||
private _onLinkTooltip = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
|
||||
|
||||
@@ -307,7 +307,7 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._onShowLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._element!.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
@@ -319,7 +319,7 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._onHideLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._element!.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('Linkifier2', () => {
|
||||
};
|
||||
|
||||
it('onLinkHover event range is correct', done => {
|
||||
linkifier.onLinkHover(e => {
|
||||
linkifier.onShowLinkUnderline(e => {
|
||||
assert.equal(link.range.start.x - 1, e.x1);
|
||||
assert.equal(link.range.start.y - 1, e.y1);
|
||||
assert.equal(link.range.end.x, e.x2);
|
||||
@@ -61,7 +61,7 @@ describe('Linkifier2', () => {
|
||||
});
|
||||
|
||||
it('onLinkLeave event range is correct', done => {
|
||||
linkifier.onLinkLeave(e => {
|
||||
linkifier.onHideLinkUnderline(e => {
|
||||
assert.equal(link.range.start.x - 1, e.x1);
|
||||
assert.equal(link.range.start.y - 1, e.y1);
|
||||
assert.equal(link.range.end.x, e.x2);
|
||||
|
||||
+65
-14
@@ -9,20 +9,26 @@ import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
|
||||
interface ILinkState {
|
||||
hideDecorations: boolean;
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
export class Linkifier2 implements ILinkifier2 {
|
||||
private _element: HTMLElement | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
private _currentLink: ILink | undefined;
|
||||
private _currentLinkState: ILinkState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
private _lastBufferCell: IBufferCellPosition | undefined;
|
||||
|
||||
private _onLinkHover = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
private _onLinkLeave = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
|
||||
private _onShowLinkUnderline = new EventEmitter<ILinkifierEvent>();
|
||||
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
|
||||
private _onHideLinkUnderline = new EventEmitter<ILinkifierEvent>();
|
||||
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
|
||||
|
||||
constructor(
|
||||
private readonly _bufferService: IBufferService
|
||||
@@ -67,6 +73,20 @@ export class Linkifier2 implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the event if it's an embedder created hover widget
|
||||
const composedPath = event.composedPath() as HTMLElement[];
|
||||
for (let i = 0; i < composedPath.length; i++) {
|
||||
const target = composedPath[i];
|
||||
// Hit Terminal.element, break and continue
|
||||
if (target.classList.contains('xterm')) {
|
||||
break;
|
||||
}
|
||||
// It's a hover, don't respect hover event
|
||||
if (target.classList.contains('xterm-hover')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) {
|
||||
this._onHover(position);
|
||||
this._lastBufferCell = position;
|
||||
@@ -151,6 +171,7 @@ export class Linkifier2 implements ILinkifier2 {
|
||||
if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) {
|
||||
this._linkLeave(this._element, this._currentLink, this._lastMouseEvent);
|
||||
this._currentLink = undefined;
|
||||
this._currentLinkState = undefined;
|
||||
this._linkCacheDisposables.forEach(l => l.dispose());
|
||||
this._linkCacheDisposables = [];
|
||||
}
|
||||
@@ -170,8 +191,28 @@ export class Linkifier2 implements ILinkifier2 {
|
||||
// Trigger hover if the we have a link at the position
|
||||
if (this._linkAtPosition(link, position)) {
|
||||
this._currentLink = link;
|
||||
this._currentLinkState = {
|
||||
hideDecorations: link.hideDecorations || false,
|
||||
isHovered: true
|
||||
};
|
||||
this._linkHover(this._element, link, this._lastMouseEvent);
|
||||
|
||||
// Add listener for tracking hideDecorations changes
|
||||
Object.defineProperties(link, {
|
||||
hideDecorations: {
|
||||
get: () => this._currentLinkState?.hideDecorations,
|
||||
set: v => {
|
||||
if (this._currentLinkState && this._currentLinkState.hideDecorations !== v) {
|
||||
this._currentLinkState.hideDecorations = v;
|
||||
if (this._currentLinkState?.isHovered) {
|
||||
this._fireUnderlineEvent(link, !v);
|
||||
this._element?.classList.toggle('xterm-cursor-pointer', !v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add listener for rerendering
|
||||
if (this._renderService) {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
|
||||
@@ -182,25 +223,35 @@ export class Linkifier2 implements ILinkifier2 {
|
||||
}
|
||||
|
||||
protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void {
|
||||
const range = link.range;
|
||||
const scrollOffset = this._bufferService.buffer.ydisp;
|
||||
|
||||
if (!link.hideUnderline) {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined));
|
||||
if (!link.hideDecorations) {
|
||||
this._fireUnderlineEvent(link, true);
|
||||
element.classList.add('xterm-cursor-pointer');
|
||||
}
|
||||
if (this._currentLinkState) {
|
||||
this._currentLinkState.isHovered = true;
|
||||
}
|
||||
element.classList.add('xterm-cursor-pointer');
|
||||
|
||||
if (link.hover) {
|
||||
link.hover(event, link.text);
|
||||
}
|
||||
}
|
||||
|
||||
protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void {
|
||||
private _fireUnderlineEvent(link: ILink, showEvent: boolean): void {
|
||||
const range = link.range;
|
||||
const scrollOffset = this._bufferService.buffer.ydisp;
|
||||
const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined);
|
||||
const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline;
|
||||
emitter.fire(event);
|
||||
}
|
||||
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined));
|
||||
element.classList.remove('xterm-cursor-pointer');
|
||||
protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void {
|
||||
if (!link.hideDecorations) {
|
||||
this._fireUnderlineEvent(link, false);
|
||||
element.classList.remove('xterm-cursor-pointer');
|
||||
}
|
||||
if (this._currentLinkState) {
|
||||
this._currentLinkState.isHovered = false;
|
||||
}
|
||||
|
||||
if (link.leave) {
|
||||
link.leave(event, link.text);
|
||||
@@ -240,7 +291,7 @@ export class Linkifier2 implements ILinkifier2 {
|
||||
return { x: coords[0], y: coords[1] + this._bufferService.buffer.ydisp };
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
|
||||
private _createLinkUnderlineEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
|
||||
return { x1, y1, x2, y2, cols: this._bufferService.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-5
@@ -96,8 +96,8 @@ export interface ILinkifierEvent {
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void;
|
||||
@@ -107,8 +107,8 @@ export interface ILinkifier {
|
||||
}
|
||||
|
||||
export interface ILinkifier2 {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
@@ -172,7 +172,7 @@ interface ILinkProvider {
|
||||
interface ILink {
|
||||
range: IBufferRange;
|
||||
text: string;
|
||||
hideUnderline?: boolean;
|
||||
hideDecorations?: boolean;
|
||||
activate(event: MouseEvent, text: string): void;
|
||||
hover?(event: MouseEvent, text: string): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
|
||||
@@ -24,11 +24,11 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
|
||||
linkifier2.onLinkHover(e => this._onLinkHover(e));
|
||||
linkifier2.onLinkLeave(e => this._onLinkLeave(e));
|
||||
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -53,7 +53,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (e.fg && is256Color(e.fg)) {
|
||||
@@ -77,7 +77,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
this._screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
this._linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
this._linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e));
|
||||
this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e));
|
||||
|
||||
this._linkifier2.onLinkHover(e => this._onLinkHover(e));
|
||||
this._linkifier2.onLinkLeave(e => this._onLinkLeave(e));
|
||||
this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e));
|
||||
this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
Vendored
+6
-3
@@ -1121,9 +1121,10 @@ declare module 'xterm' {
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* Whether to hide the link's underline.
|
||||
* Whether to hide the underline and cursor styles, this property is tracked and changes made
|
||||
* after the link is provided will trigger changes.
|
||||
*/
|
||||
hideUnderline?: boolean;
|
||||
hideDecorations?: boolean;
|
||||
|
||||
/**
|
||||
* Calls when the link is activated.
|
||||
@@ -1133,7 +1134,9 @@ declare module 'xterm' {
|
||||
activate(event: MouseEvent, text: string): void;
|
||||
|
||||
/**
|
||||
* Called when the mouse hovers the link.
|
||||
* Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip,
|
||||
* create the hover element within `Terminal.element` and add the `xterm-hover` class to it,
|
||||
* that will cause mouse events to not fall through and activate other links.
|
||||
* @param event The mouse event triggering the callback.
|
||||
* @param text The text of the link.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user