@ecsplico/editor 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/adapters/comments/client.js +2 -2
  4. package/dist/adapters/comments/client.js.map +1 -1
  5. package/dist/adapters/comments/drizzle.js +9 -9
  6. package/dist/adapters/comments/drizzle.js.map +1 -1
  7. package/dist/adapters/comments/errors.js +1 -1
  8. package/dist/adapters/comments/errors.js.map +1 -1
  9. package/dist/adapters/comments/record.js +11 -11
  10. package/dist/adapters/comments/record.js.map +1 -1
  11. package/dist/adapters/comments/server.js +10 -10
  12. package/dist/adapters/comments/server.js.map +1 -1
  13. package/dist/adapters/vue.js +2 -2
  14. package/dist/adapters/vue.js.map +1 -1
  15. package/dist/features/block-ui/block-actions.d.ts +21 -0
  16. package/dist/features/block-ui/block-actions.js +25 -1
  17. package/dist/features/block-ui/block-actions.js.map +1 -1
  18. package/dist/features/block-ui/components/BlockActionsOverlay.js +33 -4
  19. package/dist/features/block-ui/components/BlockActionsOverlay.js.map +1 -1
  20. package/dist/features/block-ui/components/BlockActionsOverlay.vue.d.ts +4 -0
  21. package/dist/features/block-ui/index.d.ts +2 -2
  22. package/dist/features/block-ui/index.js +2 -2
  23. package/dist/features/block-ui/react/BlockActionsOverlay.d.ts +4 -0
  24. package/dist/features/block-ui/react/BlockActionsOverlay.js +38 -12
  25. package/dist/features/block-ui/react/BlockActionsOverlay.js.map +1 -1
  26. package/dist/features/comments/model.js.map +1 -1
  27. package/dist/markdown/MarkdownSurface.vue.d.ts +1 -1
  28. package/dist/tree/tree-host.js +1 -1
  29. package/dist/tree/tree-items.js +1 -1
  30. package/docs/integration.md +3 -1
  31. package/docs/llms.md +1 -1
  32. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\nimport { blockActionsFor, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n */\nexport function BlockActionsOverlay({ editor, target, actions, host, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n onHover: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n })()\n const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : []\n\n const [place, setPlace] = useState<{ top: number, right: number } | null>(null)\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const rect = dom.getBoundingClientRect()\n const top = rect.top - hostRect.top + 2\n const right = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n setPlace(previous => (previous && previous.top === top && previous.right === right ? previous : { top, right }))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const resizeObserver = new ResizeObserver(run)\n resizeObserver.observe(editor.view.dom)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n resizeObserver.disconnect()\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n onHover(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n onHover(false)\n }\n wasVisible.current = visible\n }, [visible, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className=\"block-actions absolute z-10 flex gap-0.5 rounded-md border border-border bg-background p-0.5 shadow-sm\"\n style={{ top: `${place.top}px`, right: `${place.right}px` }}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => { if (!menuOpen.current) onHover(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\nimport { blockActionsFor, blockCornerOf, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n/** Gap between the bar and the block's own corner elements it docks next to. */\nconst CORNER_GAP = 4\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n *\n * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):\n * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements\n * it sits at the editor's right edge, aligned to the block's top.\n */\nexport function BlockActionsOverlay({ editor, target, actions, host, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n onHover: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n })()\n const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : []\n\n // `docked`: the bar sits next to the block's corner elements, and `top` is then their vertical centre.\n const [place, setPlace] = useState<{ top: number, right: number, docked: boolean } | null>(null)\n const resizeObserver = useRef<ResizeObserver | null>(null)\n const watched = useRef<HTMLElement[]>([])\n // The corner elements can change size (a badge appears) without the document changing: watch the current ones.\n const watchCorner = (elements: HTMLElement[]) => {\n const current = watched.current\n if (elements.length === current.length && elements.every((element, index) => element === current[index])) return\n for (const element of current) resizeObserver.current?.unobserve(element)\n watched.current = elements\n for (const element of elements) resizeObserver.current?.observe(element)\n }\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n const next = corner\n ? { top: corner.centerY - hostRect.top, right: hostRect.right - corner.left + CORNER_GAP, docked: true }\n : {\n top: dom.getBoundingClientRect().top - hostRect.top + 2,\n right: hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER,\n docked: false,\n }\n setPlace(previous => (previous && previous.top === next.top && previous.right === next.right && previous.docked === next.docked ? previous : next))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const observer = new ResizeObserver(run)\n resizeObserver.current = observer\n observer.observe(editor.view.dom)\n for (const element of watched.current) observer.observe(element)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n observer.disconnect()\n resizeObserver.current = null\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n onHover(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n onHover(false)\n }\n wasVisible.current = visible\n }, [visible, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5\"\n style={{ top: `${place.top}px`, right: `${place.right}px`, transform: place.docked ? 'translateY(-50%)' : undefined }}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => { if (!menuOpen.current) onHover(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","names":["EMPTY"],"sources":["../../../src/features/comments/model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport * as Y from 'yjs'\nimport type { AuthorIdentity, FeatureContext } from '../../core'\nimport { anchorResolverOf, editorEntryOf, releaseRanges, resolveRange } from '../../core'\nimport { newBlockId } from '../../core/schema'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport { cancelCommentDraft, commentsDoc, commentsKey, type CommentsMeta, commentsUser, setActiveComment, startCommentDraft } from './comments-plugin'\nimport { COMMENT_LIMITS, COMMENTS_FIELD, type CommentAnchor, type CommentReply, type CommentThread } from './schema'\nimport { EMPTY_SNAPSHOT as EMPTY, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from './snapshot-equality'\nimport { captureThreadAnchor, resolveThreadAnchor } from './thread-anchor'\n\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A thread's `Y.Map` as plain data; `replies` converted explicitly so it is always a plain array. */\nfunction snapshotOf(thread: Y.Map<unknown>): CommentThread {\n const replies = thread.get('replies')\n return { ...(thread.toJSON() as CommentThread), replies: replies instanceof Y.Array ? replies.toArray() as CommentReply[] : [] }\n}\n\n/**\n * The comment threads of a collaborative editor, stored in its Y.Doc (`COMMENTS_FIELD`, one nested `Y.Map` per thread,\n * replies in a nested `Y.Array`), so they sync live with the document and persist with its Yjs state. Keeps the\n * `Comments` plugin's `known`/`hidden` thread ids in step with the store (highlights), exposes the plugin's ranges,\n * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and\n * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).\n * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).\n * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves\n * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.\n */\nexport function createCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY)\n\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed ? instance : undefined\n }\n\n // The editor whose transactions are followed, and the Y.Map of threads observed.\n let bound: Editor | undefined\n let observed: { doc: Y.Doc, map: Y.Map<unknown> } | undefined\n let threads: readonly CommentThread[] = NO_THREADS\n let syncQueued = false\n // The thread list the plugin was last told about, for the deactivation rule.\n let told: readonly CommentThread[] = NO_THREADS\n // Threads already tried for an anchor (written, or not capturable), so a failed capture is not repeated on every change.\n const anchorTried = new Set<string>()\n let healQueued = false\n\n function unobserve() {\n observed?.map.unobserveDeep(onThreadsChanged)\n observed = undefined\n }\n\n function onThreadsChanged() {\n if (!observed) return\n threads = [...observed.map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n recompute()\n queueSync()\n }\n\n function onTransaction() {\n recompute()\n }\n\n function unbind() {\n bound?.off('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` follows `isEditable`.\n bound?.off('update', onTransaction)\n bound = undefined\n }\n\n /** The snapshot of the editor's current state; the previous object when nothing changed. */\n function recompute() {\n const instance = current()\n if (!instance) {\n store.set(EMPTY)\n return\n }\n const previous = store.get()\n const ydoc = commentsDoc(instance)\n const user = commentsUser(instance)\n const plugin = commentsKey.getState(instance.state)\n const ranges = plugin?.ranges ?? NO_RANGES\n const orphanAt = orphanPositions(instance, ranges)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = anchor ? resolveRange(instance.state, anchor) : null\n const draftQuote = draft ? instance.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const available = !!ydoc && !!user\n const canComment = available && instance.isEditable\n\n const sameThreads = threads === previous.threads\n const sameRangeMap = sameRanges(ranges, previous.ranges)\n const sameOrphanMap = sameOrphanAt(orphanAt, previous.orphanAt)\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && sameOrphanMap\n && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = sameThreads && sameRangeMap\n const open = kept\n ? previous.open\n : threads\n .filter(t => !t.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt))\n const resolved = sameThreads\n ? previous.resolved\n : threads.filter(t => t.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n store.set({\n available,\n canComment,\n user,\n threads,\n ranges: sameRangeMap ? previous.ranges : ranges,\n orphanAt: sameOrphanMap ? previous.orphanAt : orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open,\n resolved,\n })\n if (available) queueHeal()\n }\n\n /** Where each unresolved thread without a text range was, from its anchor; empty without a Yjs session. */\n function orphanPositions(instance: Editor, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n const resolver = anchorResolverOf(instance)\n if (!resolver) return NO_POSITIONS\n const size = instance.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.resolved || ranges.has(thread.id) || !thread.anchor) continue\n const position = resolveThreadAnchor(instance.state, resolver, thread.anchor)\n if (position !== null && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n // Threads from before anchors get theirs once they are seen with their text in the document (any client with a user\n // writes it; the positions are equivalent, so concurrent writes agree). Queued: not written from inside a Yjs\n // observer or a transaction listener.\n function queueHeal() {\n if (healQueued || !threads.some(t => !t.anchor && !anchorTried.has(t.id))) return\n healQueued = true\n queueMicrotask(() => {\n healQueued = false\n healAnchors()\n })\n }\n\n function healAnchors() {\n const ctx = writable()\n const resolver = ctx && anchorResolverOf(ctx.editor)\n if (!ctx || !resolver || ctx.editor !== bound) return\n const { ranges } = store.get()\n const written: [Y.Map<unknown>, CommentAnchor][] = []\n for (const { id, anchor } of threads) {\n const range = ranges.get(id)\n if (anchor || anchorTried.has(id) || !range) continue\n anchorTried.add(id)\n const thread = threadMap(ctx.map, id)\n const captured = thread && captureThreadAnchor(ctx.editor.state, resolver, range)\n if (thread && captured) written.push([thread, captured])\n }\n if (written.length) ctx.ydoc.transact(() => written.forEach(([thread, captured]) => thread.set('anchor', captured)))\n }\n\n // Highlights appear and disappear on every client as threads arrive, get resolved or deleted. The active thread\n // stays highlighted even when resolved (a resolved thread activated from its card), so a thread that turns resolved\n // or disappears while active is deactivated here, on every client, not only on the one that resolved or deleted it.\n // Queued (not dispatched from inside the Yjs observer or a transaction listener).\n function queueSync() {\n if (syncQueued) return\n syncQueued = true\n queueMicrotask(() => {\n syncQueued = false\n syncPlugin()\n })\n }\n\n function syncPlugin() {\n const instance = current()\n if (!instance || instance !== bound) return\n const value = commentsKey.getState(instance.state)\n if (!value) return\n const known = new Set(threads.map(t => t.id))\n const hidden = new Set(threads.filter(t => t.resolved).map(t => t.id))\n const meta: CommentsMeta = { known, hidden }\n const before = value.active ? told.find(t => t.id === value.active) : undefined\n if (value.active && before && !before.resolved && (!known.has(value.active) || hidden.has(value.active))) meta.active = null\n told = threads\n instance.view.dispatch(instance.state.tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n }\n\n /** Follows the editor `getEditor` names now, and the Y.Doc of its collaboration session. */\n function refresh() {\n const instance = current()\n if (instance !== bound) {\n unbind()\n unobserve()\n bound = instance\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n instance?.on('transaction', onTransaction)\n instance?.on('update', onTransaction)\n // The new editor's plugin knows no threads yet.\n queueSync()\n }\n const ydoc = instance ? commentsDoc(instance) : null\n if ((observed?.doc ?? null) !== ydoc) {\n unobserve()\n if (ydoc) {\n const map = ydoc.getMap<unknown>(COMMENTS_FIELD)\n observed = { doc: ydoc, map }\n map.observeDeep(onThreadsChanged)\n threads = [...map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n }\n else {\n threads = NO_THREADS\n }\n queueSync()\n }\n recompute()\n }\n\n function dispose() {\n unbind()\n unobserve()\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n store.set(EMPTY)\n }\n\n /** Editor, thread map and user while comments are available; null otherwise. */\n function writable() {\n const instance = current()\n const ydoc = instance && commentsDoc(instance)\n const author = instance && commentsUser(instance)\n if (!instance || !ydoc || !author) return null\n return { editor: instance, ydoc, map: ydoc.getMap<unknown>(COMMENTS_FIELD), user: { id: author.id, name: author.name, color: author.color } }\n }\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function threadMap(map: Y.Map<unknown>, threadId: string): Y.Map<unknown> | null {\n const thread = map.get(threadId)\n return thread instanceof Y.Map ? thread : null\n }\n\n function repliesOf(thread: Y.Map<unknown>): Y.Array<CommentReply> | null {\n const replies = thread.get('replies')\n return replies instanceof Y.Array ? replies as Y.Array<CommentReply> : null\n }\n\n function add(text: string): string | null {\n const ctx = writable()\n const body = clean(text)\n const anchor = ctx && commentsKey.getState(ctx.editor.state)?.draft\n const range = ctx && anchor ? resolveRange(ctx.editor.state, anchor) : null\n const type = ctx?.editor.schema.marks.comment\n if (!ctx || !anchor || !range || range.from >= range.to || !body || !type) return null\n const id = newBlockId()\n const { state } = ctx.editor\n const resolver = anchorResolverOf(ctx.editor)\n const threadAnchor = resolver ? captureThreadAnchor(state, resolver, range) : null\n const quote = state.doc.textBetween(range.from, range.to, ' ').slice(0, COMMENT_LIMITS.quote)\n const tr = state.tr.addMark(range.from, range.to, type.create({ id }))\n // Text that takes no marks (code block, inline code): no thread that would start out without a range.\n if (!tr.docChanged) return null\n // Not undoable: the mark is the thread's anchor, not an edit of the text.\n const meta: CommentsMeta = { draft: null, active: id }\n ctx.editor.view.dispatch(tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n releaseRanges(ctx.editor, [anchor])\n const thread = new Y.Map<unknown>()\n thread.set('id', id)\n thread.set('author', ctx.user)\n thread.set('text', body)\n thread.set('createdAt', new Date().toISOString())\n thread.set('quote', quote)\n thread.set('resolved', false)\n if (threadAnchor) thread.set('anchor', threadAnchor)\n thread.set('replies', new Y.Array<CommentReply>())\n ctx.map.set(id, thread)\n return id\n }\n\n function reply(threadId: string, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies || !body) return\n replies.push([{ id: newBlockId(), author: ctx.user, text: body, createdAt: new Date().toISOString() }])\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || !body) return\n const now = new Date().toISOString()\n if (replyId === null) {\n if ((thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id || thread.get('text') === body) return\n ctx.ydoc.transact(() => {\n thread.set('text', body)\n thread.set('editedAt', now)\n })\n return\n }\n const replies = repliesOf(thread)\n const index = replies ? replies.toArray().findIndex(r => r.id === replyId) : -1\n const previous = replies && index >= 0 ? replies.get(index) : undefined\n if (!replies || !previous || previous.author.id !== ctx.user.id || previous.text === body) return\n ctx.ydoc.transact(() => {\n replies.delete(index, 1)\n replies.insert(index, [{ ...previous, text: body, editedAt: now }])\n })\n }\n\n function setResolved(threadId: string, value: boolean): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread) return\n thread.set('resolved', value)\n if (value && commentsKey.getState(ctx.editor.state)?.active === threadId) setActiveComment(ctx.editor, null)\n }\n\n function remove(threadId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || (thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id) return\n const { state } = ctx.editor\n const tr = state.tr\n state.doc.descendants((node, pos) => {\n if (!node.isText) return\n const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.id === threadId)\n if (mark) tr.removeMark(pos, pos + node.nodeSize, mark)\n })\n if (commentsKey.getState(state)?.active === threadId) tr.setMeta(commentsKey, { active: null } satisfies CommentsMeta)\n if (tr.docChanged || tr.getMeta(commentsKey)) ctx.editor.view.dispatch(tr.setMeta('addToHistory', false))\n ctx.map.delete(threadId)\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies) return\n const index = replies.toArray().findIndex(r => r.id === replyId)\n if (index >= 0 && replies.get(index).author.id === ctx.user.id) replies.delete(index, 1)\n }\n\n function activate(id: string | null, options: { reveal?: boolean } = {}): void {\n const instance = current()\n if (instance) setActiveComment(instance, id, options)\n }\n\n function startDraft(): void {\n const ctx = writable()\n if (ctx) startCommentDraft(ctx.editor)\n }\n\n function cancelDraft(): void {\n const instance = current()\n if (instance) cancelCommentDraft(instance)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft }\n}\n\n/** Creates the comments model of one editor, as a host supplies it to `commentsFeatureCore`. */\nexport type CommentsModelFactory = (getEditor: () => Editor | undefined) => CommentsModel\n\n// The host's model factory per surface context, set by the comments feature while the surface builds its extensions.\nconst modelFactories = new WeakMap<FeatureContext, CommentsModelFactory>()\n\n/** Registers the host model factory of `ctx` (the comments feature does this; hosts pass `model` to the feature). */\nexport function provideCommentsModel(ctx: FeatureContext, factory: CommentsModelFactory): void {\n modelFactories.set(ctx, factory)\n}\n\n/** The host model factory the comments feature of `ctx` was configured with; undefined for the default Y.Doc model. */\nexport function commentsModelOf(ctx: FeatureContext): CommentsModelFactory | undefined {\n return modelFactories.get(ctx)\n}\n\n/**\n * The comments model the framework hooks (`useComments`) use: the model of the surface `getEditor` belongs to\n * (`commentsModelOf` of its feature context: the host's `model` factory) and `createCommentsModel` for every other\n * editor. It follows the editor over time: when the editor changes to one of a surface with another model, the inner\n * model is replaced (disposed, created, refreshed) and `state` carries on with the new one. The factory runs once per\n * consumer of this function, so a host model that must share state across them keeps it outside the factory.\n */\nexport function createEditorCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY)\n let inner: { model: CommentsModel, factory: CommentsModelFactory, stop: () => void } | undefined\n\n function release() {\n inner?.stop()\n inner?.model.dispose()\n inner = undefined\n }\n\n /** The inner model for the editor now; a replacement when its surface brings another factory. */\n function current(): CommentsModel {\n const entry = editorEntryOf(getEditor())\n const factory = (entry && commentsModelOf(entry.ctx)) ?? createCommentsModel\n if (inner?.factory !== factory) {\n release()\n const model = factory(getEditor)\n inner = { model, factory, stop: model.state.subscribe(snapshot => store.set(snapshot)) }\n store.set(model.state.get())\n }\n return inner!.model\n }\n\n return {\n state: store,\n refresh: () => current().refresh(),\n dispose: () => {\n release()\n store.set(EMPTY)\n },\n add: text => current().add(text),\n reply: (threadId, text) => current().reply(threadId, text),\n edit: (threadId, replyId, text) => current().edit(threadId, replyId, text),\n setResolved: (threadId, resolved) => current().setResolved(threadId, resolved),\n remove: threadId => current().remove(threadId),\n removeReply: (threadId, replyId) => current().removeReply(threadId, replyId),\n activate: (id, options) => current().activate(id, options),\n startDraft: () => current().startDraft(),\n cancelDraft: () => current().cancelDraft(),\n }\n}\n"],"mappings":";;;;;;;;;;;AA2EA,SAAS,WAAW,QAAuC;CACzD,MAAM,UAAU,OAAO,IAAI,SAAS;CACpC,OAAO;EAAE,GAAI,OAAO,OAAO;EAAqB,SAAS,mBAAmB,EAAE,QAAQ,QAAQ,QAAQ,IAAsB,CAAC;CAAE;AACjI;;;;;;;;;;;AAYA,SAAgB,oBAAoB,WAAoD;CACtF,MAAM,QAAQ,YAA8BA,cAAK;CAEjD,MAAM,gBAAgB;EACpB,MAAM,WAAW,UAAU;EAC3B,OAAO,YAAY,CAAC,SAAS,cAAc,WAAW,KAAA;CACxD;CAGA,IAAI;CACJ,IAAI;CACJ,IAAI,UAAoC;CACxC,IAAI,aAAa;CAEjB,IAAI,OAAiC;CAErC,MAAM,8BAAc,IAAI,IAAY;CACpC,IAAI,aAAa;CAEjB,SAAS,YAAY;EACnB,UAAU,IAAI,cAAc,gBAAgB;EAC5C,WAAW,KAAA;CACb;CAEA,SAAS,mBAAmB;EAC1B,IAAI,CAAC,UAAU;EACf,UAAU,CAAC,GAAG,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,QAAQ,MAA2B,aAAa,EAAE,GAAG,CAAC,CAAC,IAAI,UAAU;EAC1G,UAAU;EACV,UAAU;CACZ;CAEA,SAAS,gBAAgB;EACvB,UAAU;CACZ;CAEA,SAAS,SAAS;EAChB,OAAO,IAAI,eAAe,aAAa;EAEvC,OAAO,IAAI,UAAU,aAAa;EAClC,QAAQ,KAAA;CACV;;CAGA,SAAS,YAAY;EACnB,MAAM,WAAW,QAAQ;EACzB,IAAI,CAAC,UAAU;GACb,MAAM,IAAIA,cAAK;GACf;EACF;EACA,MAAM,WAAW,MAAM,IAAI;EAC3B,MAAM,OAAO,YAAY,QAAQ;EACjC,MAAM,OAAO,aAAa,QAAQ;EAClC,MAAM,SAAS,YAAY,SAAS,SAAS,KAAK;EAClD,MAAM,SAAS,QAAQ,UAAU;EACjC,MAAM,WAAW,gBAAgB,UAAU,MAAM;EACjD,MAAM,SAAS,QAAQ,UAAU;EACjC,MAAM,SAAS,QAAQ;EACvB,MAAM,QAAQ,SAAS,aAAa,SAAS,OAAO,MAAM,IAAI;EAC9D,MAAM,aAAa,QAAQ,SAAS,MAAM,IAAI,YAAY,MAAM,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,GAAG,eAAe,KAAK,IAAI;EACtH,MAAM,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC;EAC9B,MAAM,aAAa,aAAa,SAAS;EAEzC,MAAM,cAAc,YAAY,SAAS;EACzC,MAAM,eAAe,WAAW,QAAQ,SAAS,MAAM;EACvD,MAAM,gBAAgB,aAAa,UAAU,SAAS,QAAQ;EAC9D,IACE,cAAc,SAAS,aAAa,eAAe,SAAS,cAAc,SAAS,MAAM,SAAS,IAAI,KAAK,eAAe,gBAAgB,iBACvI,WAAW,SAAS,UAAU,UAAU,OAAO,SAAS,KAAK,KAAK,eAAe,SAAS,YAC7F;EAGF,MAAM,OADO,eAAe,eAExB,SAAS,OACT,QACG,QAAO,MAAK,CAAC,EAAE,QAAQ,CAAC,CACxB,MAAM,GAAG,OAAO,OAAO,IAAI,EAAE,EAAE,CAAC,EAAE,QAAQ,aAAa,OAAO,IAAI,EAAE,EAAE,CAAC,EAAE,QAAQ,aAAa,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;EAC3I,MAAM,WAAW,cACb,SAAS,WACT,QAAQ,QAAO,MAAK,EAAE,QAAQ,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;EACzF,MAAM,IAAI;GACR;GACA;GACA;GACA;GACA,QAAQ,eAAe,SAAS,SAAS;GACzC,UAAU,gBAAgB,SAAS,WAAW;GAC9C;GACA,OAAO,UAAU,OAAO,SAAS,KAAK,IAAI,SAAS,QAAQ;GAC3D;GACA;GACA;EACF,CAAC;EACD,IAAI,WAAW,UAAU;CAC3B;;CAGA,SAAS,gBAAgB,UAAkB,QAAoE;EAC7G,MAAM,WAAW,iBAAiB,QAAQ;EAC1C,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,OAAO,SAAS,MAAM,IAAI,QAAQ;EACxC,IAAI;EACJ,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,OAAO,YAAY,OAAO,IAAI,OAAO,EAAE,KAAK,CAAC,OAAO,QAAQ;GAChE,MAAM,WAAW,oBAAoB,SAAS,OAAO,UAAU,OAAO,MAAM;GAC5E,IAAI,aAAa,QAAQ,YAAY,MAAM,CAAC,0BAAU,IAAI,IAAI,EAAA,CAAG,IAAI,OAAO,IAAI,QAAQ;EAC1F;EACA,OAAO,SAAS;CAClB;CAKA,SAAS,YAAY;EACnB,IAAI,cAAc,CAAC,QAAQ,MAAK,MAAK,CAAC,EAAE,UAAU,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC,GAAG;EAC3E,aAAa;EACb,qBAAqB;GACnB,aAAa;GACb,YAAY;EACd,CAAC;CACH;CAEA,SAAS,cAAc;EACrB,MAAM,MAAM,SAAS;EACrB,MAAM,WAAW,OAAO,iBAAiB,IAAI,MAAM;EACnD,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,WAAW,OAAO;EAC/C,MAAM,EAAE,WAAW,MAAM,IAAI;EAC7B,MAAM,UAA6C,CAAC;EACpD,KAAK,MAAM,EAAE,IAAI,YAAY,SAAS;GACpC,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,YAAY,IAAI,EAAE,KAAK,CAAC,OAAO;GAC7C,YAAY,IAAI,EAAE;GAClB,MAAM,SAAS,UAAU,IAAI,KAAK,EAAE;GACpC,MAAM,WAAW,UAAU,oBAAoB,IAAI,OAAO,OAAO,UAAU,KAAK;GAChF,IAAI,UAAU,UAAU,QAAQ,KAAK,CAAC,QAAQ,QAAQ,CAAC;EACzD;EACA,IAAI,QAAQ,QAAQ,IAAI,KAAK,eAAe,QAAQ,SAAS,CAAC,QAAQ,cAAc,OAAO,IAAI,UAAU,QAAQ,CAAC,CAAC;CACrH;CAMA,SAAS,YAAY;EACnB,IAAI,YAAY;EAChB,aAAa;EACb,qBAAqB;GACnB,aAAa;GACb,WAAW;EACb,CAAC;CACH;CAEA,SAAS,aAAa;EACpB,MAAM,WAAW,QAAQ;EACzB,IAAI,CAAC,YAAY,aAAa,OAAO;EACrC,MAAM,QAAQ,YAAY,SAAS,SAAS,KAAK;EACjD,IAAI,CAAC,OAAO;EACZ,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,EAAE,CAAC;EAC5C,MAAM,SAAS,IAAI,IAAI,QAAQ,QAAO,MAAK,EAAE,QAAQ,CAAC,CAAC,KAAI,MAAK,EAAE,EAAE,CAAC;EACrE,MAAM,OAAqB;GAAE;GAAO;EAAO;EAC3C,MAAM,SAAS,MAAM,SAAS,KAAK,MAAK,MAAK,EAAE,OAAO,MAAM,MAAM,IAAI,KAAA;EACtE,IAAI,MAAM,UAAU,UAAU,CAAC,OAAO,aAAa,CAAC,MAAM,IAAI,MAAM,MAAM,KAAK,OAAO,IAAI,MAAM,MAAM,IAAI,KAAK,SAAS;EACxH,OAAO;EACP,SAAS,KAAK,SAAS,SAAS,MAAM,GAAG,QAAQ,aAAa,IAAI,CAAC,CAAC,QAAQ,gBAAgB,KAAK,CAAC;CACpG;;CAGA,SAAS,UAAU;EACjB,MAAM,WAAW,QAAQ;EACzB,IAAI,aAAa,OAAO;GACtB,OAAO;GACP,UAAU;GACV,QAAQ;GACR,UAAU;GACV,OAAO;GACP,YAAY,MAAM;GAClB,UAAU,GAAG,eAAe,aAAa;GACzC,UAAU,GAAG,UAAU,aAAa;GAEpC,UAAU;EACZ;EACA,MAAM,OAAO,WAAW,YAAY,QAAQ,IAAI;EAChD,KAAK,UAAU,OAAO,UAAU,MAAM;GACpC,UAAU;GACV,IAAI,MAAM;IACR,MAAM,MAAM,KAAK,OAAgB,cAAc;IAC/C,WAAW;KAAE,KAAK;KAAM;IAAI;IAC5B,IAAI,YAAY,gBAAgB;IAChC,UAAU,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,QAAQ,MAA2B,aAAa,EAAE,GAAG,CAAC,CAAC,IAAI,UAAU;GACnG,OAEE,UAAU;GAEZ,UAAU;EACZ;EACA,UAAU;CACZ;CAEA,SAAS,UAAU;EACjB,OAAO;EACP,UAAU;EACV,UAAU;EACV,OAAO;EACP,YAAY,MAAM;EAClB,MAAM,IAAIA,cAAK;CACjB;;CAGA,SAAS,WAAW;EAClB,MAAM,WAAW,QAAQ;EACzB,MAAM,OAAO,YAAY,YAAY,QAAQ;EAC7C,MAAM,SAAS,YAAY,aAAa,QAAQ;EAChD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,OAAO;EAC1C,OAAO;GAAE,QAAQ;GAAU;GAAM,KAAK,KAAK,OAAgB,cAAc;GAAG,MAAM;IAAE,IAAI,OAAO;IAAI,MAAM,OAAO;IAAM,OAAO,OAAO;GAAM;EAAE;CAC9I;;CAGA,MAAM,SAAS,SAAiB,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,eAAe,IAAI;CAExE,SAAS,UAAU,KAAqB,UAAyC;EAC/E,MAAM,SAAS,IAAI,IAAI,QAAQ;EAC/B,OAAO,kBAAkB,EAAE,MAAM,SAAS;CAC5C;CAEA,SAAS,UAAU,QAAsD;EACvE,MAAM,UAAU,OAAO,IAAI,SAAS;EACpC,OAAO,mBAAmB,EAAE,QAAQ,UAAmC;CACzE;CAEA,SAAS,IAAI,MAA6B;EACxC,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,MAAM,IAAI;EACvB,MAAM,SAAS,OAAO,YAAY,SAAS,IAAI,OAAO,KAAK,CAAC,EAAE;EAC9D,MAAM,QAAQ,OAAO,SAAS,aAAa,IAAI,OAAO,OAAO,MAAM,IAAI;EACvE,MAAM,OAAO,KAAK,OAAO,OAAO,MAAM;EACtC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,MAAM,QAAQ,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,OAAO;EAClF,MAAM,KAAK,WAAW;EACtB,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,WAAW,iBAAiB,IAAI,MAAM;EAC5C,MAAM,eAAe,WAAW,oBAAoB,OAAO,UAAU,KAAK,IAAI;EAC9E,MAAM,QAAQ,MAAM,IAAI,YAAY,MAAM,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,GAAG,eAAe,KAAK;EAC5F,MAAM,KAAK,MAAM,GAAG,QAAQ,MAAM,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,GAAG,CAAC,CAAC;EAErE,IAAI,CAAC,GAAG,YAAY,OAAO;EAE3B,MAAM,OAAqB;GAAE,OAAO;GAAM,QAAQ;EAAG;EACrD,IAAI,OAAO,KAAK,SAAS,GAAG,QAAQ,aAAa,IAAI,CAAC,CAAC,QAAQ,gBAAgB,KAAK,CAAC;EACrF,cAAc,IAAI,QAAQ,CAAC,MAAM,CAAC;EAClC,MAAM,SAAS,IAAI,EAAE,IAAa;EAClC,OAAO,IAAI,MAAM,EAAE;EACnB,OAAO,IAAI,UAAU,IAAI,IAAI;EAC7B,OAAO,IAAI,QAAQ,IAAI;EACvB,OAAO,IAAI,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;EAChD,OAAO,IAAI,SAAS,KAAK;EACzB,OAAO,IAAI,YAAY,KAAK;EAC5B,IAAI,cAAc,OAAO,IAAI,UAAU,YAAY;EACnD,OAAO,IAAI,WAAW,IAAI,EAAE,MAAoB,CAAC;EACjD,IAAI,IAAI,IAAI,IAAI,MAAM;EACtB,OAAO;CACT;CAEA,SAAS,MAAM,UAAkB,MAAoB;EACnD,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,MAAM,IAAI;EACvB,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ;EACjD,MAAM,UAAU,UAAU,UAAU,MAAM;EAC1C,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM;EAC/B,QAAQ,KAAK,CAAC;GAAE,IAAI,WAAW;GAAG,QAAQ,IAAI;GAAM,MAAM;GAAM,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC,CAAC;CACxG;CAEA,SAAS,KAAK,UAAkB,SAAwB,MAAoB;EAC1E,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,MAAM,IAAI;EACvB,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ;EACjD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM;EAC9B,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,IAAI,YAAY,MAAM;GACpB,IAAK,OAAO,IAAI,QAAQ,CAAC,EAA0C,OAAO,IAAI,KAAK,MAAM,OAAO,IAAI,MAAM,MAAM,MAAM;GACtH,IAAI,KAAK,eAAe;IACtB,OAAO,IAAI,QAAQ,IAAI;IACvB,OAAO,IAAI,YAAY,GAAG;GAC5B,CAAC;GACD;EACF;EACA,MAAM,UAAU,UAAU,MAAM;EAChC,MAAM,QAAQ,UAAU,QAAQ,QAAQ,CAAC,CAAC,WAAU,MAAK,EAAE,OAAO,OAAO,IAAI;EAC7E,MAAM,WAAW,WAAW,SAAS,IAAI,QAAQ,IAAI,KAAK,IAAI,KAAA;EAC9D,IAAI,CAAC,WAAW,CAAC,YAAY,SAAS,OAAO,OAAO,IAAI,KAAK,MAAM,SAAS,SAAS,MAAM;EAC3F,IAAI,KAAK,eAAe;GACtB,QAAQ,OAAO,OAAO,CAAC;GACvB,QAAQ,OAAO,OAAO,CAAC;IAAE,GAAG;IAAU,MAAM;IAAM,UAAU;GAAI,CAAC,CAAC;EACpE,CAAC;CACH;CAEA,SAAS,YAAY,UAAkB,OAAsB;EAC3D,MAAM,MAAM,SAAS;EACrB,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ;EACjD,IAAI,CAAC,OAAO,CAAC,QAAQ;EACrB,OAAO,IAAI,YAAY,KAAK;EAC5B,IAAI,SAAS,YAAY,SAAS,IAAI,OAAO,KAAK,CAAC,EAAE,WAAW,UAAU,iBAAiB,IAAI,QAAQ,IAAI;CAC7G;CAEA,SAAS,OAAO,UAAwB;EACtC,MAAM,MAAM,SAAS;EACrB,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ;EACjD,IAAI,CAAC,OAAO,CAAC,UAAW,OAAO,IAAI,QAAQ,CAAC,EAA0C,OAAO,IAAI,KAAK,IAAI;EAC1G,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,KAAK,MAAM;EACjB,MAAM,IAAI,aAAa,MAAM,QAAQ;GACnC,IAAI,CAAC,KAAK,QAAQ;GAClB,MAAM,OAAO,KAAK,MAAM,MAAK,MAAK,EAAE,KAAK,SAAS,aAAa,EAAE,MAAM,OAAO,QAAQ;GACtF,IAAI,MAAM,GAAG,WAAW,KAAK,MAAM,KAAK,UAAU,IAAI;EACxD,CAAC;EACD,IAAI,YAAY,SAAS,KAAK,CAAC,EAAE,WAAW,UAAU,GAAG,QAAQ,aAAa,EAAE,QAAQ,KAAK,CAAwB;EACrH,IAAI,GAAG,cAAc,GAAG,QAAQ,WAAW,GAAG,IAAI,OAAO,KAAK,SAAS,GAAG,QAAQ,gBAAgB,KAAK,CAAC;EACxG,IAAI,IAAI,OAAO,QAAQ;CACzB;CAEA,SAAS,YAAY,UAAkB,SAAuB;EAC5D,MAAM,MAAM,SAAS;EACrB,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ;EACjD,MAAM,UAAU,UAAU,UAAU,MAAM;EAC1C,IAAI,CAAC,OAAO,CAAC,SAAS;EACtB,MAAM,QAAQ,QAAQ,QAAQ,CAAC,CAAC,WAAU,MAAK,EAAE,OAAO,OAAO;EAC/D,IAAI,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,OAAO,OAAO,IAAI,KAAK,IAAI,QAAQ,OAAO,OAAO,CAAC;CACzF;CAEA,SAAS,SAAS,IAAmB,UAAgC,CAAC,GAAS;EAC7E,MAAM,WAAW,QAAQ;EACzB,IAAI,UAAU,iBAAiB,UAAU,IAAI,OAAO;CACtD;CAEA,SAAS,aAAmB;EAC1B,MAAM,MAAM,SAAS;EACrB,IAAI,KAAK,kBAAkB,IAAI,MAAM;CACvC;CAEA,SAAS,cAAoB;EAC3B,MAAM,WAAW,QAAQ;EACzB,IAAI,UAAU,mBAAmB,QAAQ;CAC3C;CAEA,OAAO;EAAE,OAAO;EAAO;EAAS;EAAS;EAAK;EAAO;EAAM;EAAa;EAAQ;EAAa;EAAU;EAAY;CAAY;AACjI;AAMA,MAAM,iCAAiB,IAAI,QAA8C;;AAGzE,SAAgB,qBAAqB,KAAqB,SAAqC;CAC7F,eAAe,IAAI,KAAK,OAAO;AACjC;;AAGA,SAAgB,gBAAgB,KAAuD;CACrF,OAAO,eAAe,IAAI,GAAG;AAC/B;;;;;;;;AASA,SAAgB,0BAA0B,WAAoD;CAC5F,MAAM,QAAQ,YAA8BA,cAAK;CACjD,IAAI;CAEJ,SAAS,UAAU;EACjB,OAAO,KAAK;EACZ,OAAO,MAAM,QAAQ;EACrB,QAAQ,KAAA;CACV;;CAGA,SAAS,UAAyB;EAChC,MAAM,QAAQ,cAAc,UAAU,CAAC;EACvC,MAAM,WAAW,SAAS,gBAAgB,MAAM,GAAG,MAAM;EACzD,IAAI,OAAO,YAAY,SAAS;GAC9B,QAAQ;GACR,MAAM,QAAQ,QAAQ,SAAS;GAC/B,QAAQ;IAAE;IAAO;IAAS,MAAM,MAAM,MAAM,WAAU,aAAY,MAAM,IAAI,QAAQ,CAAC;GAAE;GACvF,MAAM,IAAI,MAAM,MAAM,IAAI,CAAC;EAC7B;EACA,OAAO,MAAO;CAChB;CAEA,OAAO;EACL,OAAO;EACP,eAAe,QAAQ,CAAC,CAAC,QAAQ;EACjC,eAAe;GACb,QAAQ;GACR,MAAM,IAAIA,cAAK;EACjB;EACA,MAAK,SAAQ,QAAQ,CAAC,CAAC,IAAI,IAAI;EAC/B,QAAQ,UAAU,SAAS,QAAQ,CAAC,CAAC,MAAM,UAAU,IAAI;EACzD,OAAO,UAAU,SAAS,SAAS,QAAQ,CAAC,CAAC,KAAK,UAAU,SAAS,IAAI;EACzE,cAAc,UAAU,aAAa,QAAQ,CAAC,CAAC,YAAY,UAAU,QAAQ;EAC7E,SAAQ,aAAY,QAAQ,CAAC,CAAC,OAAO,QAAQ;EAC7C,cAAc,UAAU,YAAY,QAAQ,CAAC,CAAC,YAAY,UAAU,OAAO;EAC3E,WAAW,IAAI,YAAY,QAAQ,CAAC,CAAC,SAAS,IAAI,OAAO;EACzD,kBAAkB,QAAQ,CAAC,CAAC,WAAW;EACvC,mBAAmB,QAAQ,CAAC,CAAC,YAAY;CAC3C;AACF"}
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../../src/features/comments/model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport * as Y from 'yjs'\nimport type { AuthorIdentity, FeatureContext } from '../../core'\nimport { anchorResolverOf, editorEntryOf, releaseRanges, resolveRange } from '../../core'\nimport { newBlockId } from '../../core/schema'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport { cancelCommentDraft, commentsDoc, commentsKey, type CommentsMeta, commentsUser, setActiveComment, startCommentDraft } from './comments-plugin'\nimport { COMMENT_LIMITS, COMMENTS_FIELD, type CommentAnchor, type CommentReply, type CommentThread } from './schema'\nimport { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from './snapshot-equality'\nimport { captureThreadAnchor, resolveThreadAnchor } from './thread-anchor'\n\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A thread's `Y.Map` as plain data; `replies` converted explicitly so it is always a plain array. */\nfunction snapshotOf(thread: Y.Map<unknown>): CommentThread {\n const replies = thread.get('replies')\n return { ...(thread.toJSON() as CommentThread), replies: replies instanceof Y.Array ? replies.toArray() as CommentReply[] : [] }\n}\n\n/**\n * The comment threads of a collaborative editor, stored in its Y.Doc (`COMMENTS_FIELD`, one nested `Y.Map` per thread,\n * replies in a nested `Y.Array`), so they sync live with the document and persist with its Yjs state. Keeps the\n * `Comments` plugin's `known`/`hidden` thread ids in step with the store (highlights), exposes the plugin's ranges,\n * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and\n * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).\n * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).\n * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves\n * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.\n */\nexport function createCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed ? instance : undefined\n }\n\n // The editor whose transactions are followed, and the Y.Map of threads observed.\n let bound: Editor | undefined\n let observed: { doc: Y.Doc, map: Y.Map<unknown> } | undefined\n let threads: readonly CommentThread[] = NO_THREADS\n let syncQueued = false\n // The thread list the plugin was last told about, for the deactivation rule.\n let told: readonly CommentThread[] = NO_THREADS\n // Threads already tried for an anchor (written, or not capturable), so a failed capture is not repeated on every change.\n const anchorTried = new Set<string>()\n let healQueued = false\n\n function unobserve() {\n observed?.map.unobserveDeep(onThreadsChanged)\n observed = undefined\n }\n\n function onThreadsChanged() {\n if (!observed) return\n threads = [...observed.map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n recompute()\n queueSync()\n }\n\n function onTransaction() {\n recompute()\n }\n\n function unbind() {\n bound?.off('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` follows `isEditable`.\n bound?.off('update', onTransaction)\n bound = undefined\n }\n\n /** The snapshot of the editor's current state; the previous object when nothing changed. */\n function recompute() {\n const instance = current()\n if (!instance) {\n store.set(EMPTY_SNAPSHOT)\n return\n }\n const previous = store.get()\n const ydoc = commentsDoc(instance)\n const user = commentsUser(instance)\n const plugin = commentsKey.getState(instance.state)\n const ranges = plugin?.ranges ?? NO_RANGES\n const orphanAt = orphanPositions(instance, ranges)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = anchor ? resolveRange(instance.state, anchor) : null\n const draftQuote = draft ? instance.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const available = !!ydoc && !!user\n const canComment = available && instance.isEditable\n\n const sameThreads = threads === previous.threads\n const sameRangeMap = sameRanges(ranges, previous.ranges)\n const sameOrphanMap = sameOrphanAt(orphanAt, previous.orphanAt)\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && sameOrphanMap\n && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = sameThreads && sameRangeMap\n const open = kept\n ? previous.open\n : threads\n .filter(t => !t.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt))\n const resolved = sameThreads\n ? previous.resolved\n : threads.filter(t => t.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n store.set({\n available,\n canComment,\n user,\n threads,\n ranges: sameRangeMap ? previous.ranges : ranges,\n orphanAt: sameOrphanMap ? previous.orphanAt : orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open,\n resolved,\n })\n if (available) queueHeal()\n }\n\n /** Where each unresolved thread without a text range was, from its anchor; empty without a Yjs session. */\n function orphanPositions(instance: Editor, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n const resolver = anchorResolverOf(instance)\n if (!resolver) return NO_POSITIONS\n const size = instance.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.resolved || ranges.has(thread.id) || !thread.anchor) continue\n const position = resolveThreadAnchor(instance.state, resolver, thread.anchor)\n if (position !== null && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n // Threads from before anchors get theirs once they are seen with their text in the document (any client with a user\n // writes it; the positions are equivalent, so concurrent writes agree). Queued: not written from inside a Yjs\n // observer or a transaction listener.\n function queueHeal() {\n if (healQueued || !threads.some(t => !t.anchor && !anchorTried.has(t.id))) return\n healQueued = true\n queueMicrotask(() => {\n healQueued = false\n healAnchors()\n })\n }\n\n function healAnchors() {\n const ctx = writable()\n const resolver = ctx && anchorResolverOf(ctx.editor)\n if (!ctx || !resolver || ctx.editor !== bound) return\n const { ranges } = store.get()\n const written: [Y.Map<unknown>, CommentAnchor][] = []\n for (const { id, anchor } of threads) {\n const range = ranges.get(id)\n if (anchor || anchorTried.has(id) || !range) continue\n anchorTried.add(id)\n const thread = threadMap(ctx.map, id)\n const captured = thread && captureThreadAnchor(ctx.editor.state, resolver, range)\n if (thread && captured) written.push([thread, captured])\n }\n if (written.length) ctx.ydoc.transact(() => written.forEach(([thread, captured]) => thread.set('anchor', captured)))\n }\n\n // Highlights appear and disappear on every client as threads arrive, get resolved or deleted. The active thread\n // stays highlighted even when resolved (a resolved thread activated from its card), so a thread that turns resolved\n // or disappears while active is deactivated here, on every client, not only on the one that resolved or deleted it.\n // Queued (not dispatched from inside the Yjs observer or a transaction listener).\n function queueSync() {\n if (syncQueued) return\n syncQueued = true\n queueMicrotask(() => {\n syncQueued = false\n syncPlugin()\n })\n }\n\n function syncPlugin() {\n const instance = current()\n if (!instance || instance !== bound) return\n const value = commentsKey.getState(instance.state)\n if (!value) return\n const known = new Set(threads.map(t => t.id))\n const hidden = new Set(threads.filter(t => t.resolved).map(t => t.id))\n const meta: CommentsMeta = { known, hidden }\n const before = value.active ? told.find(t => t.id === value.active) : undefined\n if (value.active && before && !before.resolved && (!known.has(value.active) || hidden.has(value.active))) meta.active = null\n told = threads\n instance.view.dispatch(instance.state.tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n }\n\n /** Follows the editor `getEditor` names now, and the Y.Doc of its collaboration session. */\n function refresh() {\n const instance = current()\n if (instance !== bound) {\n unbind()\n unobserve()\n bound = instance\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n instance?.on('transaction', onTransaction)\n instance?.on('update', onTransaction)\n // The new editor's plugin knows no threads yet.\n queueSync()\n }\n const ydoc = instance ? commentsDoc(instance) : null\n if ((observed?.doc ?? null) !== ydoc) {\n unobserve()\n if (ydoc) {\n const map = ydoc.getMap<unknown>(COMMENTS_FIELD)\n observed = { doc: ydoc, map }\n map.observeDeep(onThreadsChanged)\n threads = [...map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n }\n else {\n threads = NO_THREADS\n }\n queueSync()\n }\n recompute()\n }\n\n function dispose() {\n unbind()\n unobserve()\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n store.set(EMPTY_SNAPSHOT)\n }\n\n /** Editor, thread map and user while comments are available; null otherwise. */\n function writable() {\n const instance = current()\n const ydoc = instance && commentsDoc(instance)\n const author = instance && commentsUser(instance)\n if (!instance || !ydoc || !author) return null\n return { editor: instance, ydoc, map: ydoc.getMap<unknown>(COMMENTS_FIELD), user: { id: author.id, name: author.name, color: author.color } }\n }\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function threadMap(map: Y.Map<unknown>, threadId: string): Y.Map<unknown> | null {\n const thread = map.get(threadId)\n return thread instanceof Y.Map ? thread : null\n }\n\n function repliesOf(thread: Y.Map<unknown>): Y.Array<CommentReply> | null {\n const replies = thread.get('replies')\n return replies instanceof Y.Array ? replies as Y.Array<CommentReply> : null\n }\n\n function add(text: string): string | null {\n const ctx = writable()\n const body = clean(text)\n const anchor = ctx && commentsKey.getState(ctx.editor.state)?.draft\n const range = ctx && anchor ? resolveRange(ctx.editor.state, anchor) : null\n const type = ctx?.editor.schema.marks.comment\n if (!ctx || !anchor || !range || range.from >= range.to || !body || !type) return null\n const id = newBlockId()\n const { state } = ctx.editor\n const resolver = anchorResolverOf(ctx.editor)\n const threadAnchor = resolver ? captureThreadAnchor(state, resolver, range) : null\n const quote = state.doc.textBetween(range.from, range.to, ' ').slice(0, COMMENT_LIMITS.quote)\n const tr = state.tr.addMark(range.from, range.to, type.create({ id }))\n // Text that takes no marks (code block, inline code): no thread that would start out without a range.\n if (!tr.docChanged) return null\n // Not undoable: the mark is the thread's anchor, not an edit of the text.\n const meta: CommentsMeta = { draft: null, active: id }\n ctx.editor.view.dispatch(tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n releaseRanges(ctx.editor, [anchor])\n const thread = new Y.Map<unknown>()\n thread.set('id', id)\n thread.set('author', ctx.user)\n thread.set('text', body)\n thread.set('createdAt', new Date().toISOString())\n thread.set('quote', quote)\n thread.set('resolved', false)\n if (threadAnchor) thread.set('anchor', threadAnchor)\n thread.set('replies', new Y.Array<CommentReply>())\n ctx.map.set(id, thread)\n return id\n }\n\n function reply(threadId: string, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies || !body) return\n replies.push([{ id: newBlockId(), author: ctx.user, text: body, createdAt: new Date().toISOString() }])\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || !body) return\n const now = new Date().toISOString()\n if (replyId === null) {\n if ((thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id || thread.get('text') === body) return\n ctx.ydoc.transact(() => {\n thread.set('text', body)\n thread.set('editedAt', now)\n })\n return\n }\n const replies = repliesOf(thread)\n const index = replies ? replies.toArray().findIndex(r => r.id === replyId) : -1\n const previous = replies && index >= 0 ? replies.get(index) : undefined\n if (!replies || !previous || previous.author.id !== ctx.user.id || previous.text === body) return\n ctx.ydoc.transact(() => {\n replies.delete(index, 1)\n replies.insert(index, [{ ...previous, text: body, editedAt: now }])\n })\n }\n\n function setResolved(threadId: string, value: boolean): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread) return\n thread.set('resolved', value)\n if (value && commentsKey.getState(ctx.editor.state)?.active === threadId) setActiveComment(ctx.editor, null)\n }\n\n function remove(threadId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || (thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id) return\n const { state } = ctx.editor\n const tr = state.tr\n state.doc.descendants((node, pos) => {\n if (!node.isText) return\n const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.id === threadId)\n if (mark) tr.removeMark(pos, pos + node.nodeSize, mark)\n })\n if (commentsKey.getState(state)?.active === threadId) tr.setMeta(commentsKey, { active: null } satisfies CommentsMeta)\n if (tr.docChanged || tr.getMeta(commentsKey)) ctx.editor.view.dispatch(tr.setMeta('addToHistory', false))\n ctx.map.delete(threadId)\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies) return\n const index = replies.toArray().findIndex(r => r.id === replyId)\n if (index >= 0 && replies.get(index).author.id === ctx.user.id) replies.delete(index, 1)\n }\n\n function activate(id: string | null, options: { reveal?: boolean } = {}): void {\n const instance = current()\n if (instance) setActiveComment(instance, id, options)\n }\n\n function startDraft(): void {\n const ctx = writable()\n if (ctx) startCommentDraft(ctx.editor)\n }\n\n function cancelDraft(): void {\n const instance = current()\n if (instance) cancelCommentDraft(instance)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft }\n}\n\n/** Creates the comments model of one editor, as a host supplies it to `commentsFeatureCore`. */\nexport type CommentsModelFactory = (getEditor: () => Editor | undefined) => CommentsModel\n\n// The host's model factory per surface context, set by the comments feature while the surface builds its extensions.\nconst modelFactories = new WeakMap<FeatureContext, CommentsModelFactory>()\n\n/** Registers the host model factory of `ctx` (the comments feature does this; hosts pass `model` to the feature). */\nexport function provideCommentsModel(ctx: FeatureContext, factory: CommentsModelFactory): void {\n modelFactories.set(ctx, factory)\n}\n\n/** The host model factory the comments feature of `ctx` was configured with; undefined for the default Y.Doc model. */\nexport function commentsModelOf(ctx: FeatureContext): CommentsModelFactory | undefined {\n return modelFactories.get(ctx)\n}\n\n/**\n * The comments model the framework hooks (`useComments`) use: the model of the surface `getEditor` belongs to\n * (`commentsModelOf` of its feature context: the host's `model` factory) and `createCommentsModel` for every other\n * editor. It follows the editor over time: when the editor changes to one of a surface with another model, the inner\n * model is replaced (disposed, created, refreshed) and `state` carries on with the new one. The factory runs once per\n * consumer of this function, so a host model that must share state across them keeps it outside the factory.\n */\nexport function createEditorCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n let inner: { model: CommentsModel, factory: CommentsModelFactory, stop: () => void } | undefined\n\n function release() {\n inner?.stop()\n inner?.model.dispose()\n inner = undefined\n }\n\n /** The inner model for the editor now; a replacement when its surface brings another factory. */\n function current(): CommentsModel {\n const entry = editorEntryOf(getEditor())\n const factory = (entry && commentsModelOf(entry.ctx)) ?? createCommentsModel\n if (inner?.factory !== factory) {\n release()\n const model = factory(getEditor)\n inner = { model, factory, stop: model.state.subscribe(snapshot => store.set(snapshot)) }\n store.set(model.state.get())\n }\n return inner!.model\n }\n\n return {\n state: store,\n refresh: () => current().refresh(),\n dispose: () => {\n release()\n store.set(EMPTY_SNAPSHOT)\n },\n add: text => current().add(text),\n reply: (threadId, text) => current().reply(threadId, text),\n edit: (threadId, replyId, text) => current().edit(threadId, replyId, text),\n setResolved: (threadId, resolved) => current().setResolved(threadId, resolved),\n remove: threadId => current().remove(threadId),\n removeReply: (threadId, replyId) => current().removeReply(threadId, replyId),\n activate: (id, options) => current().activate(id, options),\n startDraft: () => current().startDraft(),\n cancelDraft: () => current().cancelDraft(),\n }\n}\n"],"mappings":"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"}
@@ -61,8 +61,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
61
61
  onDiagnostics?: ((diagnostics: MarkdownDiagnostic[]) => any) | undefined;
62
62
  }>, {
63
63
  editable: boolean;
64
- settingsUi: "internal" | "external";
65
64
  modelValue: string;
65
+ settingsUi: "internal" | "external";
66
66
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
67
67
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
68
68
  declare const _default: typeof __VLS_export;
@@ -1,8 +1,8 @@
1
1
  import { findBlockById } from "../core/schema/block-ids.js";
2
2
  import { compositeJSON, compositeKindsOf } from "../features/composites/nodes.js";
3
- import { revealBlockInEditor } from "../outline/heading-outline.js";
4
3
  import { moveBlockInto, moveIntoVerdict } from "../features/composites/outline.js";
5
4
  import { pickFiles } from "../features/media/upload.js";
5
+ import { revealBlockInEditor } from "../outline/heading-outline.js";
6
6
  import { readOutline } from "./tree-items.js";
7
7
  //#region src/tree/tree-host.ts
8
8
  /** The model's view of `editor`: its Baustein kinds, outline and move verdicts (worded in the language of `t`), read live. Create it once per editor. */
@@ -1,6 +1,6 @@
1
1
  import { compositeKind, defaultCompositeKinds, kindAccepts } from "../features/composites/kinds.js";
2
- import { headingOutline } from "../outline/heading-outline.js";
3
2
  import { compositeOutline } from "../features/composites/outline.js";
3
+ import { headingOutline } from "../outline/heading-outline.js";
4
4
  import { folderKey } from "./folder-paths.js";
5
5
  //#region src/tree/tree-items.ts
6
6
  /** Key of the virtual-mode root item (the open document). */
@@ -28,7 +28,7 @@ How a host composes the editor from features, where settings and view state live
28
28
  | `@ecsplico/editor/server/handlers` | server | One handler per editor/AI route, `(ctx, request) => Promise<HandlerResponse>` (`usersGet`, `tagsGet`, `settingsGet/Put`, `assetGet/Post`, `themesGet`, `themeGet/Post/Delete`, `modelsGet`, `chatPost`, `generatePost`, `completePost`, `transcribePost`, `transcriptCleanupPost`) and the zod schemas of their bodies (`chatBodySchema`, …). Map any HTTP server's request onto `HandlerRequest` and send the `HandlerResponse`; `HttpError` carries the status. |
29
29
  | `@ecsplico/editor/server/node` | server (Node) | `listenCollab(collab, { port, host })`: a Node HTTP server that serves a `createCollabServer` over websockets, for a plain script (needs the `crossws` dependency; Node only). |
30
30
  | `@ecsplico/editor/collab`, `/collab/vue`, `/collab/react` | browser | framework-free `collabFeatureCore(ui?)`, `yjsAnchorResolver`; `collabFeature()` per surface |
31
- | `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | browser | framework-free `blockUiFeatureCore(ui)`, `blockActionsFor`, `effectiveTarget`; `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay` per surface |
31
+ | `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | browser | framework-free `blockUiFeatureCore(ui)`, `blockActionsFor`, `effectiveTarget`, `BLOCK_CORNER_ATTR`, `blockCornerOf`; `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay` per surface |
32
32
  | `@ecsplico/editor/authors`, `/authors/vue`, `/authors/react` | browser | framework-free `authorsFeatureCore(ui)`, `currentAuthor`, `documentAuthors`; `authorsFeature()` plus `AuthorColorsButton`, `AuthorColorsPanel`, `SettingsAuthorsTab` per surface (see the [usage guide](./usage.md#authors-and-comments)) |
33
33
  | `@ecsplico/editor/comments`, `/comments/vue`, `/comments/react` | browser | framework-free `commentsFeatureCore(ui, { model? })`, `createCommentsModel`; `commentsFeature({ model? })` (requires `collab` unless a host `model` backs it), `useComments`, `CommentsMargin`, `CommentsPopover`, `CommentsListMenu`, … per surface |
34
34
  | `@ecsplico/editor/themes`, `/themes/vue`, `/themes/react` | browser | framework-free `themesFeatureCore(ui)`, `startObsidianTheme`, `scopeThemeCss`, `THEME_SCOPE_CLASS`; `themesFeature()` (site-wide Obsidian themes; see the [usage guide](./usage.md#themes-obsidian)) and `SettingsAppearanceTab` per surface; Vue-free in `/themes/schema`: `ThemeRecord`, `THEME_ID`, `MAX_THEME_BYTES`, `themeManifestSchema` |
@@ -361,6 +361,8 @@ Which blocks have which handle follows the settings and the block:
361
361
  - **`blocks.dragHandle: 'hover'`** (default): one handle follows the hovered block.
362
362
  - **`blocks.dragHandle: 'always'`**: the hover handle is replaced by one permanent handle per eligible block, each at the position the hover handle would take for that block (right edge on the block's left edge, centred on its first text line; list items left of their bullet). Eligible are the blocks the hover handle can target at any depth: paragraphs, headings, lists' items, quotes, code blocks, media, tables and the blocks inside quotes, list items and table cells. Not eligible: composite chrome (title, description, tags), table rows and cells themselves, list wrappers, the first block of a list item (the item has the handle), and blocks with a kind icon. A nested block whose handle would cover its container's handle on the same line (the first block of a quote) has none; the container's handle moves it. Only blocks within one screen of the viewport render a handle. Both modes are hidden while the editor is read-only.
363
363
 
364
+ **Blocks with icons of their own in the top-right corner.** The hover bar sits at the editor's right edge, which a node view that draws its own icons there (status badges, a kind or properties button) would have covered. Such a block marks those elements with the attribute `data-block-corner` (`BLOCK_CORNER_ATTR`, `blockCornerOf(dom)` from `@ecsplico/editor/block-ui`): the bar docks immediately left of the leftmost marked element (4px gap) and is centred on the first one's row, so bar and icons read as one row. Marked elements inside a nested block's own node view (`[data-node-view-wrapper]`) belong to that block, hidden ones (no layout box) are ignored, and a block without marked elements keeps the placement at the editor's right edge. The overlay re-measures when a marked element resizes; Vue and React behave the same.
365
+
364
366
  `startBlockDrag(editor, event, pos)` (`@ecsplico/editor/core`) starts such a drag from any element outside the editor's DOM: call it from the element's `dragstart`.
365
367
 
366
368
  ## 8. Vue-free advanced schema for servers
package/docs/llms.md CHANGED
@@ -274,7 +274,7 @@ const preset = defineEditor({ features: [...advancedEditor().features, notesFeat
274
274
  Things that bite:
275
275
 
276
276
  - Items gated by an own `ability` (`commandItem({ ability })`, `SlashCommandItem.ability`, `BlockAction.ability`) stay inert until the ability exists at the caret. Grant custom abilities at root through the surface prop: `:abilities="{ root: [...ROOT_ABILITIES, MY_ABILITY] }"` (`ROOT_ABILITIES` from `/core/schema`).
277
- - The hover bar of `BlockAction`s is right-aligned 8px inside the editor's right edge for every block and is hidden while the editor is read-only.
277
+ - The hover bar of `BlockAction`s is right-aligned 8px inside the editor's right edge for every block and is hidden while the editor is read-only. A block that draws its own icons in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR` from `/block-ui`): the bar then docks immediately left of them, centred on their row, instead of covering them.
278
278
  - A feature that adds a **node** must be known to every schema that touches the document. The Nuxt module's collab route builds its schema from `advancedSchemaExtensions()` plus the adapter's `schemaExtensions()`, so register the Vue-free node file there (`schemaExtensions: () => [YourNode]`, [extending guide, Collaboration and the server schema](./extending.md#collaboration-and-the-server-schema)); a custom Hocuspocus server builds `getSchema([...advancedSchemaExtensions(), YourNode])` itself. Standalone editors (JSON `v-model`) need none of this. `editorBlock: true` (via `NodeFlags`) gives an own node block IDs, also on the server.
279
279
  - A tab component can bind only to fields that exist in `EditorSettings`: `parseEditorSettings` drops unknown keys, so new keys are neither stored locally nor sent to the server. The tab component runs inside the app, so it can call `useEditorSettings()` (from `/vue`: `{ settings, ready, synced }` as refs; from `/react`: the same names as values).
280
280
  - Factories (`extensions`, `toolbar`, `slashCommands`, ...) receive the `FeatureContext`: `app`, `settings`, `metadata`, `ui`, `notify`, `fetch`, `ai`, `documentId`, `upload`, `collaboration`, `getEditor()`. `settings` is a framework-free store: read `ctx.settings.get()`, change it with `ctx.settings.update(s => { ... })` (or `set`), observe it with `ctx.settings.subscribe(listener)`. `ctx.notify({ id?, title, description?, kind?, icon?, duration? })` shows a user-facing notice (Nuxt UI toast or sonner toast, per surface; features never import a toast library). `ctx.app` is the app context that app-level models key on. Toolbar and block-action menus return core `MenuItem[]` (`type?: 'label' | 'separator'`, `label`, `description`, `icon`, `disabled`, `onSelect`). `getEditor()` is `undefined` while extensions are constructed; use it in event handlers only. Components in `overlays` and `component` toolbar items receive `{ editor }` as props.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecsplico/editor",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "description": "Vue and React editor library on TipTap v3 (Vue: Nuxt UI; React: shadcn/ui): framework-free features with Vue and React bindings, simple and advanced presets, tree-shakeable entries. Built ESM with type declarations.",
6
6
  "license": "MIT",