mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
8c716b173a
* chore: Update editor generics * fix: Address PR review on editor generics - Restore null-guard on Link click handler so anchors aren't inert when no onClickLink is provided - Mark onClickLink optional in LinkOptions and openLink command to match runtime - Remove dead `collapsed` option from HeadingOptions - Make ToggleBlock dictionary optional and restore optional-chained access for server-side schema instantiation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
import { action, observable } from "mobx";
|
|
import { Plugin } from "prosemirror-state";
|
|
import type { EditorView } from "prosemirror-view";
|
|
import Extension from "@shared/editor/lib/Extension";
|
|
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
|
import stores from "~/stores";
|
|
import HoverPreview from "~/components/HoverPreview";
|
|
import env from "~/env";
|
|
|
|
/**
|
|
* Options for the HoverPreviews extension.
|
|
*/
|
|
interface HoverPreviewsOptions {
|
|
/** Delay in milliseconds before the target is considered "hovered" and the preview is shown. */
|
|
delay: number;
|
|
}
|
|
|
|
export default class HoverPreviews extends Extension<HoverPreviewsOptions> {
|
|
state: {
|
|
activeLinkElement: HTMLElement | null;
|
|
unfurlId: string | null;
|
|
dataLoading: boolean;
|
|
} = observable({
|
|
activeLinkElement: null,
|
|
unfurlId: null,
|
|
dataLoading: false,
|
|
});
|
|
|
|
get defaultOptions(): HoverPreviewsOptions {
|
|
return {
|
|
delay: 600,
|
|
};
|
|
}
|
|
|
|
get name() {
|
|
return "hover-previews";
|
|
}
|
|
|
|
get allowInReadOnly() {
|
|
return true;
|
|
}
|
|
|
|
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)) {
|
|
hoveringTimeout = setTimeout(
|
|
action(async () => {
|
|
const element = target as HTMLElement;
|
|
|
|
const url =
|
|
element?.getAttribute("href") || element?.dataset.url;
|
|
const documentId = parseDocumentSlug(
|
|
window.location.pathname
|
|
);
|
|
|
|
if (url) {
|
|
const transformedUrl = url.startsWith("/")
|
|
? env.URL + url
|
|
: url;
|
|
|
|
this.state.dataLoading = true;
|
|
|
|
const unfurl = await stores.unfurls.fetchUnfurl({
|
|
url: transformedUrl,
|
|
documentId,
|
|
});
|
|
|
|
if (unfurl) {
|
|
this.state.activeLinkElement = element;
|
|
this.state.unfurlId = transformedUrl;
|
|
} else {
|
|
this.state.activeLinkElement = null;
|
|
}
|
|
|
|
this.state.dataLoading = false;
|
|
}
|
|
}),
|
|
this.options.delay
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
mouseout: action((view: EditorView, event: MouseEvent) => {
|
|
const target = (event.target as HTMLElement)?.closest(
|
|
".use-hover-preview"
|
|
);
|
|
if (isHoverTarget(target, view)) {
|
|
clearTimeout(hoveringTimeout);
|
|
this.state.activeLinkElement = null;
|
|
}
|
|
return false;
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
}
|
|
|
|
widget = () => (
|
|
<HoverPreview
|
|
element={this.state.activeLinkElement}
|
|
unfurlId={this.state.unfurlId}
|
|
dataLoading={this.state.dataLoading}
|
|
onClose={action(() => {
|
|
this.state.activeLinkElement = null;
|
|
this.state.unfurlId = null;
|
|
})}
|
|
/>
|
|
);
|
|
}
|