@dxos/ui-editor 0.10.0 → 0.11.0
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.
- package/dist/lib/chunk-diff.mjs +935 -0
- package/dist/lib/chunk-diff.mjs.map +1 -0
- package/dist/lib/headless.mjs +2 -0
- package/dist/lib/index.mjs +9937 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/types.mjs +18 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/types/src/extensions/{bookmarks.d.ts → behavior/bookmarks.d.ts} +3 -0
- package/dist/types/src/extensions/behavior/bookmarks.d.ts.map +1 -0
- package/dist/types/src/extensions/behavior/dnd.d.ts.map +1 -0
- package/dist/types/src/extensions/behavior/index.d.ts +6 -0
- package/dist/types/src/extensions/behavior/index.d.ts.map +1 -0
- package/dist/types/src/extensions/{listener.d.ts → behavior/listener.d.ts} +3 -0
- package/dist/types/src/extensions/behavior/listener.d.ts.map +1 -0
- package/dist/types/src/extensions/{spacing.d.ts → behavior/spacing.d.ts} +3 -0
- package/dist/types/src/extensions/behavior/spacing.d.ts.map +1 -0
- package/dist/types/src/extensions/behavior/submit.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/automerge/automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/cursor.d.ts +1 -1
- package/dist/types/src/extensions/collab/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/defs.d.ts +3 -1
- package/dist/types/src/extensions/collab/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/automerge/index.d.ts +3 -0
- package/dist/types/src/extensions/collab/automerge/index.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/automerge/sync.d.ts.map +1 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/update-automerge.d.ts +4 -0
- package/dist/types/src/extensions/collab/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/update-codemirror.d.ts +4 -0
- package/dist/types/src/extensions/collab/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/{awareness → collab/awareness}/awareness.d.ts +4 -1
- package/dist/types/src/extensions/collab/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/awareness/index.d.ts.map +1 -0
- package/dist/types/src/extensions/collab/index.d.ts +3 -0
- package/dist/types/src/extensions/collab/index.d.ts.map +1 -0
- package/dist/types/src/extensions/completion/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/{autocomplete → completion}/index.d.ts +1 -0
- package/dist/types/src/extensions/completion/index.d.ts.map +1 -0
- package/dist/types/src/extensions/{autocomplete → completion}/match.d.ts +2 -2
- package/dist/types/src/extensions/completion/match.d.ts.map +1 -0
- package/dist/types/src/extensions/{mention.d.ts → completion/mention.d.ts} +3 -0
- package/dist/types/src/extensions/completion/mention.d.ts.map +1 -0
- package/dist/types/src/extensions/completion/placeholder.d.ts.map +1 -0
- package/dist/types/src/extensions/{autocomplete → completion}/typeahead.d.ts +2 -2
- package/dist/types/src/extensions/completion/typeahead.d.ts.map +1 -0
- package/dist/types/src/extensions/{factories.d.ts → core/factories.d.ts} +23 -2
- package/dist/types/src/extensions/core/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/core/factories.test.d.ts.map +1 -0
- package/dist/types/src/extensions/core/index.d.ts +3 -0
- package/dist/types/src/extensions/core/index.d.ts.map +1 -0
- package/dist/types/src/extensions/{modes.d.ts → core/modes.d.ts} +3 -0
- package/dist/types/src/extensions/core/modes.d.ts.map +1 -0
- package/dist/types/src/extensions/{debug.d.ts → debug/debug.d.ts} +3 -0
- package/dist/types/src/extensions/debug/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/debug/index.d.ts +2 -0
- package/dist/types/src/extensions/debug/index.d.ts.map +1 -0
- package/dist/types/src/extensions/{annotations.d.ts → decoration/annotations.d.ts} +1 -1
- package/dist/types/src/extensions/decoration/annotations.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/assistant.d.ts +27 -0
- package/dist/types/src/extensions/decoration/assistant.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/fader.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/hashtag.d.ts +6 -0
- package/dist/types/src/extensions/decoration/hashtag.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/index.d.ts +8 -0
- package/dist/types/src/extensions/decoration/index.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/marker.d.ts.map +1 -0
- package/dist/types/src/extensions/{pos.d.ts → decoration/pos.d.ts} +40 -14
- package/dist/types/src/extensions/decoration/pos.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/pos.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{replacer.d.ts → decoration/replacer.d.ts} +8 -8
- package/dist/types/src/extensions/decoration/replacer.d.ts.map +1 -0
- package/dist/types/src/extensions/decoration/replacer.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{blast.d.ts → demo/blast.d.ts} +3 -0
- package/dist/types/src/extensions/demo/blast.d.ts.map +1 -0
- package/dist/types/src/extensions/demo/index.d.ts +3 -0
- package/dist/types/src/extensions/demo/index.d.ts.map +1 -0
- package/dist/types/src/extensions/demo/snippets.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +10 -33
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/language/index.d.ts +5 -0
- package/dist/types/src/extensions/language/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/json/index.d.ts +2 -0
- package/dist/types/src/extensions/language/json/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/json/json.d.ts +12 -0
- package/dist/types/src/extensions/language/json/json.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/changes.d.ts +1 -1
- package/dist/types/src/extensions/language/markdown/changes.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/changes.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/debug.d.ts +3 -0
- package/dist/types/src/extensions/language/markdown/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/decorate.d.ts +5 -1
- package/dist/types/src/extensions/language/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/formatting.d.ts +11 -2
- package/dist/types/src/extensions/language/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/formatting.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/highlight.d.ts +16 -12
- package/dist/types/src/extensions/language/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/image.test.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/link.d.ts +25 -0
- package/dist/types/src/extensions/language/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/link.test.d.ts +2 -0
- package/dist/types/src/extensions/language/markdown/link.test.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/parser.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/styles.d.ts +3 -0
- package/dist/types/src/extensions/language/markdown/styles.d.ts.map +1 -0
- package/dist/types/src/extensions/language/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/language/mermaid/index.d.ts +2 -0
- package/dist/types/src/extensions/language/mermaid/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/mermaid/mermaid.d.ts +18 -0
- package/dist/types/src/extensions/language/mermaid/mermaid.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/extended-markdown.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/extended-markdown.test.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/stub.d.ts +76 -0
- package/dist/types/src/extensions/language/xml/stub.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/widgets/anchor.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/widgets/index.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/xml-block-decoration.d.ts +48 -0
- package/dist/types/src/extensions/language/xml/xml-block-decoration.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/xml-formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/{tags → language/xml}/xml-tags.d.ts +12 -1
- package/dist/types/src/extensions/language/xml/xml-tags.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/xml-tags.test.d.ts +2 -0
- package/dist/types/src/extensions/language/xml/xml-tags.test.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/xml-util.d.ts.map +1 -0
- package/dist/types/src/extensions/language/xml/xml-util.test.d.ts.map +1 -0
- package/dist/types/src/extensions/review/change-bar.d.ts +29 -0
- package/dist/types/src/extensions/review/change-bar.d.ts.map +1 -0
- package/dist/types/src/extensions/{comments.d.ts → review/comments.d.ts} +56 -38
- package/dist/types/src/extensions/review/comments.d.ts.map +1 -0
- package/dist/types/src/extensions/review/comments.test.d.ts +2 -0
- package/dist/types/src/extensions/review/comments.test.d.ts.map +1 -0
- package/dist/types/src/extensions/review/diff.d.ts +131 -0
- package/dist/types/src/extensions/review/diff.d.ts.map +1 -0
- package/dist/types/src/extensions/review/diff.test.d.ts +2 -0
- package/dist/types/src/extensions/review/diff.test.d.ts.map +1 -0
- package/dist/types/src/extensions/review/index.d.ts +7 -0
- package/dist/types/src/extensions/review/index.d.ts.map +1 -0
- package/dist/types/src/extensions/review/suggest.d.ts +89 -0
- package/dist/types/src/extensions/review/suggest.d.ts.map +1 -0
- package/dist/types/src/extensions/review/suggest.test.d.ts +2 -0
- package/dist/types/src/extensions/review/suggest.test.d.ts.map +1 -0
- package/dist/types/src/extensions/review/suggestions-overlay.d.ts +25 -0
- package/dist/types/src/extensions/review/suggestions-overlay.d.ts.map +1 -0
- package/dist/types/src/extensions/review/suggestions-overlay.test.d.ts +2 -0
- package/dist/types/src/extensions/review/suggestions-overlay.test.d.ts.map +1 -0
- package/dist/types/src/extensions/review/track-changes.d.ts +27 -0
- package/dist/types/src/extensions/review/track-changes.d.ts.map +1 -0
- package/dist/types/src/extensions/review/track-changes.test.d.ts +2 -0
- package/dist/types/src/extensions/review/track-changes.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{busy.d.ts → state/busy.d.ts} +1 -0
- package/dist/types/src/extensions/state/busy.d.ts.map +1 -0
- package/dist/types/src/extensions/{focus.d.ts → state/focus.d.ts} +4 -0
- package/dist/types/src/extensions/state/focus.d.ts.map +1 -0
- package/dist/types/src/extensions/state/index.d.ts +5 -0
- package/dist/types/src/extensions/state/index.d.ts.map +1 -0
- package/dist/types/src/extensions/state/modal.d.ts.map +1 -0
- package/dist/types/src/extensions/{selection.d.ts → state/selection.d.ts} +1 -1
- package/dist/types/src/extensions/state/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/state/selection.test.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/index.d.ts +4 -0
- package/dist/types/src/extensions/streaming/index.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/pending/index.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/pending/pending-text-stream.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/pending/pending-text-stream.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{pending → streaming/pending}/pending-text.d.ts +1 -0
- package/dist/types/src/extensions/streaming/pending/pending-text.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/pending/pending-text.test.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/crawler.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/index.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/scroll-past-end.d.ts +4 -0
- package/dist/types/src/extensions/streaming/scrolling/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/scrollbar-autohide.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/scrolling/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/{typewriter.d.ts → streaming/typewriter.d.ts} +12 -12
- package/dist/types/src/extensions/streaming/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/streaming/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/blocks.d.ts +52 -0
- package/dist/types/src/extensions/structure/blocks/blocks.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/blocks.test.d.ts +2 -0
- package/dist/types/src/extensions/structure/blocks/blocks.test.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/drag.d.ts +57 -0
- package/dist/types/src/extensions/structure/blocks/drag.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/index.d.ts +5 -0
- package/dist/types/src/extensions/structure/blocks/index.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/selection.d.ts +47 -0
- package/dist/types/src/extensions/structure/blocks/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/selection.test.d.ts +2 -0
- package/dist/types/src/extensions/structure/blocks/selection.test.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/blocks/types.d.ts +17 -0
- package/dist/types/src/extensions/structure/blocks/types.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/folding/folding.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/folding/index.d.ts +3 -0
- package/dist/types/src/extensions/structure/folding/index.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/folding/turns.d.ts +55 -0
- package/dist/types/src/extensions/structure/folding/turns.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/folding/turns.test.d.ts +2 -0
- package/dist/types/src/extensions/structure/folding/turns.test.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/index.d.ts +4 -0
- package/dist/types/src/extensions/structure/index.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/commands.d.ts +31 -0
- package/dist/types/src/extensions/structure/outliner/commands.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/dnd.d.ts +43 -0
- package/dist/types/src/extensions/structure/outliner/dnd.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/dnd.test.d.ts +2 -0
- package/dist/types/src/extensions/structure/outliner/dnd.test.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/editor.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/editor.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/index.d.ts +1 -0
- package/dist/types/src/extensions/structure/outliner/index.d.ts.map +1 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/menu.d.ts +1 -0
- package/dist/types/src/extensions/structure/outliner/menu.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/outliner.test.d.ts.map +1 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/tree.d.ts +1 -1
- package/dist/types/src/extensions/structure/outliner/tree.d.ts.map +1 -0
- package/dist/types/src/extensions/structure/outliner/tree.test.d.ts.map +1 -0
- package/dist/types/src/headless.d.ts +4 -0
- package/dist/types/src/headless.d.ts.map +1 -0
- package/dist/types/src/styles/theme.d.ts +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/util/util.d.ts +2 -0
- package/dist/types/src/util/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +48 -42
- package/src/defaults.ts +1 -1
- package/src/extensions/{bookmarks.ts → behavior/bookmarks.ts} +24 -14
- package/src/extensions/behavior/index.ts +9 -0
- package/src/extensions/{listener.ts → behavior/listener.ts} +4 -1
- package/src/extensions/{spacing.ts → behavior/spacing.ts} +8 -2
- package/src/extensions/{submit.ts → behavior/submit.ts} +2 -10
- package/src/extensions/{automerge → collab/automerge}/automerge.test.tsx +4 -4
- package/src/extensions/{automerge → collab/automerge}/automerge.ts +7 -4
- package/src/extensions/{automerge → collab/automerge}/cursor.ts +1 -1
- package/src/extensions/{automerge → collab/automerge}/defs.ts +4 -1
- package/src/extensions/{automerge → collab/automerge}/index.ts +1 -0
- package/src/extensions/{automerge → collab/automerge}/sync.ts +1 -2
- package/src/extensions/{automerge → collab/automerge}/update-automerge.ts +4 -0
- package/src/extensions/{automerge → collab/automerge}/update-codemirror.ts +5 -1
- package/src/extensions/{awareness → collab/awareness}/awareness-provider.ts +3 -4
- package/src/extensions/{awareness → collab/awareness}/awareness.ts +19 -16
- package/src/extensions/collab/index.ts +6 -0
- package/src/extensions/{autocomplete → completion}/autocomplete.ts +7 -5
- package/src/extensions/{autocomplete → completion}/index.ts +2 -1
- package/src/extensions/{autocomplete → completion}/match.ts +2 -2
- package/src/extensions/{mention.ts → completion/mention.ts} +3 -0
- package/src/extensions/{autocomplete → completion}/placeholder.ts +1 -1
- package/src/extensions/completion/typeahead.ts +97 -0
- package/src/extensions/{factories.ts → core/factories.ts} +35 -13
- package/src/extensions/core/index.ts +6 -0
- package/src/extensions/{modes.ts → core/modes.ts} +4 -1
- package/src/extensions/{debug.ts → debug/debug.ts} +3 -0
- package/src/extensions/debug/index.ts +5 -0
- package/src/extensions/{annotations.ts → decoration/annotations.ts} +12 -10
- package/src/extensions/decoration/assistant.ts +225 -0
- package/src/extensions/{fader.ts → decoration/fader.ts} +17 -19
- package/src/extensions/{hashtag.ts → decoration/hashtag.ts} +35 -28
- package/src/extensions/decoration/index.ts +11 -0
- package/src/extensions/{marker.ts → decoration/marker.ts} +18 -18
- package/src/extensions/{pos.test.ts → decoration/pos.test.ts} +57 -1
- package/src/extensions/{pos.ts → decoration/pos.ts} +165 -31
- package/src/extensions/{blast.ts → demo/blast.ts} +3 -0
- package/src/extensions/demo/index.ts +6 -0
- package/src/extensions/{snippets.ts → demo/snippets.ts} +5 -1
- package/src/extensions/index.ts +10 -33
- package/src/extensions/language/index.ts +8 -0
- package/src/extensions/language/json/index.ts +5 -0
- package/src/extensions/{json.ts → language/json/json.ts} +11 -6
- package/src/extensions/{markdown → language/markdown}/action.ts +1 -1
- package/src/extensions/{markdown → language/markdown}/bundle.ts +7 -1
- package/src/extensions/{markdown → language/markdown}/changes.test.ts +2 -2
- package/src/extensions/{markdown → language/markdown}/changes.ts +2 -2
- package/src/extensions/{markdown → language/markdown}/debug.ts +3 -0
- package/src/extensions/{markdown → language/markdown}/decorate.ts +96 -85
- package/src/extensions/{markdown → language/markdown}/formatting.test.ts +88 -1
- package/src/extensions/{markdown → language/markdown}/formatting.ts +193 -74
- package/src/extensions/{markdown → language/markdown}/highlight.ts +42 -41
- package/src/extensions/{markdown → language/markdown}/image.test.ts +1 -1
- package/src/extensions/{markdown → language/markdown}/image.ts +6 -6
- package/src/extensions/language/markdown/link.test.ts +58 -0
- package/src/extensions/language/markdown/link.ts +109 -0
- package/src/extensions/{markdown → language/markdown}/parser.test.ts +0 -28
- package/src/extensions/{markdown → language/markdown}/styles.ts +38 -7
- package/src/extensions/{markdown → language/markdown}/table.ts +5 -5
- package/src/extensions/language/mermaid/index.ts +5 -0
- package/src/extensions/language/mermaid/mermaid.ts +44 -0
- package/src/extensions/{tags → language/xml}/DESIGN.md +3 -3
- package/src/extensions/{tags → language/xml}/extended-markdown.test.ts +2 -1
- package/src/extensions/{tags → language/xml}/extended-markdown.ts +2 -4
- package/src/extensions/language/xml/stub.ts +233 -0
- package/src/extensions/language/xml/xml-block-decoration.ts +165 -0
- package/src/extensions/{tags → language/xml}/xml-formatting.ts +54 -38
- package/src/extensions/language/xml/xml-tags.test.ts +588 -0
- package/src/extensions/{tags → language/xml}/xml-tags.ts +165 -66
- package/src/extensions/{tags → language/xml}/xml-util.test.ts +2 -2
- package/src/extensions/review/change-bar.ts +94 -0
- package/src/extensions/review/comments.test.ts +72 -0
- package/src/extensions/{comments.ts → review/comments.ts} +395 -267
- package/src/extensions/review/diff.test.ts +405 -0
- package/src/extensions/review/diff.ts +502 -0
- package/src/extensions/review/index.ts +10 -0
- package/src/extensions/review/suggest.test.ts +170 -0
- package/src/extensions/review/suggest.ts +512 -0
- package/src/extensions/review/suggestions-overlay.test.ts +58 -0
- package/src/extensions/review/suggestions-overlay.ts +50 -0
- package/src/extensions/review/track-changes.test.ts +101 -0
- package/src/extensions/review/track-changes.ts +215 -0
- package/src/extensions/{busy.ts → state/busy.ts} +1 -0
- package/src/extensions/{focus.ts → state/focus.ts} +4 -0
- package/src/extensions/state/index.ts +8 -0
- package/src/extensions/{selection.ts → state/selection.ts} +3 -3
- package/src/extensions/streaming/index.ts +7 -0
- package/src/extensions/{pending → streaming/pending}/pending-text-stream.ts +10 -3
- package/src/extensions/{pending → streaming/pending}/pending-text.test.ts +1 -1
- package/src/extensions/{pending → streaming/pending}/pending-text.ts +20 -19
- package/src/extensions/{scrolling → streaming/scrolling}/auto-scroll.ts +9 -6
- package/src/extensions/{scrolling → streaming/scrolling}/scroll-past-end.ts +1 -0
- package/src/extensions/{scrolling → streaming/scrolling}/scrollbar-autohide.ts +13 -11
- package/src/extensions/{typewriter.ts → streaming/typewriter.ts} +46 -47
- package/src/extensions/structure/blocks/DESIGN.md +134 -0
- package/src/extensions/structure/blocks/blocks.test.ts +147 -0
- package/src/extensions/structure/blocks/blocks.ts +219 -0
- package/src/extensions/structure/blocks/drag.ts +996 -0
- package/src/extensions/structure/blocks/index.ts +8 -0
- package/src/extensions/structure/blocks/selection.test.ts +56 -0
- package/src/extensions/structure/blocks/selection.ts +279 -0
- package/src/extensions/structure/blocks/types.ts +22 -0
- package/src/extensions/{folding.ts → structure/folding/folding.ts} +12 -10
- package/src/extensions/structure/folding/index.ts +6 -0
- package/src/extensions/structure/folding/turns.test.ts +72 -0
- package/src/extensions/structure/folding/turns.ts +209 -0
- package/src/extensions/structure/index.ts +7 -0
- package/src/extensions/structure/outliner/DESIGN.md +183 -0
- package/src/extensions/structure/outliner/SPEC.mdl +305 -0
- package/src/extensions/{outliner → structure/outliner}/commands.ts +125 -42
- package/src/extensions/structure/outliner/dnd.test.ts +142 -0
- package/src/extensions/structure/outliner/dnd.ts +375 -0
- package/src/extensions/structure/outliner/editor.test.ts +85 -0
- package/src/extensions/{outliner → structure/outliner}/editor.ts +14 -9
- package/src/extensions/{outliner → structure/outliner}/index.ts +1 -0
- package/src/extensions/{outliner → structure/outliner}/menu.ts +48 -25
- package/src/extensions/structure/outliner/outliner.test.ts +193 -0
- package/src/extensions/structure/outliner/outliner.ts +183 -0
- package/src/extensions/{outliner → structure/outliner}/tree.test.ts +2 -2
- package/src/extensions/{outliner → structure/outliner}/tree.ts +1 -1
- package/src/headless.ts +13 -0
- package/src/styles/theme.ts +3 -1
- package/src/util/util.ts +3 -0
- package/dist/lib/browser/index.mjs +0 -9594
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/types/index.mjs +0 -33
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -9596
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/types/index.mjs +0 -35
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/extensions/annotations.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete/index.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -2
- package/dist/types/src/extensions/automerge/index.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +0 -1
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness/index.d.ts.map +0 -1
- package/dist/types/src/extensions/blast.d.ts.map +0 -1
- package/dist/types/src/extensions/blocks.d.ts +0 -2
- package/dist/types/src/extensions/blocks.d.ts.map +0 -1
- package/dist/types/src/extensions/bookmarks.d.ts.map +0 -1
- package/dist/types/src/extensions/busy.d.ts.map +0 -1
- package/dist/types/src/extensions/comments.d.ts.map +0 -1
- package/dist/types/src/extensions/debug.d.ts.map +0 -1
- package/dist/types/src/extensions/dnd.d.ts.map +0 -1
- package/dist/types/src/extensions/factories.d.ts.map +0 -1
- package/dist/types/src/extensions/factories.test.d.ts.map +0 -1
- package/dist/types/src/extensions/fader.d.ts.map +0 -1
- package/dist/types/src/extensions/focus.d.ts.map +0 -1
- package/dist/types/src/extensions/folding.d.ts.map +0 -1
- package/dist/types/src/extensions/hashtag.d.ts +0 -3
- package/dist/types/src/extensions/hashtag.d.ts.map +0 -1
- package/dist/types/src/extensions/json.d.ts +0 -7
- package/dist/types/src/extensions/json.d.ts.map +0 -1
- package/dist/types/src/extensions/listener.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/changes.test.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/debug.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/image.test.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/index.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/link.d.ts +0 -7
- package/dist/types/src/extensions/markdown/link.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/parser.test.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +0 -1
- package/dist/types/src/extensions/marker.d.ts.map +0 -1
- package/dist/types/src/extensions/mention.d.ts.map +0 -1
- package/dist/types/src/extensions/modal.d.ts.map +0 -1
- package/dist/types/src/extensions/modes.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/commands.d.ts +0 -10
- package/dist/types/src/extensions/outliner/commands.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/editor.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/editor.test.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/index.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/menu.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/selection.d.ts +0 -12
- package/dist/types/src/extensions/outliner/selection.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/tree.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner/tree.test.d.ts.map +0 -1
- package/dist/types/src/extensions/pending/index.d.ts.map +0 -1
- package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +0 -1
- package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +0 -1
- package/dist/types/src/extensions/pending/pending-text.d.ts.map +0 -1
- package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +0 -1
- package/dist/types/src/extensions/pos.d.ts.map +0 -1
- package/dist/types/src/extensions/pos.test.d.ts.map +0 -1
- package/dist/types/src/extensions/replacer.d.ts.map +0 -1
- package/dist/types/src/extensions/replacer.test.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/crawler.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/index.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts +0 -3
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling/scroller.d.ts.map +0 -1
- package/dist/types/src/extensions/selection.d.ts.map +0 -1
- package/dist/types/src/extensions/selection.test.d.ts.map +0 -1
- package/dist/types/src/extensions/snippets.d.ts.map +0 -1
- package/dist/types/src/extensions/spacing.d.ts.map +0 -1
- package/dist/types/src/extensions/state.d.ts +0 -2
- package/dist/types/src/extensions/state.d.ts.map +0 -1
- package/dist/types/src/extensions/submit.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/index.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/stub.d.ts +0 -35
- package/dist/types/src/extensions/tags/stub.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/widgets/index.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +0 -31
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +0 -1
- package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
- package/dist/types/src/extensions/typewriter.test.d.ts.map +0 -1
- package/src/extensions/autocomplete/typeahead.ts +0 -87
- package/src/extensions/blocks.ts +0 -131
- package/src/extensions/markdown/link.ts +0 -49
- package/src/extensions/outliner/editor.test.ts +0 -33
- package/src/extensions/outliner/outliner.test.ts +0 -99
- package/src/extensions/outliner/outliner.ts +0 -166
- package/src/extensions/outliner/selection.ts +0 -50
- package/src/extensions/state.ts +0 -7
- package/src/extensions/tags/stub.ts +0 -80
- package/src/extensions/tags/xml-block-decoration.ts +0 -123
- package/dist/types/src/extensions/{dnd.d.ts → behavior/dnd.d.ts} +0 -0
- package/dist/types/src/extensions/{submit.d.ts → behavior/submit.d.ts} +0 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/automerge.d.ts +0 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/automerge.test.d.ts +0 -0
- package/dist/types/src/extensions/{automerge → collab/automerge}/sync.d.ts +0 -0
- package/dist/types/src/extensions/{awareness → collab/awareness}/awareness-provider.d.ts +0 -0
- package/dist/types/src/extensions/{awareness → collab/awareness}/index.d.ts +0 -0
- package/dist/types/src/extensions/{autocomplete → completion}/autocomplete.d.ts +0 -0
- package/dist/types/src/extensions/{autocomplete → completion}/placeholder.d.ts +0 -0
- package/dist/types/src/extensions/{factories.test.d.ts → core/factories.test.d.ts} +0 -0
- package/dist/types/src/extensions/{fader.d.ts → decoration/fader.d.ts} +0 -0
- package/dist/types/src/extensions/{marker.d.ts → decoration/marker.d.ts} +0 -0
- package/dist/types/src/extensions/{pos.test.d.ts → decoration/pos.test.d.ts} +0 -0
- package/dist/types/src/extensions/{replacer.test.d.ts → decoration/replacer.test.d.ts} +0 -0
- package/dist/types/src/extensions/{snippets.d.ts → demo/snippets.d.ts} +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/action.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/bundle.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/changes.test.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/formatting.test.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/image.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/image.test.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/index.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/parser.test.d.ts +0 -0
- package/dist/types/src/extensions/{markdown → language/markdown}/table.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/extended-markdown.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/extended-markdown.test.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/index.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/widgets/anchor.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/widgets/index.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/xml-formatting.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/xml-util.d.ts +0 -0
- package/dist/types/src/extensions/{tags → language/xml}/xml-util.test.d.ts +0 -0
- package/dist/types/src/extensions/{modal.d.ts → state/modal.d.ts} +0 -0
- package/dist/types/src/extensions/{selection.test.d.ts → state/selection.test.d.ts} +0 -0
- package/dist/types/src/extensions/{pending → streaming/pending}/index.d.ts +0 -0
- package/dist/types/src/extensions/{pending → streaming/pending}/pending-text-stream.d.ts +0 -0
- package/dist/types/src/extensions/{pending → streaming/pending}/pending-text-stream.test.d.ts +0 -0
- package/dist/types/src/extensions/{pending → streaming/pending}/pending-text.test.d.ts +0 -0
- package/dist/types/src/extensions/{scrolling → streaming/scrolling}/auto-scroll.d.ts +0 -0
- package/dist/types/src/extensions/{scrolling → streaming/scrolling}/crawler.d.ts +0 -0
- package/dist/types/src/extensions/{scrolling → streaming/scrolling}/index.d.ts +0 -0
- package/dist/types/src/extensions/{scrolling → streaming/scrolling}/scrollbar-autohide.d.ts +0 -0
- package/dist/types/src/extensions/{scrolling → streaming/scrolling}/scroller.d.ts +0 -0
- package/dist/types/src/extensions/{typewriter.test.d.ts → streaming/typewriter.test.d.ts} +0 -0
- package/dist/types/src/extensions/{folding.d.ts → structure/folding/folding.d.ts} +0 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/editor.d.ts +0 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/editor.test.d.ts +0 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/outliner.d.ts +0 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/outliner.test.d.ts +0 -0
- package/dist/types/src/extensions/{outliner → structure/outliner}/tree.test.d.ts +0 -0
- package/src/extensions/{dnd.ts → behavior/dnd.ts} +29 -29
- package/src/extensions/{awareness → collab/awareness}/index.ts +0 -0
- package/src/extensions/{factories.test.ts → core/factories.test.ts} +0 -0
- package/src/extensions/{replacer.test.ts → decoration/replacer.test.ts} +0 -0
- package/src/extensions/{replacer.ts → decoration/replacer.ts} +25 -25
- /package/src/extensions/{markdown → language/markdown}/index.ts +0 -0
- /package/src/extensions/{tags → language/xml}/index.ts +0 -0
- /package/src/extensions/{tags → language/xml}/testing/text.md +0 -0
- /package/src/extensions/{tags → language/xml}/testing/text.txt +0 -0
- /package/src/extensions/{tags → language/xml}/widgets/anchor.ts +0 -0
- /package/src/extensions/{tags → language/xml}/widgets/index.ts +0 -0
- /package/src/extensions/{tags → language/xml}/xml-util.ts +0 -0
- /package/src/extensions/{modal.ts → state/modal.ts} +0 -0
- /package/src/extensions/{selection.test.ts → state/selection.test.ts} +0 -0
- /package/src/extensions/{pending → streaming/pending}/index.ts +0 -0
- /package/src/extensions/{pending → streaming/pending}/pending-text-stream.test.ts +0 -0
- /package/src/extensions/{scrolling → streaming/scrolling}/crawler.ts +0 -0
- /package/src/extensions/{scrolling → streaming/scrolling}/index.ts +0 -0
- /package/src/extensions/{scrolling → streaming/scrolling}/scroller.ts +0 -0
- /package/src/extensions/{typewriter.test.ts → streaming/typewriter.test.ts} +0 -0
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorState, type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { Domino } from '@dxos/ui';
|
|
9
|
+
|
|
10
|
+
import { blockSelectionField, getSelectedBlocks, setBlockSelection, toggleBlockSelection } from './selection';
|
|
11
|
+
import { type Block, type BlockExtent } from './types';
|
|
12
|
+
|
|
13
|
+
export type BlockDragOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Enumerates the draggable blocks for a state, in document order. Ranges must be non-overlapping so
|
|
16
|
+
* the gutter, drop-index, and preview geometry stay unambiguous.
|
|
17
|
+
*/
|
|
18
|
+
getBlocks: (state: EditorState) => Block[];
|
|
19
|
+
/**
|
|
20
|
+
* Moves the blocks at `sourceIndices` to the slot before `dropIndex` (the end of the document when
|
|
21
|
+
* `dropIndex === blocks.length`), preserving their relative order, as a single edit. `indent` (from
|
|
22
|
+
* `getDropIndent`, when provided) is the leading-space count the moved block(s) should re-root to.
|
|
23
|
+
*/
|
|
24
|
+
moveBlocks: (view: EditorView, sourceIndices: number[], dropIndex: number, indent?: number) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the drop target's indentation live during a drag: the leading-space count the dragged block(s)
|
|
27
|
+
* would re-root to at `dropIndex` for the current `pointerY`, plus the horizontal pixel `offset` to shift
|
|
28
|
+
* the preview so it previews that level. Omit for blocks whose drop never re-indents (markdown). The
|
|
29
|
+
* outliner uses it to keep an item in its branch vs. outdenting it to a sibling of an ancestor.
|
|
30
|
+
*/
|
|
31
|
+
getDropIndent?: (
|
|
32
|
+
view: EditorView,
|
|
33
|
+
sourceIndices: number[],
|
|
34
|
+
dropIndex: number,
|
|
35
|
+
pointerY: number,
|
|
36
|
+
) => { indent: number; offset: number } | null;
|
|
37
|
+
/**
|
|
38
|
+
* Pin the drag preview to the block's left edge so it only tracks vertically (default `true`).
|
|
39
|
+
* When `false`, the preview follows the pointer on both axes.
|
|
40
|
+
*/
|
|
41
|
+
clampX?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Maps a block to the range it visually occupies for the collapse/preview/placeholder — its subtree for
|
|
44
|
+
* the outliner. Defaults to the block's own range (markdown blocks). The grips and drop-index logic still
|
|
45
|
+
* use `getBlocks` (own, non-overlapping ranges).
|
|
46
|
+
*/
|
|
47
|
+
getExtent?: BlockExtent;
|
|
48
|
+
/**
|
|
49
|
+
* Collapse only the dragged block's content, keeping its trailing line break (default `false`, which
|
|
50
|
+
* collapses through to the next block, consuming a blank-line separator). Set `true` for single-newline
|
|
51
|
+
* separated blocks (the outliner): a block-replace that ends at the next line's start makes CodeMirror
|
|
52
|
+
* attribute that line's `Decoration.line` (its indent) to the removed block, so the following item would
|
|
53
|
+
* lose its indentation while dragging. Keeping the break ends the collapse at the content, preserving it.
|
|
54
|
+
*/
|
|
55
|
+
keepTrailingBreak?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Merges sorted ranges, combining any that overlap or touch (nested subtree extents from a parent and its
|
|
59
|
+
// selected child would otherwise produce overlapping block-replace decorations).
|
|
60
|
+
const mergeRanges = (ranges: { from: number; to: number }[]): { from: number; to: number }[] => {
|
|
61
|
+
const sorted = [...ranges].sort((a, b) => a.from - b.from);
|
|
62
|
+
const merged: { from: number; to: number }[] = [];
|
|
63
|
+
for (const range of sorted) {
|
|
64
|
+
const last = merged.at(-1);
|
|
65
|
+
if (last && range.from <= last.to) {
|
|
66
|
+
last.to = Math.max(last.to, range.to);
|
|
67
|
+
} else {
|
|
68
|
+
merged.push({ ...range });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Space (px) between the text bounds and the box border, applied above and below. Matches the outline
|
|
75
|
+
// layer so the drag preview lifts off exactly over the source block.
|
|
76
|
+
const BOX_PADDING = 2;
|
|
77
|
+
|
|
78
|
+
// Amount (px) the preview box extends left of the content. Kept at 0 so the box, the cloned content,
|
|
79
|
+
// and the drop placeholder all share the content's left edge (they otherwise drift by this much).
|
|
80
|
+
const BOX_INSET_X = 0;
|
|
81
|
+
|
|
82
|
+
// Fine adjustment of the cloned preview content relative to the source (0 = flush over the source line).
|
|
83
|
+
const PREVIEW_OFFSET = { x: 0, y: 0 };
|
|
84
|
+
|
|
85
|
+
// Distance (px) from the viewport's top/bottom edge within which a drag auto-scrolls the editor.
|
|
86
|
+
const AUTOSCROLL_ZONE = 48;
|
|
87
|
+
|
|
88
|
+
// Maximum auto-scroll speed (px per animation frame).
|
|
89
|
+
const AUTOSCROLL_MAX_SPEED = 14;
|
|
90
|
+
|
|
91
|
+
// Pointer travel (px) past which an armed handle press becomes a drag rather than a click.
|
|
92
|
+
const DRAG_THRESHOLD = 4;
|
|
93
|
+
|
|
94
|
+
// The gutters on each side of the content, where the grip (left) and menu (right) float, centered within.
|
|
95
|
+
// `GUTTER_WIDTH` (3rem) is the strip width; `CONTENT_WIDTH` reserves one on each side (`6rem` = 2×3rem) and
|
|
96
|
+
// centers the column — keep the two in sync. Shared by the grip (`drag.ts`), menu (`menu.ts`), and the
|
|
97
|
+
// outliner content (`outliner.ts`).
|
|
98
|
+
// Both utilities need `!`: CodeMirror's own `.ͼ1 .cm-content` rule sets `margin: 0` at a higher
|
|
99
|
+
// specificity, which otherwise drops the centering and collapses both gutters onto one side.
|
|
100
|
+
export const GUTTER_WIDTH = 48;
|
|
101
|
+
export const CONTENT_WIDTH = 'max-w-[min(50rem,100%-6rem)]! mx-auto!';
|
|
102
|
+
|
|
103
|
+
// Square grip size (px) and its drag-handle icon. Matches `dx-button` density `xs` + `aspect-square`
|
|
104
|
+
// (`size-6`), so the button's own box lines up with the centering math below.
|
|
105
|
+
const GRIP_SIZE = 24;
|
|
106
|
+
const GRIP_ICON = 'ph--dots-six-vertical--regular';
|
|
107
|
+
|
|
108
|
+
// A grip's target placement, computed in the measure read phase and applied in the write phase.
|
|
109
|
+
type GripPosition = { index: number; anchor: number; left: number; top: number };
|
|
110
|
+
|
|
111
|
+
// The `from` of the block under the pointer (a hovered block shows its grip), or null.
|
|
112
|
+
const setHoveredBlock = StateEffect.define<number | null>();
|
|
113
|
+
const hoveredBlockField = StateField.define<number | null>({
|
|
114
|
+
create: () => null,
|
|
115
|
+
update: (value, tr) => {
|
|
116
|
+
for (const effect of tr.effects) {
|
|
117
|
+
if (effect.is(setHoveredBlock)) {
|
|
118
|
+
return effect.value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return value != null && tr.docChanged ? tr.changes.mapPos(value, -1) : value;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Tracks the block under the pointer (by its vertical position, so it works over the text, the gutter,
|
|
126
|
+
// or the margin) and publishes it to `hoveredBlockField` so that block's grip can show. Listens on the
|
|
127
|
+
// scroller — which spans the gutter and content — so moving from the text toward the grip doesn't clear
|
|
128
|
+
// it; the hover clears only when the pointer leaves the editor. Dispatches only when the block changes.
|
|
129
|
+
const createHoverPlugin = (getBlocks: BlockDragOptions['getBlocks'], dragPlugin: ReturnType<typeof createDragPlugin>) =>
|
|
130
|
+
ViewPlugin.fromClass(
|
|
131
|
+
class {
|
|
132
|
+
constructor(readonly view: EditorView) {
|
|
133
|
+
view.scrollDOM.addEventListener('mousemove', this.#onMove);
|
|
134
|
+
view.scrollDOM.addEventListener('mouseleave', this.#onLeave);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
destroy() {
|
|
138
|
+
this.view.scrollDOM.removeEventListener('mousemove', this.#onMove);
|
|
139
|
+
this.view.scrollDOM.removeEventListener('mouseleave', this.#onLeave);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#set(anchor: number | null) {
|
|
143
|
+
if (anchor !== (this.view.state.field(hoveredBlockField, false) ?? null)) {
|
|
144
|
+
this.view.dispatch({ effects: setHoveredBlock.of(anchor) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#onMove = (event: MouseEvent) => {
|
|
149
|
+
// While a drag is in flight the pointer sweeps across rows; updating the hovered block here would
|
|
150
|
+
// flicker other rows' grips on and off. The drag plugin owns which grip shows during a drag.
|
|
151
|
+
if (this.view.plugin(dragPlugin)?.dragging) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Map the pointer's row (content-center x + pointer y) to a block, so any horizontal position works.
|
|
155
|
+
const contentRect = this.view.contentDOM.getBoundingClientRect();
|
|
156
|
+
const pos = this.view.posAtCoords({ x: contentRect.left + contentRect.width / 2, y: event.clientY });
|
|
157
|
+
this.#set(
|
|
158
|
+
pos == null
|
|
159
|
+
? null
|
|
160
|
+
: (getBlocks(this.view.state).find((block) => pos >= block.from && pos <= block.to)?.from ?? null),
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
#onLeave = () => this.#set(null);
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// The single block whose grip is shown: the block under the pointer, else the block at the caret (only
|
|
169
|
+
// one handle is ever visible). Grabbing it drags the whole block selection when it is part of one.
|
|
170
|
+
const activeBlockIndex = (state: EditorState, getBlocks: BlockDragOptions['getBlocks']): number | null => {
|
|
171
|
+
const blocks = getBlocks(state);
|
|
172
|
+
const hovered = state.field(hoveredBlockField, false);
|
|
173
|
+
if (hovered != null) {
|
|
174
|
+
const hoverIndex = blocks.findIndex((block) => block.from === hovered);
|
|
175
|
+
if (hoverIndex >= 0) {
|
|
176
|
+
return hoverIndex;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const head = state.selection.main.head;
|
|
180
|
+
const cursorIndex = blocks.findIndex((block) => head >= block.from && head <= block.to);
|
|
181
|
+
return cursorIndex >= 0 ? cursorIndex : null;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Builds a grip element (outer `dx-button` + inner phosphor glyph). Shared by the floating overlay and the
|
|
185
|
+
// drag preview; callers attach behavior and position it.
|
|
186
|
+
const createGripElement = (): HTMLElement =>
|
|
187
|
+
Domino.of('div')
|
|
188
|
+
.classNames('dx-button aspect-square cm-blockDragHandle')
|
|
189
|
+
.attributes({ 'data-variant': 'ghost', 'data-density': 'xs' })
|
|
190
|
+
.append(Domino.of('div').classNames('cm-blockDragHandleIcon').append(Domino.svg(GRIP_ICON))).root;
|
|
191
|
+
|
|
192
|
+
//
|
|
193
|
+
// The in-progress drag's decorations, or `null` when idle: each dragged block (with its trailing blank
|
|
194
|
+
// line) is collapsed out of the document (`collapses`), and a gap (`placeholderPos`) opens at the drop
|
|
195
|
+
// slot so the content closes over the lifted blocks and reopens at the landing spot.
|
|
196
|
+
//
|
|
197
|
+
type DragDecoState = {
|
|
198
|
+
collapses: { from: number; to: number }[];
|
|
199
|
+
placeholderPos: number;
|
|
200
|
+
// Height of the bordered box (the blocks) and the total reserved height (blocks + trailing blanks); the
|
|
201
|
+
// difference is empty space below the box, matching the collapsed separators.
|
|
202
|
+
blockHeight: number;
|
|
203
|
+
totalHeight: number;
|
|
204
|
+
// Horizontal pixel offset for the drop level (see `getDropIndent`), so the placeholder lands where the
|
|
205
|
+
// dragged content lands.
|
|
206
|
+
indentOffset: number;
|
|
207
|
+
} | null;
|
|
208
|
+
|
|
209
|
+
const setDragDeco = StateEffect.define<DragDecoState>();
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Block-level spacer at the drop position. The outer element reserves the full collapsed height so
|
|
213
|
+
* nothing shifts; the inner box carries the border and matches the blocks, leaving the blanks as empty
|
|
214
|
+
* space below.
|
|
215
|
+
*/
|
|
216
|
+
class PlaceholderWidget extends WidgetType {
|
|
217
|
+
constructor(
|
|
218
|
+
readonly blockHeight: number,
|
|
219
|
+
readonly totalHeight: number,
|
|
220
|
+
readonly indentOffset: number,
|
|
221
|
+
) {
|
|
222
|
+
super();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override eq(other: PlaceholderWidget): boolean {
|
|
226
|
+
return (
|
|
227
|
+
other.blockHeight === this.blockHeight &&
|
|
228
|
+
other.totalHeight === this.totalHeight &&
|
|
229
|
+
other.indentOffset === this.indentOffset
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Without this the height map assumes a default-height widget until the async measure pass corrects
|
|
234
|
+
// it, and the correction can shift scroll bookkeeping for a frame at drag start.
|
|
235
|
+
override get estimatedHeight(): number {
|
|
236
|
+
return this.totalHeight;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override toDOM(): HTMLElement {
|
|
240
|
+
const element = document.createElement('div');
|
|
241
|
+
element.className = 'cm-blockDropPlaceholder';
|
|
242
|
+
element.style.height = `${this.totalHeight}px`;
|
|
243
|
+
const box = element.appendChild(document.createElement('div'));
|
|
244
|
+
box.className = 'cm-blockDropPlaceholderBox';
|
|
245
|
+
box.style.height = `${this.blockHeight}px`;
|
|
246
|
+
// Shift the box to the drop level (plus the preview's own x nudge) so its left edge aligns exactly with
|
|
247
|
+
// the dragged content's left edge; the right edge stays at the content's right.
|
|
248
|
+
const shift = this.indentOffset + PREVIEW_OFFSET.x;
|
|
249
|
+
box.style.marginLeft = `${shift}px`;
|
|
250
|
+
box.style.width = `calc(100% - ${shift}px)`;
|
|
251
|
+
return element;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const dragDecoField = StateField.define<DecorationSet>({
|
|
256
|
+
create: () => Decoration.none,
|
|
257
|
+
update: (value, tr) => {
|
|
258
|
+
let next = value.map(tr.changes);
|
|
259
|
+
for (const effect of tr.effects) {
|
|
260
|
+
if (effect.is(setDragDeco)) {
|
|
261
|
+
if (effect.value == null) {
|
|
262
|
+
next = Decoration.none;
|
|
263
|
+
} else {
|
|
264
|
+
const { collapses, placeholderPos, blockHeight, totalHeight, indentOffset } = effect.value;
|
|
265
|
+
next = Decoration.set(
|
|
266
|
+
[
|
|
267
|
+
// Collapse each dragged block (and its trailing blank line) out of the document flow.
|
|
268
|
+
...collapses.map((range) => Decoration.replace({ block: true }).range(range.from, range.to)),
|
|
269
|
+
// Open the gap at the drop slot (before the target line, or after the last line).
|
|
270
|
+
Decoration.widget({
|
|
271
|
+
widget: new PlaceholderWidget(blockHeight, totalHeight, indentOffset),
|
|
272
|
+
block: true,
|
|
273
|
+
side: placeholderPos >= tr.state.doc.length ? 1 : -1,
|
|
274
|
+
}).range(placeholderPos),
|
|
275
|
+
],
|
|
276
|
+
// Sort: the placeholder and collapses may interleave.
|
|
277
|
+
true,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return next;
|
|
283
|
+
},
|
|
284
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/** Manages the armed press → click / drag lifecycle, the drop placeholder, and pointer tracking. */
|
|
288
|
+
const createDragPlugin = (
|
|
289
|
+
getBlocks: BlockDragOptions['getBlocks'],
|
|
290
|
+
moveBlocks: BlockDragOptions['moveBlocks'],
|
|
291
|
+
clampX: boolean,
|
|
292
|
+
getExtent?: BlockExtent,
|
|
293
|
+
keepTrailingBreak = false,
|
|
294
|
+
getDropIndent?: BlockDragOptions['getDropIndent'],
|
|
295
|
+
) =>
|
|
296
|
+
ViewPlugin.fromClass(
|
|
297
|
+
class {
|
|
298
|
+
// Armed but not yet dragging: the press that may become a click or a drag.
|
|
299
|
+
#armed: { index: number; shiftKey: boolean; x: number; y: number } | null = null;
|
|
300
|
+
|
|
301
|
+
// Active drag state (null until a press crosses the drag threshold).
|
|
302
|
+
#sourceIndices: number[] | null = null;
|
|
303
|
+
#dropIndex: number | null = null;
|
|
304
|
+
// Resolved drop indentation for the current pointer (from `getDropIndent`), or null when the block
|
|
305
|
+
// kind does not re-indent on drop. `offset` shifts the preview to preview the target level.
|
|
306
|
+
#dropIndent: { indent: number; offset: number } | null = null;
|
|
307
|
+
#collapseHeights: number[] = [];
|
|
308
|
+
#placeholderKey: string | null = null;
|
|
309
|
+
#preview: HTMLElement | null = null;
|
|
310
|
+
// The cloned content inside the preview; indented (within the fixed container) to preview the drop level.
|
|
311
|
+
#previewContent: HTMLElement | null = null;
|
|
312
|
+
#previewOrigin: { left: number; top: number } | null = null;
|
|
313
|
+
// A grip that rides along with the preview so a drag handle stays visible while dragging (the static
|
|
314
|
+
// overlay grip is hidden during a drag). `#gripOrigin` is its resting viewport position at grab time.
|
|
315
|
+
#previewGrip: HTMLElement | null = null;
|
|
316
|
+
#gripOrigin: { left: number; top: number } | null = null;
|
|
317
|
+
// The first dragged block's viewport rect (the preview lifts off from here) and the grab point.
|
|
318
|
+
#blockRect: { left: number; top: number; width: number; height: number; layoutHeight: number } | null = null;
|
|
319
|
+
#grabX = 0;
|
|
320
|
+
#grabY = 0;
|
|
321
|
+
#lastPointer: { clientX: number; clientY: number } | null = null;
|
|
322
|
+
#scrollFrame: number | null = null;
|
|
323
|
+
|
|
324
|
+
constructor(readonly view: EditorView) {}
|
|
325
|
+
|
|
326
|
+
// True once a press has crossed the drag threshold and a reorder is in flight.
|
|
327
|
+
get dragging(): boolean {
|
|
328
|
+
return this.#sourceIndices != null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The block indices being dragged (null when not dragging).
|
|
332
|
+
get sourceIndices(): number[] | null {
|
|
333
|
+
return this.#sourceIndices;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Abort an in-flight drag when a concurrent/external edit lands — the stored indices would otherwise
|
|
337
|
+
// resolve against a changed document on drop.
|
|
338
|
+
update(update: ViewUpdate) {
|
|
339
|
+
if (this.#sourceIndices && update.docChanged) {
|
|
340
|
+
this.#stop();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
destroy() {
|
|
345
|
+
// Skip the placeholder-clearing dispatch: destroy can run mid-update where dispatching would throw.
|
|
346
|
+
this.#stop(false);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Called from the gutter mousedown: arm a press that resolves to a click or a drag.
|
|
350
|
+
arm(index: number, event: MouseEvent) {
|
|
351
|
+
this.#armed = { index, shiftKey: event.shiftKey, x: event.clientX, y: event.clientY };
|
|
352
|
+
window.addEventListener('mousemove', this.#onArmedMove);
|
|
353
|
+
window.addEventListener('mouseup', this.#onArmedUp);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#disarm() {
|
|
357
|
+
window.removeEventListener('mousemove', this.#onArmedMove);
|
|
358
|
+
window.removeEventListener('mouseup', this.#onArmedUp);
|
|
359
|
+
this.#armed = null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#onArmedMove = (event: MouseEvent) => {
|
|
363
|
+
if (!this.#armed) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (Math.abs(event.clientX - this.#armed.x) + Math.abs(event.clientY - this.#armed.y) > DRAG_THRESHOLD) {
|
|
367
|
+
const { index } = this.#armed;
|
|
368
|
+
this.#disarm();
|
|
369
|
+
this.#beginDrag(index, event);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
#onArmedUp = () => {
|
|
374
|
+
if (this.#armed) {
|
|
375
|
+
const { index, shiftKey } = this.#armed;
|
|
376
|
+
this.#disarm();
|
|
377
|
+
this.#click(index, shiftKey);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// A press without drag: move the caret to the end of the block (collapsing any text selection) and
|
|
382
|
+
// update the block selection in the same transaction (so the field doesn't treat the caret move as a
|
|
383
|
+
// clear). A plain press toggles a single-block selection — selecting only this block, or clearing it
|
|
384
|
+
// when it is already the sole selection; shift toggles it within the set. The block's `from` is its
|
|
385
|
+
// selection identity; the caret goes to its `to`.
|
|
386
|
+
#click(index: number, shiftKey: boolean) {
|
|
387
|
+
const block = getBlocks(this.view.state)[index];
|
|
388
|
+
if (!block) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const anchor = block.from;
|
|
392
|
+
let effect;
|
|
393
|
+
if (shiftKey) {
|
|
394
|
+
effect = toggleBlockSelection.of(anchor);
|
|
395
|
+
} else {
|
|
396
|
+
const current = this.view.state.field(blockSelectionField, false) ?? [];
|
|
397
|
+
const isSoleSelection = current.length === 1 && current[0] === anchor;
|
|
398
|
+
effect = setBlockSelection.of(isSoleSelection ? [] : [anchor]);
|
|
399
|
+
}
|
|
400
|
+
this.view.dispatch({ selection: { anchor: block.to }, effects: effect });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Enter drag mode: drag the whole block selection when the grabbed block is part of it, else just
|
|
404
|
+
// the grabbed block. Grabbing a block outside the current selection shifts the selection to it, so
|
|
405
|
+
// the dragged block shows as selected immediately.
|
|
406
|
+
#beginDrag(index: number, event: MouseEvent) {
|
|
407
|
+
const selected = getSelectedBlocks(this.view.state, getBlocks).map((entry) => entry.index);
|
|
408
|
+
const indices = selected.length > 1 && selected.includes(index) ? selected : [index];
|
|
409
|
+
if (!selected.includes(index)) {
|
|
410
|
+
const block = getBlocks(this.view.state)[index];
|
|
411
|
+
if (block) {
|
|
412
|
+
this.view.dispatch({ effects: setBlockSelection.of([block.from]) });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
this.#sourceIndices = indices;
|
|
416
|
+
this.#dropIndex = indices[0];
|
|
417
|
+
this.#grabX = event.clientX;
|
|
418
|
+
this.#grabY = event.clientY;
|
|
419
|
+
this.#lastPointer = { clientX: event.clientX, clientY: event.clientY };
|
|
420
|
+
this.#blockRect = this.#measureBlock(indices[0]);
|
|
421
|
+
this.#collapseHeights = indices.map((sourceIndex) => this.#measureCollapseHeight(sourceIndex));
|
|
422
|
+
// Measure and clone before the collapse decoration removes the source from the DOM.
|
|
423
|
+
this.#preview = this.#buildPreview(indices);
|
|
424
|
+
if (this.#preview) {
|
|
425
|
+
this.#preview.style.transform = 'none';
|
|
426
|
+
const rect = this.#preview.getBoundingClientRect();
|
|
427
|
+
this.#previewOrigin = { left: rect.left, top: rect.top };
|
|
428
|
+
}
|
|
429
|
+
// A grip rides with the preview: hidden as the static overlay grip, re-shown here at the same
|
|
430
|
+
// resting spot (gutter-centered, on the first row) and translated by the pointer delta each move.
|
|
431
|
+
this.#gripOrigin = this.#measureGripOrigin(indices[0]);
|
|
432
|
+
if (this.#gripOrigin) {
|
|
433
|
+
this.#previewGrip = createGripElement();
|
|
434
|
+
this.#previewGrip.classList.add('cm-blockDragPreviewHandle');
|
|
435
|
+
this.view.dom.appendChild(this.#previewGrip);
|
|
436
|
+
}
|
|
437
|
+
this.#positionPreview(event.clientX, event.clientY);
|
|
438
|
+
this.#updateDragDeco(this.#dropIndex);
|
|
439
|
+
this.view.scrollDOM.classList.add('cm-blockDragging');
|
|
440
|
+
window.addEventListener('mousemove', this.#onMove);
|
|
441
|
+
window.addEventListener('mouseup', this.#onUp);
|
|
442
|
+
this.#scrollFrame = requestAnimationFrame(this.#autoScrollTick);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#stop(clearPlaceholder = true) {
|
|
446
|
+
this.#disarm();
|
|
447
|
+
window.removeEventListener('mousemove', this.#onMove);
|
|
448
|
+
window.removeEventListener('mouseup', this.#onUp);
|
|
449
|
+
if (this.#scrollFrame != null) {
|
|
450
|
+
cancelAnimationFrame(this.#scrollFrame);
|
|
451
|
+
this.#scrollFrame = null;
|
|
452
|
+
}
|
|
453
|
+
this.view.scrollDOM.classList.remove('cm-blockDragging');
|
|
454
|
+
if (clearPlaceholder && this.#placeholderKey != null) {
|
|
455
|
+
this.view.dispatch({ effects: setDragDeco.of(null) });
|
|
456
|
+
}
|
|
457
|
+
this.#placeholderKey = null;
|
|
458
|
+
this.#collapseHeights = [];
|
|
459
|
+
this.#preview?.remove();
|
|
460
|
+
this.#preview = null;
|
|
461
|
+
this.#previewContent = null;
|
|
462
|
+
this.#previewOrigin = null;
|
|
463
|
+
this.#previewGrip?.remove();
|
|
464
|
+
this.#previewGrip = null;
|
|
465
|
+
this.#gripOrigin = null;
|
|
466
|
+
this.#blockRect = null;
|
|
467
|
+
this.#sourceIndices = null;
|
|
468
|
+
this.#dropIndex = null;
|
|
469
|
+
this.#dropIndent = null;
|
|
470
|
+
this.#lastPointer = null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The range the block at `sourceIndex` visually occupies (its subtree for the outliner, else its own
|
|
474
|
+
// range).
|
|
475
|
+
#extentOf(sourceIndex: number): Block | null {
|
|
476
|
+
const block = getBlocks(this.view.state)[sourceIndex];
|
|
477
|
+
if (!block) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
return getExtent ? getExtent(this.view.state, block) : block;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Start of the first block after `pos`, or the document end when there is none — the point a collapse
|
|
484
|
+
// extends to so it swallows the trailing blank/separator.
|
|
485
|
+
#nextStartAfter(pos: number): number {
|
|
486
|
+
const next = getBlocks(this.view.state).find((block) => block.from > pos);
|
|
487
|
+
return next ? next.from : this.view.state.doc.length;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Rendered height of the region a block's collapse hides: the block (its extent) plus the blank line
|
|
491
|
+
// below it.
|
|
492
|
+
#measureCollapseHeight(sourceIndex: number): number {
|
|
493
|
+
const extent = this.#extentOf(sourceIndex);
|
|
494
|
+
const firstRect = extent && this.#lineRect(extent.from);
|
|
495
|
+
if (!extent || !firstRect) {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
const nextStart = this.#nextStartAfter(extent.to);
|
|
499
|
+
if (nextStart >= this.view.state.doc.length) {
|
|
500
|
+
const lastRect = this.#lineRect(extent.to);
|
|
501
|
+
return lastRect ? lastRect.bottom - firstRect.top : 0;
|
|
502
|
+
}
|
|
503
|
+
const nextTop = this.#lineRect(nextStart)?.top;
|
|
504
|
+
return nextTop != null ? nextTop - firstRect.top : 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// The `.cm-line` DOM rect at `pos`, or null if the line isn't rendered.
|
|
508
|
+
#lineRect(pos: number): DOMRect | null {
|
|
509
|
+
const { node } = this.view.domAtPos(pos);
|
|
510
|
+
const element = node instanceof Element ? node : node.parentElement;
|
|
511
|
+
const lineElement = element?.closest('.cm-line');
|
|
512
|
+
return lineElement ? lineElement.getBoundingClientRect() : null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// The block's box rect in viewport coords, measured from its rendered `.cm-line` rects (the
|
|
516
|
+
// `lineBlockAt` geometry drifts by a pixel or two). Falls back to `lineBlockAt` when off-screen.
|
|
517
|
+
#measureBlock(sourceIndex: number): {
|
|
518
|
+
left: number;
|
|
519
|
+
top: number;
|
|
520
|
+
width: number;
|
|
521
|
+
height: number;
|
|
522
|
+
layoutHeight: number;
|
|
523
|
+
} | null {
|
|
524
|
+
const extent = this.#extentOf(sourceIndex);
|
|
525
|
+
if (!extent) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const contentRect = this.view.contentDOM.getBoundingClientRect();
|
|
530
|
+
const firstRect = this.#lineRect(extent.from);
|
|
531
|
+
const lastRect = this.#lineRect(extent.to);
|
|
532
|
+
if (firstRect && lastRect) {
|
|
533
|
+
const layoutHeight = lastRect.bottom - firstRect.top;
|
|
534
|
+
return {
|
|
535
|
+
left: contentRect.left - BOX_INSET_X,
|
|
536
|
+
top: firstRect.top - BOX_PADDING,
|
|
537
|
+
width: contentRect.width + BOX_INSET_X,
|
|
538
|
+
height: layoutHeight + BOX_PADDING * 2,
|
|
539
|
+
layoutHeight,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const firstLine = this.view.lineBlockAt(extent.from);
|
|
544
|
+
const lastLine = this.view.lineBlockAt(extent.to);
|
|
545
|
+
const layoutHeight = lastLine.bottom - firstLine.top;
|
|
546
|
+
return {
|
|
547
|
+
left: contentRect.left - BOX_INSET_X,
|
|
548
|
+
top: contentRect.top + firstLine.top - BOX_PADDING,
|
|
549
|
+
width: contentRect.width + BOX_INSET_X,
|
|
550
|
+
height: layoutHeight + BOX_PADDING * 2,
|
|
551
|
+
layoutHeight,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Clones the dragged blocks' rendered lines into a floating preview inside `.cm-editor`. A single
|
|
556
|
+
// block is sized to its exact rect (pixel-perfect lift-off); multiple blocks stack at natural height.
|
|
557
|
+
#buildPreview(sourceIndices: number[]): HTMLElement | null {
|
|
558
|
+
const { state } = this.view;
|
|
559
|
+
if (!this.#blockRect) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const preview = document.createElement('div');
|
|
564
|
+
preview.className = 'cm-blockDragPreview';
|
|
565
|
+
preview.style.width = `${this.#blockRect.width}px`;
|
|
566
|
+
if (sourceIndices.length === 1) {
|
|
567
|
+
preview.style.height = `${this.#blockRect.height}px`;
|
|
568
|
+
}
|
|
569
|
+
preview.style.paddingLeft = `${BOX_INSET_X}px`;
|
|
570
|
+
|
|
571
|
+
const content = document.createElement('div');
|
|
572
|
+
content.className = 'cm-lineWrapping';
|
|
573
|
+
content.style.width = `${this.view.contentDOM.clientWidth}px`;
|
|
574
|
+
content.style.transform = `translate(${PREVIEW_OFFSET.x}px, ${PREVIEW_OFFSET.y}px)`;
|
|
575
|
+
|
|
576
|
+
const seen = new Set<Element>();
|
|
577
|
+
sourceIndices.forEach((sourceIndex, order) => {
|
|
578
|
+
const extent = this.#extentOf(sourceIndex);
|
|
579
|
+
if (!extent) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (order > 0) {
|
|
583
|
+
const spacer = content.appendChild(document.createElement('div'));
|
|
584
|
+
spacer.style.height = `${this.view.defaultLineHeight}px`;
|
|
585
|
+
}
|
|
586
|
+
const startLine = state.doc.lineAt(extent.from).number;
|
|
587
|
+
const endLine = state.doc.lineAt(extent.to).number;
|
|
588
|
+
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
|
|
589
|
+
const { node } = this.view.domAtPos(state.doc.line(lineNumber).from);
|
|
590
|
+
const element = node instanceof Element ? node : node.parentElement;
|
|
591
|
+
const lineElement = element?.closest('.cm-line');
|
|
592
|
+
if (lineElement && !seen.has(lineElement)) {
|
|
593
|
+
seen.add(lineElement);
|
|
594
|
+
content.appendChild(lineElement.cloneNode(true));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
preview.appendChild(content);
|
|
600
|
+
// Held so the drop-level preview indents the content within the (unmoved) container.
|
|
601
|
+
this.#previewContent = content;
|
|
602
|
+
return this.view.dom.appendChild(preview);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// The grip's resting viewport position for the block at `sourceIndex`: gutter-centered horizontally
|
|
606
|
+
// (matching the static overlay grip) and centered on the block's first row.
|
|
607
|
+
#measureGripOrigin(sourceIndex: number): { left: number; top: number } | null {
|
|
608
|
+
const block = getBlocks(this.view.state)[sourceIndex];
|
|
609
|
+
const coords = block && this.view.coordsAtPos(block.from);
|
|
610
|
+
if (!coords) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const contentRect = this.view.contentDOM.getBoundingClientRect();
|
|
614
|
+
return {
|
|
615
|
+
left: contentRect.left - GUTTER_WIDTH / 2 - GRIP_SIZE / 2,
|
|
616
|
+
top: (coords.top + coords.bottom) / 2 - GRIP_SIZE / 2,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#positionPreview(clientX: number, clientY: number) {
|
|
621
|
+
const dx = clampX ? 0 : clientX - this.#grabX;
|
|
622
|
+
const dy = clientY - this.#grabY;
|
|
623
|
+
if (this.#preview && this.#previewOrigin && this.#blockRect) {
|
|
624
|
+
const left = this.#blockRect.left + dx - this.#previewOrigin.left;
|
|
625
|
+
const top = this.#blockRect.top + dy - this.#previewOrigin.top;
|
|
626
|
+
this.#preview.style.transform = `translate(${left}px, ${top}px)`;
|
|
627
|
+
}
|
|
628
|
+
// Preview the target indent level (from `getDropIndent`) by indenting the content WITHIN the
|
|
629
|
+
// container — the container and grip stay put, only the text shifts to show where it will land.
|
|
630
|
+
if (this.#previewContent) {
|
|
631
|
+
const indentOffset = this.#dropIndent?.offset ?? 0;
|
|
632
|
+
this.#previewContent.style.transform = `translate(${PREVIEW_OFFSET.x + indentOffset}px, ${PREVIEW_OFFSET.y}px)`;
|
|
633
|
+
}
|
|
634
|
+
if (this.#previewGrip && this.#gripOrigin) {
|
|
635
|
+
this.#previewGrip.style.left = `${this.#gripOrigin.left + dx}px`;
|
|
636
|
+
this.#previewGrip.style.top = `${this.#gripOrigin.top + dy}px`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The slot the pointer is over: the first non-source block whose vertical midpoint is below the
|
|
641
|
+
// pointer. The dragged (collapsed) blocks are skipped — including blocks inside a dragged extent
|
|
642
|
+
// (a parent's children share its collapse but are not source indices) — so the drop gap, and thus
|
|
643
|
+
// the placeholder, always lands on a visible block, never inside the lifted group where the
|
|
644
|
+
// collapse decoration would absorb it.
|
|
645
|
+
#dropIndexAt(clientY: number): number {
|
|
646
|
+
const blocks = getBlocks(this.view.state);
|
|
647
|
+
const sources = new Set(this.#sourceIndices ?? []);
|
|
648
|
+
const extents = [...sources]
|
|
649
|
+
.map((sourceIndex) => this.#extentOf(sourceIndex))
|
|
650
|
+
.filter((extent): extent is Block => extent != null);
|
|
651
|
+
const collapsed = (block: Block) =>
|
|
652
|
+
extents.some((extent) => block.from >= extent.from && block.from <= extent.to);
|
|
653
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
654
|
+
if (sources.has(index) || collapsed(blocks[index])) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const top = this.view.coordsAtPos(blocks[index].from);
|
|
658
|
+
const bottom = this.view.coordsAtPos(blocks[index].to);
|
|
659
|
+
if (!top || !bottom) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (clientY < (top.top + bottom.bottom) / 2) {
|
|
663
|
+
return index;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return blocks.length;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#onMove = (event: MouseEvent) => {
|
|
670
|
+
this.#lastPointer = { clientX: event.clientX, clientY: event.clientY };
|
|
671
|
+
this.#updateFromPointer();
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
#updateFromPointer() {
|
|
675
|
+
if (!this.#lastPointer || this.#sourceIndices == null) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
this.#dropIndex = this.#dropIndexAt(this.#lastPointer.clientY);
|
|
679
|
+
this.#dropIndent =
|
|
680
|
+
getDropIndent?.(this.view, this.#sourceIndices, this.#dropIndex, this.#lastPointer.clientY) ?? null;
|
|
681
|
+
this.#updateDragDeco(this.#dropIndex);
|
|
682
|
+
this.#positionPreview(this.#lastPointer.clientX, this.#lastPointer.clientY);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#autoScrollDelta(clientY: number): number {
|
|
686
|
+
const rect = this.view.scrollDOM.getBoundingClientRect();
|
|
687
|
+
const fromTop = clientY - rect.top;
|
|
688
|
+
const fromBottom = rect.bottom - clientY;
|
|
689
|
+
if (fromTop < AUTOSCROLL_ZONE) {
|
|
690
|
+
const intensity = Math.min(1, (AUTOSCROLL_ZONE - fromTop) / AUTOSCROLL_ZONE);
|
|
691
|
+
return -Math.max(1, Math.round(intensity * AUTOSCROLL_MAX_SPEED));
|
|
692
|
+
}
|
|
693
|
+
if (fromBottom < AUTOSCROLL_ZONE) {
|
|
694
|
+
const intensity = Math.min(1, (AUTOSCROLL_ZONE - fromBottom) / AUTOSCROLL_ZONE);
|
|
695
|
+
return Math.max(1, Math.round(intensity * AUTOSCROLL_MAX_SPEED));
|
|
696
|
+
}
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
#autoScrollTick = () => {
|
|
701
|
+
this.#scrollFrame = null;
|
|
702
|
+
if (this.#sourceIndices == null || !this.#lastPointer) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const delta = this.#autoScrollDelta(this.#lastPointer.clientY);
|
|
706
|
+
if (delta !== 0) {
|
|
707
|
+
const scroller = this.view.scrollDOM;
|
|
708
|
+
const before = scroller.scrollTop;
|
|
709
|
+
scroller.scrollTop = before + delta;
|
|
710
|
+
if (scroller.scrollTop !== before) {
|
|
711
|
+
this.#updateFromPointer();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
this.#scrollFrame = requestAnimationFrame(this.#autoScrollTick);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
#onUp = () => {
|
|
718
|
+
const sources = this.#sourceIndices;
|
|
719
|
+
const drop = this.#dropIndex;
|
|
720
|
+
const indent = this.#dropIndent?.indent;
|
|
721
|
+
this.#stop();
|
|
722
|
+
if (sources != null && drop != null) {
|
|
723
|
+
moveBlocks(this.view, sources, drop, indent);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// Collapses the dragged blocks out of the document and opens a gap at the drop slot. Skips the
|
|
728
|
+
// dispatch when the drop position and collapse set are unchanged, so it only re-renders on a move.
|
|
729
|
+
#updateDragDeco(dropIndex: number) {
|
|
730
|
+
const sources = this.#sourceIndices;
|
|
731
|
+
if (sources == null) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const blocks = getBlocks(this.view.state);
|
|
735
|
+
// Collapse each dragged block's extent (its subtree for the outliner) plus its trailing separator,
|
|
736
|
+
// merged so nested/adjacent extents don't produce overlapping block-replace decorations.
|
|
737
|
+
const collapses = mergeRanges(
|
|
738
|
+
sources
|
|
739
|
+
.map((sourceIndex) => {
|
|
740
|
+
const extent = this.#extentOf(sourceIndex);
|
|
741
|
+
// Collapse through the separator (next block start) for blank-line separated blocks, or stop
|
|
742
|
+
// at the content end for single-newline blocks so the next line keeps its decoration (indent).
|
|
743
|
+
return extent
|
|
744
|
+
? { from: extent.from, to: keepTrailingBreak ? extent.to : this.#nextStartAfter(extent.to) }
|
|
745
|
+
: null;
|
|
746
|
+
})
|
|
747
|
+
.filter((range): range is { from: number; to: number } => range != null),
|
|
748
|
+
);
|
|
749
|
+
const totalHeight = this.#collapseHeights.reduce((sum, height) => sum + height, 0);
|
|
750
|
+
const blockHeight =
|
|
751
|
+
totalHeight - (this.#blockRect ? this.#collapseHeights[0] - this.#blockRect.layoutHeight : 0);
|
|
752
|
+
if (totalHeight <= 0) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// A side:-1 block widget sitting inside a collapse (including at its end) is absorbed by the
|
|
757
|
+
// block-replace and renders nothing — the gap would vanish for that update. Snap such positions
|
|
758
|
+
// to that collapse's START, which renders before it — the source's own slot, a no-op drop, so
|
|
759
|
+
// the position is right.
|
|
760
|
+
let placeholderPos = dropIndex < blocks.length ? blocks[dropIndex].from : this.view.state.doc.length;
|
|
761
|
+
const atCollapse = collapses.find((range) => placeholderPos > range.from && placeholderPos <= range.to);
|
|
762
|
+
if (atCollapse) {
|
|
763
|
+
placeholderPos = atCollapse.from;
|
|
764
|
+
}
|
|
765
|
+
// The drop level (quantized to indent units) shifts the placeholder so it aligns with the content.
|
|
766
|
+
const indentOffset = this.#dropIndent?.offset ?? 0;
|
|
767
|
+
const key = `${placeholderPos}:${indentOffset}:${collapses.map((range) => range.from).join(',')}`;
|
|
768
|
+
if (key === this.#placeholderKey) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.#placeholderKey = key;
|
|
773
|
+
this.view.dispatch({
|
|
774
|
+
effects: setDragDeco.of({ collapses, placeholderPos, blockHeight, totalHeight, indentOffset }),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const dragTheme = EditorView.theme({
|
|
781
|
+
// The grip is a floating overlay (see `createGripOverlay`) pinned just left of the content, so it tracks
|
|
782
|
+
// a centered content column — a CodeMirror gutter is stuck at the scroller edge and can't. Positioned
|
|
783
|
+
// via fixed coordinates (viewport space), refreshed on layout/scroll. `dx-button` supplies the hover
|
|
784
|
+
// affordance; this only pins/sizes it.
|
|
785
|
+
'.cm-blockDragHandle': {
|
|
786
|
+
position: 'fixed',
|
|
787
|
+
zIndex: '5',
|
|
788
|
+
cursor: 'grab',
|
|
789
|
+
},
|
|
790
|
+
'.cm-blockDragHandleIcon': {
|
|
791
|
+
display: 'grid',
|
|
792
|
+
placeContent: 'center',
|
|
793
|
+
fontSize: '16px',
|
|
794
|
+
color: 'var(--color-description, currentColor)',
|
|
795
|
+
opacity: '0.4',
|
|
796
|
+
transition: 'opacity 0.2s',
|
|
797
|
+
},
|
|
798
|
+
'.cm-blockDragHandle:hover .cm-blockDragHandleIcon': {
|
|
799
|
+
opacity: '1',
|
|
800
|
+
},
|
|
801
|
+
// The grip carried by the drag preview: above the preview, fully opaque, non-interactive.
|
|
802
|
+
'.cm-blockDragPreviewHandle': {
|
|
803
|
+
zIndex: '7',
|
|
804
|
+
cursor: 'grabbing',
|
|
805
|
+
pointerEvents: 'none',
|
|
806
|
+
},
|
|
807
|
+
'.cm-blockDragPreviewHandle .cm-blockDragHandleIcon': {
|
|
808
|
+
opacity: '1',
|
|
809
|
+
},
|
|
810
|
+
'.cm-scroller.cm-blockDragging': {
|
|
811
|
+
cursor: 'grabbing',
|
|
812
|
+
},
|
|
813
|
+
// Hide the static overlay grips synchronously at drag start (they are children of the scroller; the
|
|
814
|
+
// preview grip rides on `view.dom`): the overlay only removes them in the async measure pass, which
|
|
815
|
+
// would leave both grips visible for a frame.
|
|
816
|
+
'.cm-scroller.cm-blockDragging .cm-blockDragHandle': {
|
|
817
|
+
display: 'none',
|
|
818
|
+
},
|
|
819
|
+
'.cm-scroller.cm-blockDragging .cm-content': {
|
|
820
|
+
userSelect: 'none',
|
|
821
|
+
},
|
|
822
|
+
// Outer container reserves the full collapsed height (blocks + blanks) but is invisible; the blanks end
|
|
823
|
+
// up as empty space below the bordered box.
|
|
824
|
+
'.cm-blockDropPlaceholder': {
|
|
825
|
+
pointerEvents: 'none',
|
|
826
|
+
},
|
|
827
|
+
// Left/width are set inline per drop (they track the drop level so the box aligns with the content).
|
|
828
|
+
// Unpainted: the drop slot reads as empty space matching the anticipated dropped block.
|
|
829
|
+
'.cm-blockDropPlaceholderBox': {
|
|
830
|
+
boxSizing: 'border-box',
|
|
831
|
+
},
|
|
832
|
+
'.cm-blockDragPreview': {
|
|
833
|
+
position: 'absolute',
|
|
834
|
+
top: '0',
|
|
835
|
+
left: '0',
|
|
836
|
+
zIndex: '6',
|
|
837
|
+
pointerEvents: 'none',
|
|
838
|
+
boxSizing: 'border-box',
|
|
839
|
+
overflow: 'hidden',
|
|
840
|
+
opacity: '0.9',
|
|
841
|
+
// Matches the height reserved by BOX_PADDING so the content sits symmetrically (no extra bottom gap).
|
|
842
|
+
padding: `${BOX_PADDING}px 0`,
|
|
843
|
+
backgroundColor: 'var(--color-base-surface, Canvas)',
|
|
844
|
+
boxShadow: '0 8px 24px rgb(0 0 0 / 0.35)',
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Drag-to-reorder for arbitrary document blocks, with click/shift-click block selection. The gutter shows
|
|
850
|
+
* a grip on the block under the pointer, the block at the caret, and every selected block; pressing a grip
|
|
851
|
+
* toggles the block selection, dragging it reorders the block (or the whole selection). A floating
|
|
852
|
+
* preview, a drop placeholder, and edge auto-scroll accompany the drag. Blocks and reorder semantics come
|
|
853
|
+
* from the caller (see `blocks` for markdown blocks, and the outliner for task lines). Pairs with
|
|
854
|
+
* `createBlockSelection`, which owns the selection state, highlight, and clipboard.
|
|
855
|
+
*/
|
|
856
|
+
export const createBlockDrag = ({
|
|
857
|
+
getBlocks,
|
|
858
|
+
moveBlocks,
|
|
859
|
+
clampX = true,
|
|
860
|
+
getExtent,
|
|
861
|
+
keepTrailingBreak = false,
|
|
862
|
+
getDropIndent,
|
|
863
|
+
}: BlockDragOptions): Extension => {
|
|
864
|
+
const dragPlugin = createDragPlugin(getBlocks, moveBlocks, clampX, getExtent, keepTrailingBreak, getDropIndent);
|
|
865
|
+
return [
|
|
866
|
+
dragTheme,
|
|
867
|
+
dragDecoField,
|
|
868
|
+
hoveredBlockField,
|
|
869
|
+
createHoverPlugin(getBlocks, dragPlugin),
|
|
870
|
+
dragPlugin,
|
|
871
|
+
createGripOverlay(getBlocks, dragPlugin),
|
|
872
|
+
];
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Floating grip overlay, one per active block (caret / hovered / selected), pinned just left of the
|
|
877
|
+
* content's left edge — unlike a CodeMirror gutter (stuck at the scroller edge) it tracks a `max-width` +
|
|
878
|
+
* `mx-auto` centered content column. Positions come from `coordsAtPos` (real DOM geometry, not the height
|
|
879
|
+
* map) and refresh on layout and scroll; presses arm the drag plugin. Grips whose row is scrolled out of
|
|
880
|
+
* the editor are dropped.
|
|
881
|
+
*/
|
|
882
|
+
const createGripOverlay = (
|
|
883
|
+
getBlocks: BlockDragOptions['getBlocks'],
|
|
884
|
+
dragPlugin: ReturnType<typeof createDragPlugin>,
|
|
885
|
+
): Extension =>
|
|
886
|
+
ViewPlugin.fromClass(
|
|
887
|
+
class {
|
|
888
|
+
// Grip elements keyed by the block anchor they mark; rebuilt as the active set changes.
|
|
889
|
+
#grips = new Map<number, HTMLElement>();
|
|
890
|
+
constructor(readonly view: EditorView) {
|
|
891
|
+
view.scrollDOM.addEventListener('scroll', this.#onScroll);
|
|
892
|
+
this.#schedule();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
update(update: ViewUpdate) {
|
|
896
|
+
const selectionChanged =
|
|
897
|
+
update.startState.field(blockSelectionField, false) !== update.state.field(blockSelectionField, false);
|
|
898
|
+
const hoverChanged =
|
|
899
|
+
update.startState.field(hoveredBlockField, false) !== update.state.field(hoveredBlockField, false);
|
|
900
|
+
if (
|
|
901
|
+
update.geometryChanged ||
|
|
902
|
+
update.docChanged ||
|
|
903
|
+
update.viewportChanged ||
|
|
904
|
+
update.selectionSet ||
|
|
905
|
+
selectionChanged ||
|
|
906
|
+
hoverChanged
|
|
907
|
+
) {
|
|
908
|
+
this.#schedule();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
destroy() {
|
|
913
|
+
this.view.scrollDOM.removeEventListener('scroll', this.#onScroll);
|
|
914
|
+
for (const grip of this.#grips.values()) {
|
|
915
|
+
grip.remove();
|
|
916
|
+
}
|
|
917
|
+
this.#grips.clear();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
#onScroll = () => this.#schedule();
|
|
921
|
+
|
|
922
|
+
// A keyed measure runs in CodeMirror's measure cycle — after layout, including the initial one — so
|
|
923
|
+
// `coordsAtPos` is valid (unlike a bare `rAF` fired before first layout). Requests coalesce by key.
|
|
924
|
+
#measure = {
|
|
925
|
+
key: this,
|
|
926
|
+
read: (view: EditorView): GripPosition[] => this.#read(view),
|
|
927
|
+
write: (positions: GripPosition[]) => this.#write(positions),
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
#schedule() {
|
|
931
|
+
this.view.requestMeasure(this.#measure);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#read(view: EditorView): GripPosition[] {
|
|
935
|
+
// While dragging, the static grip is hidden — the drag preview carries its own handle instead.
|
|
936
|
+
if (view.plugin(dragPlugin)?.dragging) {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
const index = activeBlockIndex(view.state, getBlocks);
|
|
940
|
+
if (index == null) {
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
const block = getBlocks(view.state)[index];
|
|
944
|
+
const coords = block && view.coordsAtPos(block.from);
|
|
945
|
+
const scrollRect = view.scrollDOM.getBoundingClientRect();
|
|
946
|
+
// Drop the grip when its first row is scrolled out of the editor viewport.
|
|
947
|
+
if (!coords || coords.bottom <= scrollRect.top || coords.top >= scrollRect.bottom) {
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
// Centered within the 3rem gutter immediately left of the content.
|
|
951
|
+
const contentRect = view.contentDOM.getBoundingClientRect();
|
|
952
|
+
const left = contentRect.left - GUTTER_WIDTH / 2 - GRIP_SIZE / 2;
|
|
953
|
+
return [{ index, anchor: block.from, left, top: (coords.top + coords.bottom) / 2 - GRIP_SIZE / 2 }];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
#write(positions: GripPosition[]) {
|
|
957
|
+
const active = new Set<number>();
|
|
958
|
+
for (const { index, anchor, left, top } of positions) {
|
|
959
|
+
active.add(index);
|
|
960
|
+
let grip = this.#grips.get(index);
|
|
961
|
+
if (!grip) {
|
|
962
|
+
grip = this.#createGrip();
|
|
963
|
+
this.#grips.set(index, grip);
|
|
964
|
+
}
|
|
965
|
+
grip.dataset.anchor = String(anchor);
|
|
966
|
+
grip.style.left = `${left}px`;
|
|
967
|
+
grip.style.top = `${top}px`;
|
|
968
|
+
}
|
|
969
|
+
for (const [index, grip] of this.#grips) {
|
|
970
|
+
if (!active.has(index)) {
|
|
971
|
+
grip.remove();
|
|
972
|
+
this.#grips.delete(index);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#createGrip(): HTMLElement {
|
|
978
|
+
const grip = createGripElement();
|
|
979
|
+
grip.addEventListener('mousedown', (event) => {
|
|
980
|
+
if (event.button !== 0) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
// Resolve the block from the grip's anchor at press time (indices shift as the doc changes).
|
|
984
|
+
const anchor = Number(grip.dataset.anchor);
|
|
985
|
+
const sourceIndex = getBlocks(this.view.state).findIndex((entry) => entry.from === anchor);
|
|
986
|
+
if (sourceIndex < 0) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.view.plugin(dragPlugin)?.arm(sourceIndex, event);
|
|
990
|
+
event.preventDefault();
|
|
991
|
+
});
|
|
992
|
+
this.view.scrollDOM.appendChild(grip);
|
|
993
|
+
return grip;
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
);
|