Skip to content
File

Blob: src/client/components/editor/extensions/create-editor-extensions.ts

typescript130 lines
1import type { AnyExtension } from "@tiptap/core";
2import Typography from "@tiptap/extension-typography";
3import CharacterCount from "@tiptap/extension-character-count";
4import { StarterKit } from "@tiptap/starter-kit";
5import { TextStyle, Color, BackgroundColor } from "@tiptap/extension-text-style";
6import { TextAlign } from "@tiptap/extension-text-align";
7import { Collaboration } from "@tiptap/extension-collaboration";
8import { Placeholder } from "@tiptap/extension-placeholder";
9import { TaskList } from "@tiptap/extension-task-list";
10import { TaskItem } from "@tiptap/extension-task-item";
11import { FileHandler } from "@tiptap/extension-file-handler";
12import type * as Y from "yjs";
13import type { Awareness } from "y-protocols/awareness";
14import { countCharacters, countWords } from "@/shared/editor/schema";
15import type { ResolveIdentity } from "@/client/lib/presence-identity";
16import { createCollaborationCaret } from "./collaboration-caret";
17import { ShareAwareImage } from "./image/node";
18import { EditorEmoji } from "./emoji";
19import { HighlightedCodeBlock } from "./code-block/extension";
20import { BlockDragDropBehavior } from "./block-drag-drop";
21import { AiSuggestion } from "./ai-suggestion";
22import { AiGenerateIndicator } from "./ai-generate-indicator";
23import { CalloutExtension } from "./callout";
24import { DetailsBlockExtensions } from "./details-block";
25import { createTableExtensions } from "./table-extensions";
26import { ContextAwareSelectAll } from "./context-aware-select-all";
27import { TopLevelBlockIdentity } from "./top-level-block-identity";
28import { PageMentionNode } from "./page-mention/node";
29import { PageMentionSuggestion } from "./page-mention/suggestion";
30import { SlashCommands } from "../controllers/slash/extension";
31import { createInsertPaletteItems } from "../lib/insert-palette";
32import { createImageFileHandlerConfig } from "../lib/media-actions";
33import type { EditorRuntimeSnapshot } from "../editor-runtime-context";
34import type { EditorAffordance } from "@/client/lib/affordance/editor";
35import type { PageMentionCandidate } from "@/client/components/page-mention/types";
36 
37interface CreateEditorExtensionsOpts {
38 fragment: Y.XmlFragment;
39 provider: { awareness: Awareness };
40 user: { userId: string | null };
41 resolveIdentity: ResolveIdentity;
42 getRuntime: () => EditorRuntimeSnapshot;
43 getAffordance: () => EditorAffordance;
44 getPageMentionCandidates: (excludePageId: string | undefined) => PageMentionCandidate[];
45}
46 
47export function createEditorExtensions(opts: CreateEditorExtensionsOpts): AnyExtension[] {
48 const { fragment, provider, user, resolveIdentity, getRuntime, getAffordance, getPageMentionCandidates } = opts;
49 const getInsertPaletteItems = () =>
50 createInsertPaletteItems({
51 getRuntime,
52 getAffordance,
53 getPageMentionCandidates,
54 });
55 
56 return [
57 StarterKit.configure({
58 undoRedo: false,
59 dropcursor: false,
60 link: { openOnClick: false, autolink: true },
61 codeBlock: false,
62 }),
63 // Keep quote and ellipsis fixes for prose, but avoid operator-style
64 // rewrites that are risky in technical and code-adjacent writing.
65 Typography.configure({
66 emDash: false,
67 openDoubleQuote: false,
68 closeDoubleQuote: false,
69 openSingleQuote: false,
70 closeSingleQuote: false,
71 leftArrow: false,
72 rightArrow: false,
73 copyright: false,
74 trademark: false,
75 servicemark: false,
76 registeredTrademark: false,
77 oneHalf: false,
78 plusMinus: false,
79 notEqual: false,
80 laquo: false,
81 raquo: false,
82 multiplication: false,
83 superscriptTwo: false,
84 superscriptThree: false,
85 oneQuarter: false,
86 threeQuarters: false,
87 }),
88 EditorEmoji,
89 CharacterCount.configure({
90 textCounter: countCharacters,
91 wordCounter: countWords,
92 }),
93 TextStyle,
94 Color,
95 BackgroundColor,
96 TextAlign.configure({ types: ["heading", "paragraph"] }),
97 ...DetailsBlockExtensions,
98 CalloutExtension,
99 HighlightedCodeBlock.configure({
100 defaultLanguage: "text",
101 enableTabIndentation: true,
102 }),
103 Collaboration.configure({ fragment }),
104 createCollaborationCaret({ provider, user, resolveIdentity }),
105 Placeholder.configure({
106 placeholder: "Start typing, or press / for blocks",
107 }),
108 TopLevelBlockIdentity,
109 BlockDragDropBehavior,
110 AiSuggestion,
111 AiGenerateIndicator,
112 ShareAwareImage.configure({ inline: false, allowBase64: false, getRuntime }),
113 TaskList,
114 TaskItem.configure({ nested: true }),
115 FileHandler.configure(createImageFileHandlerConfig({ getRuntime, getAffordance })),
116 SlashCommands.configure({ getItems: getInsertPaletteItems }),
117 PageMentionNode.configure({ getAffordance }),
118 PageMentionSuggestion.configure({
119 getCurrentPageId: () => getRuntime().pageId,
120 isAvailable: (editor) => editor.isEditable && getAffordance().canInsertPageMentions,
121 getCandidates: () => getPageMentionCandidates(getRuntime().pageId),
122 }),
123 ...createTableExtensions(),
124 // Registered last so Tiptap's keymap resolver (runs most-recently-added
125 // shortcuts first) picks this Mod-a handler before StarterKit's default
126 // selectAll, enabling the container-scoped escalation ladder.
127 ContextAwareSelectAll,
128 ] as AnyExtension[];
129}