2025-09-21 11:02:31 -07:00
|
|
|
import type { Node } from "prosemirror-model";
|
|
|
|
|
import { Plugin, PluginKey } from "prosemirror-state";
|
2026-03-20 09:37:02 +00:00
|
|
|
import { DecorationSet } from "prosemirror-view";
|
2025-09-21 11:02:31 -07:00
|
|
|
import type { Suggestion } from "@/lib/db/schema";
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
export interface UISuggestion extends Suggestion {
|
|
|
|
|
selectionStart: number;
|
|
|
|
|
selectionEnd: number;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 11:02:31 -07:00
|
|
|
type Position = {
|
2024-10-30 16:01:24 +05:30
|
|
|
start: number;
|
|
|
|
|
end: number;
|
2025-09-21 11:02:31 -07:00
|
|
|
};
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
function findPositionsInDoc(doc: Node, searchText: string): Position | null {
|
|
|
|
|
let positions: { start: number; end: number } | null = null;
|
|
|
|
|
|
|
|
|
|
doc.nodesBetween(0, doc.content.size, (node, pos) => {
|
|
|
|
|
if (node.isText && node.text) {
|
|
|
|
|
const index = node.text.indexOf(searchText);
|
|
|
|
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
positions = {
|
|
|
|
|
start: pos + index,
|
|
|
|
|
end: pos + index + searchText.length,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
export function projectWithPositions(
|
2024-10-30 16:01:24 +05:30
|
|
|
doc: Node,
|
2025-09-21 11:02:31 -07:00
|
|
|
suggestions: Suggestion[]
|
|
|
|
|
): UISuggestion[] {
|
2024-10-30 16:01:24 +05:30
|
|
|
return suggestions.map((suggestion) => {
|
|
|
|
|
const positions = findPositionsInDoc(doc, suggestion.originalText);
|
|
|
|
|
|
|
|
|
|
if (!positions) {
|
|
|
|
|
return {
|
|
|
|
|
...suggestion,
|
|
|
|
|
selectionStart: 0,
|
|
|
|
|
selectionEnd: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...suggestion,
|
|
|
|
|
selectionStart: positions.start,
|
|
|
|
|
selectionEnd: positions.end,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 11:02:31 -07:00
|
|
|
export const suggestionsPluginKey = new PluginKey("suggestions");
|
2024-10-30 16:01:24 +05:30
|
|
|
export const suggestionsPlugin = new Plugin({
|
|
|
|
|
key: suggestionsPluginKey,
|
|
|
|
|
state: {
|
|
|
|
|
init() {
|
|
|
|
|
return { decorations: DecorationSet.empty, selected: null };
|
|
|
|
|
},
|
|
|
|
|
apply(tr, state) {
|
|
|
|
|
const newDecorations = tr.getMeta(suggestionsPluginKey);
|
2025-09-21 11:02:31 -07:00
|
|
|
if (newDecorations) {
|
|
|
|
|
return newDecorations;
|
|
|
|
|
}
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
decorations: state.decorations.map(tr.mapping, tr.doc),
|
|
|
|
|
selected: state.selected,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
props: {
|
|
|
|
|
decorations(state) {
|
|
|
|
|
return this.getState(state)?.decorations ?? DecorationSet.empty;
|
|
|
|
|
},
|
2026-03-20 09:37:02 +00:00
|
|
|
handleDOMEvents: {
|
|
|
|
|
mousedown(_view, event) {
|
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
|
if (target.closest(".suggestion-highlight")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-10-30 16:01:24 +05:30
|
|
|
},
|
|
|
|
|
});
|