fix: Refactor hover previews to reduce false positives (#6091)

This commit is contained in:
Tom Moor
2023-10-29 18:31:12 -04:00
committed by GitHub
parent 90bc60d4cf
commit 6b13a32234
9 changed files with 265 additions and 212 deletions

View 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;
},
},
},
}),
];
}
}

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -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,
];
/**