@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
1 lines • 10.9 kB
Source Map (JSON)
{"version":3,"file":"UserStore-DCG6lsla.cjs","names":[],"sources":["../src/extensions/ShowSelection/ShowSelection.ts","../src/user/UserStore.ts"],"sourcesContent":["import { Plugin, PluginKey } from \"prosemirror-state\";\nimport { Decoration, DecorationSet } from \"prosemirror-view\";\nimport {\n createExtension,\n createStore,\n} from \"../../editor/BlockNoteExtension.js\";\n\nconst PLUGIN_KEY = new PluginKey(`blocknote-show-selection`);\n\n/**\n * Plugin that shows adds a decoration around the current selection\n * This can be used to highlight the current selection in the UI even when the\n * text editor is not focused.\n */\nexport const ShowSelectionExtension = createExtension(({ editor }) => {\n const store = createStore(\n { enabledSet: new Set<string>() },\n {\n onUpdate() {\n editor.transact((tr) => tr.setMeta(PLUGIN_KEY, {}));\n },\n },\n );\n return {\n key: \"showSelection\",\n store,\n prosemirrorPlugins: [\n new Plugin({\n key: PLUGIN_KEY,\n props: {\n decorations: (state) => {\n const { doc, selection } = state;\n if (store.state.enabledSet.size === 0) {\n return DecorationSet.empty;\n }\n const dec = Decoration.inline(selection.from, selection.to, {\n \"data-show-selection\": \"true\",\n });\n return DecorationSet.create(doc, [dec]);\n },\n },\n }),\n ],\n /**\n * Show or hide the selection decoration\n *\n * @param shouldShow - Whether to show the selection decoration\n * @param key - The key of the selection to show or hide,\n * this is necessary to prevent disabling ShowSelection from one place\n * will interfere with other parts of the code that need to show the selection decoration\n * (e.g.: CreateLinkButton and AIExtension)\n */\n showSelection(shouldShow: boolean, key: string) {\n store.setState({\n enabledSet: shouldShow\n ? new Set([...store.state.enabledSet, key])\n : new Set([...store.state.enabledSet].filter((k) => k !== key)),\n });\n },\n } as const;\n});\n","import { Store } from \"@tanstack/store\";\nimport { createStore } from \"../editor/BlockNoteExtension.js\";\n\n/**\n * A collaborator of the document.\n */\nexport type User = {\n /**\n * The {@link User}'s unique identifier\n */\n id: string;\n /**\n * The {@link User}'s name/label\n */\n username: string;\n /**\n * The {@link User}'s profile image\n */\n avatarUrl: string;\n /**\n * The color used to represent the user (e.g. for collaboration cursors).\n */\n color?: string;\n /**\n * A lighter variant of {@link color}.\n */\n colorLight?: string;\n};\n\n/**\n * A store that retrieves and caches information about users, generic over the\n * resolved user type `U`.\n *\n * Created via {@link createUserStore}. Features that need to resolve user ids to\n * user information (comments, suggestions, versions) build one internally and\n * expose it on their extension instance so their UI can read from it.\n */\nexport type UserStore<U extends User = User> = {\n /**\n * A store mapping user ids to the resolved {@link User} information.\n */\n store: Store<Map<User[\"id\"], U>>;\n /**\n * Load information about users based on an array of user ids.\n *\n * Users that are already cached or currently being loaded are skipped, so\n * it is safe to call this often (e.g. on every render).\n */\n loadUsers: (userIds: User[\"id\"][]) => Promise<void>;\n /**\n * Re-fetch information about users, ignoring the cache. Users that are\n * currently being loaded are still skipped to avoid duplicate requests.\n */\n refetchUsers: (userIds: User[\"id\"][]) => Promise<void>;\n /**\n * Retrieve information about a user based on their id, if cached.\n *\n * The user has to be loaded via `loadUsers` first.\n */\n getUser: (userId: User[\"id\"]) => U | undefined;\n /**\n * Manually set information about a user. This is useful if you have a\n * resolver that returns partial information (e.g. just the username) and you\n * want to fill in the rest later (e.g. avatarUrl).\n */\n setUser: (user: U | U[]) => void;\n};\n\nexport type UserStoreResolver<U extends User = User> = (\n /**\n * The user ids to resolve. The resolver should return information for all of\n * these users, or an empty array if none could be resolved.\n */\n userIds: User[\"id\"][],\n /**\n * The {@link UserStore} that is calling this resolver. This allows you to return a user synchronously, and update the store later if you need to fetch additional information asynchronously.\n */\n store: UserStore<any>,\n) => Promise<U[]>;\n\n/**\n * A resolver callback or an already-built {@link UserStore} — the shape that\n * user-facing options (comments, collaboration) accept so callers can either let\n * the extension build a store or pass a shared one.\n */\nexport type UserStoreOrResolver<U extends User = User> =\n | ((userIds: User[\"id\"][], store: UserStore<any>) => Promise<U[]>)\n | UserStore<any>;\n\n/**\n * Creates a {@link UserStore} that retrieves and caches information about users.\n *\n * It does this by calling `resolveUsers` for users that are not yet cached, and\n * stores the results in a BlockNote store so they can be subscribed to (e.g. via\n * `useStore` in React).\n *\n * `resolveUsers` is called with the ids of users that are not yet cached, and\n * should return the information for those users. The type of the returned users\n * is inferred and flows through to {@link UserStore.getUser} and the store, so\n * you can return a type with additional properties and have them be reported\n * back.\n *\n * See [Comments](https://www.blocknotejs.org/docs/features/collaboration/comments) for more info.\n */\nexport function createUserStore<U extends User = User>(\n resolveUsers: (userIds: User[\"id\"][], store: UserStore<any>) => Promise<U[]>,\n): UserStore<U> {\n if (!resolveUsers) {\n throw new Error(\"resolveUsers is required to create a user store\");\n }\n\n const store = createStore(new Map<User[\"id\"], U>());\n\n // Tracks users that are currently being fetched, to avoid duplicate\n // in-flight requests. This is intentionally kept out of the store as it is\n // not state that consumers need to subscribe to.\n const loadingUsers = new Set<User[\"id\"]>();\n\n const userStore: UserStore<U> = {\n store,\n async loadUsers(userIds) {\n const missingUsers = userIds.filter(\n (id) => !store.state.has(id) && !loadingUsers.has(id),\n );\n await fetchUsers(missingUsers);\n },\n async refetchUsers(userIds) {\n const usersToFetch = userIds.filter((id) => !loadingUsers.has(id));\n await fetchUsers(usersToFetch);\n },\n getUser(userId) {\n return store.state.get(userId);\n },\n setUser(users) {\n const usersArray = Array.isArray(users) ? users : [users];\n store.setState((prevState) => {\n const nextState = new Map(prevState);\n for (const user of usersArray) {\n nextState.set(user.id, user);\n }\n return nextState;\n });\n },\n };\n\n async function fetchUsers(userIds: User[\"id\"][]) {\n if (userIds.length === 0) {\n return;\n }\n\n for (const id of userIds) {\n loadingUsers.add(id);\n }\n\n try {\n const users = await resolveUsers(userIds, userStore);\n // Only update the store if any users were actually resolved. Emitting\n // an update when nothing changed (e.g. when the resolver can't find a\n // user) would needlessly notify subscribers and, combined with a\n // subscriber that re-triggers loading, could cause an infinite loop.\n // See https://github.com/TypeCellOS/BlockNote/issues/1548\n if (users.length > 0) {\n store.setState((prevState) => {\n const nextState = new Map(prevState);\n for (const user of users) {\n nextState.set(user.id, user);\n }\n return nextState;\n });\n }\n } finally {\n for (const id of userIds) {\n // Remove the users from the loading set. On a next call to `loadUsers`\n // we will either return the cached user, or retry loading the user if\n // the request failed.\n loadingUsers.delete(id);\n }\n }\n }\n\n return userStore;\n}\n\n/**\n * Normalize a {@link UserStoreOrResolver} to a {@link UserStore}:\n * - an existing store is returned as-is (so a single de-duped cache can be\n * shared across extensions),\n * - a resolver function is wrapped with {@link createUserStore},\n * - `undefined` yields an empty store that resolves nothing (consumers then fall\n * back to showing raw user ids).\n */\nexport function normalizeToUserStore<U extends User = User>(\n resolveUsersOrStore?: UserStoreOrResolver<U>,\n): UserStore<U> {\n if (typeof resolveUsersOrStore === \"function\") {\n return createUserStore(resolveUsersOrStore);\n }\n return resolveUsersOrStore ?? createUserStore<U>(async () => []);\n}\n"],"mappings":"sHAOA,IAAM,EAAa,IAAI,EAAA,UAAU,0BAA0B,EAO9C,EAAyB,EAAA,GAAiB,CAAE,YAAa,CACpE,IAAM,EAAQ,EAAA,EACZ,CAAE,WAAY,IAAI,GAAc,EAChC,CACE,UAAW,CACT,EAAO,SAAU,GAAO,EAAG,QAAQ,EAAY,CAAC,CAAC,CAAC,CACpD,CACF,CACF,EACA,MAAO,CACL,IAAK,gBACL,QACA,mBAAoB,CAClB,IAAI,EAAA,OAAO,CACT,IAAK,EACL,MAAO,CACL,YAAc,GAAU,CACtB,GAAM,CAAE,MAAK,aAAc,EAC3B,GAAI,EAAM,MAAM,WAAW,OAAS,EAClC,OAAO,EAAA,cAAc,MAEvB,IAAM,EAAM,EAAA,WAAW,OAAO,EAAU,KAAM,EAAU,GAAI,CAC1D,sBAAuB,MACzB,CAAC,EACD,OAAO,EAAA,cAAc,OAAO,EAAK,CAAC,CAAG,CAAC,CACxC,CACF,CACF,CAAC,CACH,EAUA,cAAc,EAAqB,EAAa,CAC9C,EAAM,SAAS,CACb,WAAY,EACR,IAAI,IAAI,CAAC,GAAG,EAAM,MAAM,WAAY,CAAG,CAAC,EACxC,IAAI,IAAI,CAAC,GAAG,EAAM,MAAM,UAAU,EAAE,OAAQ,GAAM,IAAM,CAAG,CAAC,CAClE,CAAC,CACH,CACF,CACF,CAAC,EC4CD,SAAgB,EACd,EACc,CACd,GAAI,CAAC,EACH,MAAU,MAAM,iDAAiD,EAGnE,IAAM,EAAQ,EAAA,EAAY,IAAI,GAAoB,EAK5C,EAAe,IAAI,IAEnB,EAA0B,CAC9B,QACA,MAAM,UAAU,EAAS,CAIvB,MAAM,EAHe,EAAQ,OAC1B,GAAO,CAAC,EAAM,MAAM,IAAI,CAAE,GAAK,CAAC,EAAa,IAAI,CAAE,CAErC,CAAY,CAC/B,EACA,MAAM,aAAa,EAAS,CAE1B,MAAM,EADe,EAAQ,OAAQ,GAAO,CAAC,EAAa,IAAI,CAAE,CAC/C,CAAY,CAC/B,EACA,QAAQ,EAAQ,CACd,OAAO,EAAM,MAAM,IAAI,CAAM,CAC/B,EACA,QAAQ,EAAO,CACb,IAAM,EAAa,MAAM,QAAQ,CAAK,EAAI,EAAQ,CAAC,CAAK,EACxD,EAAM,SAAU,GAAc,CAC5B,IAAM,EAAY,IAAI,IAAI,CAAS,EACnC,IAAK,IAAM,KAAQ,EACjB,EAAU,IAAI,EAAK,GAAI,CAAI,EAE7B,OAAO,CACT,CAAC,CACH,CACF,EAEA,eAAe,EAAW,EAAuB,CAC3C,KAAQ,SAAW,EAIvB,KAAK,IAAM,KAAM,EACf,EAAa,IAAI,CAAE,EAGrB,GAAI,CACF,IAAM,EAAQ,MAAM,EAAa,EAAS,CAAS,EAM/C,EAAM,OAAS,GACjB,EAAM,SAAU,GAAc,CAC5B,IAAM,EAAY,IAAI,IAAI,CAAS,EACnC,IAAK,IAAM,KAAQ,EACjB,EAAU,IAAI,EAAK,GAAI,CAAI,EAE7B,OAAO,CACT,CAAC,CAEL,QAAU,CACR,IAAK,IAAM,KAAM,EAIf,EAAa,OAAO,CAAE,CAE1B,CA1BqB,CA2BvB,CAEA,OAAO,CACT,CAUA,SAAgB,EACd,EACc,CAId,OAHI,OAAO,GAAwB,WAC1B,EAAgB,CAAmB,EAErC,GAAuB,EAAmB,SAAY,CAAC,CAAC,CACjE"}