fix: Refactor hover previews to reduce false positives (#6091)
This commit is contained in:
64
shared/editor/extensions/HoverPreviews.ts
Normal file
64
shared/editor/extensions/HoverPreviews.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import Extension from "../lib/Extension";
|
||||
|
||||
interface HoverPreviewsOptions {
|
||||
/** Callback when a hover target is found or lost. */
|
||||
onHoverLink?: (target: Element | null) => void;
|
||||
|
||||
/** Delay before the target is considered "hovered" and callback is triggered. */
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export default class HoverPreviews extends Extension {
|
||||
get defaultOptions(): HoverPreviewsOptions {
|
||||
return {
|
||||
delay: 500,
|
||||
};
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "hover-previews";
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
const isHoverTarget = (target: Element | null, view: EditorView) =>
|
||||
target instanceof HTMLElement &&
|
||||
this.editor.elementRef.current?.contains(target) &&
|
||||
(!view.editable || (view.editable && !view.hasFocus()));
|
||||
|
||||
let hoveringTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mouseover: (view: EditorView, event: MouseEvent) => {
|
||||
const target = (event.target as HTMLElement)?.closest(
|
||||
".use-hover-preview"
|
||||
);
|
||||
if (isHoverTarget(target, view)) {
|
||||
if (this.options.onHoverLink) {
|
||||
hoveringTimeout = setTimeout(() => {
|
||||
this.options.onHoverLink?.(target);
|
||||
}, this.options.delay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
mouseout: (view: EditorView, event: MouseEvent) => {
|
||||
const target = (event.target as HTMLElement)?.closest(
|
||||
".use-hover-preview"
|
||||
);
|
||||
if (isHoverTarget(target, view)) {
|
||||
clearTimeout(hoveringTimeout);
|
||||
this.options.onHoverLink?.(null);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export default class Link extends Mark {
|
||||
{
|
||||
title: node.attrs.title,
|
||||
href: sanitizeUrl(node.attrs.href),
|
||||
class: "text-link",
|
||||
class: "use-hover-preview",
|
||||
rel: "noopener noreferrer nofollow",
|
||||
},
|
||||
0,
|
||||
@@ -203,20 +203,6 @@ export default class Link extends Mark {
|
||||
props: {
|
||||
decorations: (state: EditorState) => plugin.getState(state),
|
||||
handleDOMEvents: {
|
||||
mouseover: (view: EditorView, event: MouseEvent) => {
|
||||
const target = (event.target as HTMLElement)?.closest("a");
|
||||
if (
|
||||
target instanceof HTMLAnchorElement &&
|
||||
target.className.includes("text-link") &&
|
||||
this.editor.elementRef.current?.contains(target) &&
|
||||
(!view.editable || (view.editable && !view.hasFocus()))
|
||||
) {
|
||||
if (this.options.onHoverLink) {
|
||||
return this.options.onHoverLink(target);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
mousedown: (view: EditorView, event: MouseEvent) => {
|
||||
const target = (event.target as HTMLElement)?.closest("a");
|
||||
if (!(target instanceof HTMLAnchorElement) || event.button !== 0) {
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
NodeType,
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
import { Command, Plugin, TextSelection } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { Command, TextSelection } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import Suggestion from "../extensions/Suggestion";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
@@ -64,7 +63,7 @@ export default class Mention extends Suggestion {
|
||||
toDOM: (node) => [
|
||||
"span",
|
||||
{
|
||||
class: `${node.type.name}`,
|
||||
class: `${node.type.name} use-hover-preview`,
|
||||
id: node.attrs.id,
|
||||
"data-type": node.attrs.type,
|
||||
"data-id": node.attrs.modelId,
|
||||
@@ -81,31 +80,6 @@ export default class Mention extends Suggestion {
|
||||
return [mentionRule];
|
||||
}
|
||||
|
||||
get plugins(): Plugin[] {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mouseover: (view: EditorView, event: MouseEvent) => {
|
||||
const target = (event.target as HTMLElement)?.closest("span");
|
||||
if (
|
||||
target instanceof HTMLSpanElement &&
|
||||
this.editor.elementRef.current?.contains(target) &&
|
||||
target.className.includes("mention") &&
|
||||
(!view.editable || (view.editable && !view.hasFocus()))
|
||||
) {
|
||||
if (this.options.onHoverLink) {
|
||||
return this.options.onHoverLink(target);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType; schema: Schema }) {
|
||||
return (attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import ClipboardTextSerializer from "../extensions/ClipboardTextSerializer";
|
||||
import DateTime from "../extensions/DateTime";
|
||||
import FindAndReplace from "../extensions/FindAndReplace";
|
||||
import History from "../extensions/History";
|
||||
import HoverPreviews from "../extensions/HoverPreviews";
|
||||
import Keys from "../extensions/Keys";
|
||||
import MaxLength from "../extensions/MaxLength";
|
||||
import PasteHandler from "../extensions/PasteHandler";
|
||||
@@ -113,6 +114,7 @@ export const richExtensions: Nodes = [
|
||||
MathBlock,
|
||||
PreventTab,
|
||||
FindAndReplace,
|
||||
HoverPreviews,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user