@@ -7,41 +7,173 @@ import { SuggestionMenuProseMirrorPlugin } from "../../extensions/SuggestionMenu
77import { TableHandlesProsemirrorPlugin } from "../../extensions/TableHandles/TableHandlesPlugin.js" ;
88import { BlockNoteExtension } from "../BlockNoteExtension.js" ;
99import { BlockNoteEditor } from "../BlockNoteEditor.js" ;
10+ import { Extension , ExtensionFactory } from "./extensions/types.js" ;
1011
1112export class ExtensionManager {
12- constructor ( private editor : BlockNoteEditor ) { }
13+ private extensions : Map <
14+ string ,
15+ {
16+ instance : Extension ;
17+ unmount : ( ) => void ;
18+ abortController : AbortController ;
19+ }
20+ > = new Map ( ) ;
21+ private extensionFactories : WeakMap < ExtensionFactory , Extension > =
22+ new WeakMap ( ) ;
23+ constructor ( private editor : BlockNoteEditor ) {
24+ editor . onMount ( ( ) => {
25+ for ( const extension of this . extensions . values ( ) ) {
26+ if ( extension . instance . init ) {
27+ const unmountCallback = extension . instance . init ( {
28+ dom : editor . prosemirrorView . dom ,
29+ root : editor . prosemirrorView . root ,
30+ abortController : extension . abortController ,
31+ } ) ;
32+ extension . unmount = ( ) => {
33+ unmountCallback ?.( ) ;
34+ extension . abortController . abort ( ) ;
35+ } ;
36+ }
37+ }
38+ } ) ;
39+
40+ editor . onUnmount ( ( ) => {
41+ for ( const extension of this . extensions . values ( ) ) {
42+ if ( extension . unmount ) {
43+ extension . unmount ( ) ;
44+ }
45+ }
46+ } ) ;
47+ }
1348
1449 /**
15- * Shorthand to get a typed extension from the editor, by
16- * just passing in the extension class.
17- *
18- * @param ext - The extension class to get
19- * @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
20- * @returns The extension instance
50+ * Get all extensions
2151 */
22- public extension < T extends BlockNoteExtension > (
23- ext : { new ( ...args : any [ ] ) : T } & typeof BlockNoteExtension ,
24- key = ext . key ( ) ,
25- ) : T {
26- const extension = this . editor . extensions [ key ] as T ;
27- if ( ! extension ) {
28- throw new Error ( `Extension ${ key } not found` ) ;
52+ public getExtensions ( ) {
53+ return this . extensions ;
54+ }
55+
56+ /**
57+ * Add an extension to the editor after initialization
58+ */
59+ public addExtension < T extends ExtensionFactory | Extension > (
60+ extension : T ,
61+ ) : T extends ExtensionFactory ? ReturnType < T > : T {
62+ if (
63+ typeof extension === "function" &&
64+ this . extensionFactories . has ( extension )
65+ ) {
66+ return this . extensionFactories . get ( extension ) as any ;
2967 }
30- return extension ;
68+
69+ if (
70+ typeof extension === "object" &&
71+ "key" in extension &&
72+ this . extensions . has ( extension . key )
73+ ) {
74+ return this . extensions . get ( extension . key ) as any ;
75+ }
76+
77+ const abortController = new AbortController ( ) ;
78+ let instance : Extension ;
79+ if ( typeof extension === "function" ) {
80+ instance = extension ( this . editor ) ;
81+ this . extensionFactories . set ( extension , instance ) ;
82+ } else {
83+ instance = extension ;
84+ }
85+
86+ let unmountCallback : undefined | ( ( ) => void ) = undefined ;
87+
88+ this . extensions . set ( instance . key , {
89+ instance,
90+ unmount : ( ) => {
91+ unmountCallback ?.( ) ;
92+ abortController . abort ( ) ;
93+ } ,
94+ abortController,
95+ } ) ;
96+
97+ for ( const plugin of instance . plugins || [ ] ) {
98+ this . editor . _tiptapEditor . registerPlugin ( plugin ) ;
99+ }
100+
101+ if ( "inputRules" in instance ) {
102+ // TODO do we need to add new input rules to the editor?
103+ // And other things?
104+ }
105+
106+ if ( ! this . editor . headless && instance . init ) {
107+ unmountCallback =
108+ instance . init ( {
109+ dom : this . editor . prosemirrorView . dom ,
110+ root : this . editor . prosemirrorView . root ,
111+ abortController,
112+ } ) || undefined ;
113+ }
114+
115+ return instance as any ;
31116 }
32117
33118 /**
34- * Get all extensions
119+ * Remove an extension from the editor
120+ * @param extension - The extension to remove
121+ * @returns The extension that was removed
35122 */
36- public getExtensions ( ) {
37- return this . editor . extensions ;
123+ public removeExtension < T extends ExtensionFactory | Extension | string > (
124+ extension : T ,
125+ ) : undefined {
126+ let extensionKey : string | undefined ;
127+ if ( typeof extension === "string" ) {
128+ extensionKey = extension ;
129+ } else if ( typeof extension === "function" ) {
130+ extensionKey = this . extensionFactories . get ( extension ) ?. key ;
131+ } else {
132+ extensionKey = extension . key ;
133+ }
134+ if ( ! extensionKey ) {
135+ return undefined ;
136+ }
137+ const extensionToDelete = this . extensions . get ( extensionKey ) ;
138+ if ( extensionToDelete ) {
139+ if ( extensionToDelete . unmount ) {
140+ extensionToDelete . unmount ( ) ;
141+ }
142+ this . extensions . delete ( extensionKey ) ;
143+ }
38144 }
39145
40146 /**
41- * Get a specific extension by key
147+ * Get a specific extension by it's instance
42148 */
43- public getExtension ( key : string ) {
44- return this . editor . extensions [ key ] ;
149+ public getExtension < T extends ExtensionFactory | Extension | string > (
150+ extension : T ,
151+ ) :
152+ | ( T extends ExtensionFactory
153+ ? ReturnType < T >
154+ : T extends Extension
155+ ? T
156+ : T extends string
157+ ? Extension
158+ : never )
159+ | undefined {
160+ if ( typeof extension === "string" ) {
161+ if ( ! this . extensions . has ( extension ) ) {
162+ return undefined ;
163+ }
164+ return this . extensions . get ( extension ) as any ;
165+ } else if ( typeof extension === "function" ) {
166+ if ( ! this . extensionFactories . has ( extension ) ) {
167+ return undefined ;
168+ }
169+ return this . extensionFactories . get ( extension ) as any ;
170+ } else if ( typeof extension === "object" && "key" in extension ) {
171+ if ( ! this . extensions . has ( extension . key ) ) {
172+ return undefined ;
173+ }
174+ return this . extensions . get ( extension . key ) as any ;
175+ }
176+ throw new Error ( `Invalid extension type: ${ typeof extension } ` ) ;
45177 }
46178
47179 /**
@@ -51,6 +183,25 @@ export class ExtensionManager {
51183 return key in this . editor . extensions ;
52184 }
53185
186+ /**
187+ * Shorthand to get a typed extension from the editor, by
188+ * just passing in the extension class.
189+ *
190+ * @param ext - The extension class to get
191+ * @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
192+ * @returns The extension instance
193+ */
194+ public extension < T extends BlockNoteExtension > (
195+ ext : { new ( ...args : any [ ] ) : T } & typeof BlockNoteExtension ,
196+ key = ext . key ( ) ,
197+ ) : T {
198+ const extension = this . editor . extensions [ key ] as T ;
199+ if ( ! extension ) {
200+ throw new Error ( `Extension ${ key } not found` ) ;
201+ }
202+ return extension ;
203+ }
204+
54205 // Plugin getters - these provide access to the core BlockNote plugins
55206
56207 /**
0 commit comments