@dou-ui/editor 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/catalog.js +18 -0
- package/dist/catalog.js.map +1 -0
- package/dist/code/code-editor-block.js +6 -0
- package/dist/code/code-editor-block.js.map +1 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js +116 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/code/index.js +69 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/languages.js +51 -0
- package/dist/code/languages.js.map +1 -0
- package/dist/code/view.js +255 -0
- package/dist/code/view.js.map +1 -0
- package/dist/collaboration/binding.js +381 -0
- package/dist/collaboration/binding.js.map +1 -0
- package/dist/collaboration/cursors.js +150 -0
- package/dist/collaboration/cursors.js.map +1 -0
- package/dist/collaboration/index.js +108 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/collaboration/table.js +234 -0
- package/dist/collaboration/table.js.map +1 -0
- package/dist/collaboration/text.js +91 -0
- package/dist/collaboration/text.js.map +1 -0
- package/dist/components/block-handle.js +6 -0
- package/dist/components/block-handle.js.map +1 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js +474 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/bubble-toolbar.js +6 -0
- package/dist/components/bubble-toolbar.js.map +1 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js +381 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-button.js +6 -0
- package/dist/components/comment-button.js.map +1 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js +83 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-card.js +6 -0
- package/dist/components/comment-card.js.map +1 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js +104 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/menu-escape.js +25 -0
- package/dist/components/menu-escape.js.map +1 -0
- package/dist/components/search-bar.js +6 -0
- package/dist/components/search-bar.js.map +1 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js +192 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/slash-menu.js +6 -0
- package/dist/components/slash-menu.js.map +1 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js +164 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/core/align.js +94 -0
- package/dist/core/align.js.map +1 -0
- package/dist/core/block-selection.js +155 -0
- package/dist/core/block-selection.js.map +1 -0
- package/dist/core/blocks.js +132 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/changes.js +85 -0
- package/dist/core/changes.js.map +1 -0
- package/dist/core/colors.js +16 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/commands.js +379 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/comments.js +186 -0
- package/dist/core/comments.js.map +1 -0
- package/dist/core/compare.js +106 -0
- package/dist/core/compare.js.map +1 -0
- package/dist/core/convert.js +28 -0
- package/dist/core/convert.js.map +1 -0
- package/dist/core/diff.js +141 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/editor.js +138 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/extension.js +99 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/file-drop.js +56 -0
- package/dist/core/file-drop.js.map +1 -0
- package/dist/core/highlight.js +120 -0
- package/dist/core/highlight.js.map +1 -0
- package/dist/core/input-rules.js +61 -0
- package/dist/core/input-rules.js.map +1 -0
- package/dist/core/keymap.js +38 -0
- package/dist/core/keymap.js.map +1 -0
- package/dist/core/model.js +196 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/output.js +263 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/plugins.js +102 -0
- package/dist/core/plugins.js.map +1 -0
- package/dist/core/prepare.js +102 -0
- package/dist/core/prepare.js.map +1 -0
- package/dist/core/schema.js +158 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.js +197 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/selection.js +16 -0
- package/dist/core/selection.js.map +1 -0
- package/dist/core/slash.js +83 -0
- package/dist/core/slash.js.map +1 -0
- package/dist/core/transaction.js +311 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/define-block.js +177 -0
- package/dist/define-block.js.map +1 -0
- package/dist/document.js +157 -0
- package/dist/document.js.map +1 -0
- package/dist/dom.js +48 -0
- package/dist/dom.js.map +1 -0
- package/dist/editor.js +33 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js +564 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/editor2.js +6 -0
- package/dist/editor2.js.map +1 -0
- package/dist/extensions/align/align-menu.js +6 -0
- package/dist/extensions/align/align-menu.js.map +1 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js +82 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/align-sub.js +6 -0
- package/dist/extensions/align/align-sub.js.map +1 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js +57 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/commands.js +40 -0
- package/dist/extensions/align/commands.js.map +1 -0
- package/dist/extensions/align/index.js +27 -0
- package/dist/extensions/align/index.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.js +6 -0
- package/dist/extensions/attachment/attachment-block.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js +119 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/attachment/files.js +66 -0
- package/dist/extensions/attachment/files.js.map +1 -0
- package/dist/extensions/attachment/index.js +132 -0
- package/dist/extensions/attachment/index.js.map +1 -0
- package/dist/extensions/blocks.js +170 -0
- package/dist/extensions/blocks.js.map +1 -0
- package/dist/extensions/callout/callout-block.js +6 -0
- package/dist/extensions/callout/callout-block.js.map +1 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js +71 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/callout/html.js +61 -0
- package/dist/extensions/callout/html.js.map +1 -0
- package/dist/extensions/callout/index.js +52 -0
- package/dist/extensions/callout/index.js.map +1 -0
- package/dist/extensions/callout/tones.js +12 -0
- package/dist/extensions/callout/tones.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.js +6 -0
- package/dist/extensions/code-block/code-block-view.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js +87 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/code-block/index.js +98 -0
- package/dist/extensions/code-block/index.js.map +1 -0
- package/dist/extensions/code-block/languages.js +116 -0
- package/dist/extensions/code-block/languages.js.map +1 -0
- package/dist/extensions/colors/color-menu.js +6 -0
- package/dist/extensions/colors/color-menu.js.map +1 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/colors/index.js +49 -0
- package/dist/extensions/colors/index.js.map +1 -0
- package/dist/extensions/colors/palette.js +26 -0
- package/dist/extensions/colors/palette.js.map +1 -0
- package/dist/extensions/editor-kit.js +35 -0
- package/dist/extensions/editor-kit.js.map +1 -0
- package/dist/extensions/embed/embed-block.js +6 -0
- package/dist/extensions/embed/embed-block.js.map +1 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/embed/index.js +118 -0
- package/dist/extensions/embed/index.js.map +1 -0
- package/dist/extensions/embed/targets.js +79 -0
- package/dist/extensions/embed/targets.js.map +1 -0
- package/dist/extensions/formatting.js +153 -0
- package/dist/extensions/formatting.js.map +1 -0
- package/dist/extensions/image/image-block.js +6 -0
- package/dist/extensions/image/image-block.js.map +1 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js +206 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/image/index.js +125 -0
- package/dist/extensions/image/index.js.map +1 -0
- package/dist/extensions/image/uploads.js +96 -0
- package/dist/extensions/image/uploads.js.map +1 -0
- package/dist/extensions/list-html.js +58 -0
- package/dist/extensions/list-html.js.map +1 -0
- package/dist/extensions/lists.js +182 -0
- package/dist/extensions/lists.js.map +1 -0
- package/dist/extensions/mention/index.js +56 -0
- package/dist/extensions/mention/index.js.map +1 -0
- package/dist/extensions/mention/mention-menu.js +6 -0
- package/dist/extensions/mention/mention-menu.js.map +1 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js +132 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/mention/state.js +113 -0
- package/dist/extensions/mention/state.js.map +1 -0
- package/dist/extensions/table/commands.js +356 -0
- package/dist/extensions/table/commands.js.map +1 -0
- package/dist/extensions/table/html.js +134 -0
- package/dist/extensions/table/html.js.map +1 -0
- package/dist/extensions/table/index.js +56 -0
- package/dist/extensions/table/index.js.map +1 -0
- package/dist/extensions/table/plugin.js +81 -0
- package/dist/extensions/table/plugin.js.map +1 -0
- package/dist/extensions/table/schema.js +258 -0
- package/dist/extensions/table/schema.js.map +1 -0
- package/dist/extensions/table/table-controls.js +6 -0
- package/dist/extensions/table/table-controls.js.map +1 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js +698 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/table/view.js +19 -0
- package/dist/extensions/table/view.js.map +1 -0
- package/dist/extensions/toggle/index.js +87 -0
- package/dist/extensions/toggle/index.js.map +1 -0
- package/dist/extensions/toggle/state.js +120 -0
- package/dist/extensions/toggle/state.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.js +6 -0
- package/dist/extensions/toggle/toggle-block.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +522 -0
- package/dist/locale.js.map +1 -0
- package/dist/markdown/index.js +24 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +8 -0
- package/dist/types/catalog.d.ts +8 -0
- package/dist/types/code/code-editor-block.vue.d.ts +17 -0
- package/dist/types/code/index.d.ts +18 -0
- package/dist/types/code/languages.d.ts +7 -0
- package/dist/types/code/types.d.ts +37 -0
- package/dist/types/code/view.d.ts +12 -0
- package/dist/types/collaboration/binding.d.ts +32 -0
- package/dist/types/collaboration/cursors.d.ts +26 -0
- package/dist/types/collaboration/index.d.ts +47 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/collaboration/table.d.ts +25 -0
- package/dist/types/collaboration/text.d.ts +9 -0
- package/dist/types/components/block-handle.vue.d.ts +3 -0
- package/dist/types/components/bubble-toolbar.vue.d.ts +7 -0
- package/dist/types/components/comment-button.vue.d.ts +3 -0
- package/dist/types/components/comment-card.vue.d.ts +3 -0
- package/dist/types/components/menu-escape.d.ts +7 -0
- package/dist/types/components/search-bar.vue.d.ts +22 -0
- package/dist/types/components/slash-menu.vue.d.ts +11 -0
- package/dist/types/core/align.d.ts +16 -0
- package/dist/types/core/block-selection.d.ts +22 -0
- package/dist/types/core/blocks.d.ts +52 -0
- package/dist/types/core/changes.d.ts +14 -0
- package/dist/types/core/colors.d.ts +4 -0
- package/dist/types/core/commands.d.ts +61 -0
- package/dist/types/core/comments.d.ts +44 -0
- package/dist/types/core/compare.d.ts +10 -0
- package/dist/types/core/convert.d.ts +12 -0
- package/dist/types/core/diff.d.ts +49 -0
- package/dist/types/core/editor.d.ts +64 -0
- package/dist/types/core/extension.d.ts +167 -0
- package/dist/types/core/file-drop.d.ts +19 -0
- package/dist/types/core/highlight.d.ts +32 -0
- package/dist/types/core/input-rules.d.ts +21 -0
- package/dist/types/core/keymap.d.ts +8 -0
- package/dist/types/core/model.d.ts +158 -0
- package/dist/types/core/output.d.ts +64 -0
- package/dist/types/core/plugins.d.ts +24 -0
- package/dist/types/core/prepare.d.ts +23 -0
- package/dist/types/core/schema.d.ts +38 -0
- package/dist/types/core/search.d.ts +83 -0
- package/dist/types/core/selection.d.ts +8 -0
- package/dist/types/core/slash.d.ts +21 -0
- package/dist/types/core/transaction.d.ts +97 -0
- package/dist/types/define-block.d.ts +66 -0
- package/dist/types/document.d.ts +46 -0
- package/dist/types/dom.d.ts +17 -0
- package/dist/types/editor.d.ts +252 -0
- package/dist/types/editor.vue.d.ts +210 -0
- package/dist/types/extensions/align/align-menu.vue.d.ts +3 -0
- package/dist/types/extensions/align/align-sub.vue.d.ts +7 -0
- package/dist/types/extensions/align/commands.d.ts +13 -0
- package/dist/types/extensions/align/index.d.ts +7 -0
- package/dist/types/extensions/attachment/attachment-block.vue.d.ts +17 -0
- package/dist/types/extensions/attachment/files.d.ts +6 -0
- package/dist/types/extensions/attachment/index.d.ts +20 -0
- package/dist/types/extensions/blocks.d.ts +8 -0
- package/dist/types/extensions/callout/callout-block.vue.d.ts +14 -0
- package/dist/types/extensions/callout/html.d.ts +4 -0
- package/dist/types/extensions/callout/index.d.ts +3 -0
- package/dist/types/extensions/callout/tones.d.ts +8 -0
- package/dist/types/extensions/code-block/code-block-view.vue.d.ts +16 -0
- package/dist/types/extensions/code-block/index.d.ts +19 -0
- package/dist/types/extensions/code-block/languages.d.ts +9 -0
- package/dist/types/extensions/colors/color-menu.vue.d.ts +3 -0
- package/dist/types/extensions/colors/index.d.ts +7 -0
- package/dist/types/extensions/colors/palette.d.ts +7 -0
- package/dist/types/extensions/editor-kit.d.ts +37 -0
- package/dist/types/extensions/embed/embed-block.vue.d.ts +16 -0
- package/dist/types/extensions/embed/index.d.ts +18 -0
- package/dist/types/extensions/embed/targets.d.ts +32 -0
- package/dist/types/extensions/formatting.d.ts +8 -0
- package/dist/types/extensions/image/image-block.vue.d.ts +16 -0
- package/dist/types/extensions/image/index.d.ts +16 -0
- package/dist/types/extensions/image/uploads.d.ts +35 -0
- package/dist/types/extensions/list-html.d.ts +11 -0
- package/dist/types/extensions/lists.d.ts +6 -0
- package/dist/types/extensions/mention/index.d.ts +11 -0
- package/dist/types/extensions/mention/mention-menu.vue.d.ts +7 -0
- package/dist/types/extensions/mention/state.d.ts +28 -0
- package/dist/types/extensions/table/commands.d.ts +69 -0
- package/dist/types/extensions/table/html.d.ts +4 -0
- package/dist/types/extensions/table/index.d.ts +8 -0
- package/dist/types/extensions/table/plugin.d.ts +4 -0
- package/dist/types/extensions/table/schema.d.ts +23 -0
- package/dist/types/extensions/table/table-controls.vue.d.ts +3 -0
- package/dist/types/extensions/table/view.d.ts +13 -0
- package/dist/types/extensions/toggle/index.d.ts +7 -0
- package/dist/types/extensions/toggle/state.d.ts +17 -0
- package/dist/types/extensions/toggle/toggle-block.vue.d.ts +14 -0
- package/dist/types/index.d.ts +266 -0
- package/dist/types/locale.d.ts +195 -0
- package/dist/types/markdown/index.d.ts +14 -0
- package/dist/types/use-comments.d.ts +17 -0
- package/dist/use-comments.js +141 -0
- package/dist/use-comments.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { createContentNode } from "./model.js";
|
|
2
|
+
import { blockAround, blocksIn, infoAt, nextInDocument, previousInDocument, selectedRange } from "./blocks.js";
|
|
3
|
+
import { NodeSelection, Selection, TextSelection } from "prosemirror-state";
|
|
4
|
+
import { Fragment } from "prosemirror-model";
|
|
5
|
+
// src/core/commands.ts
|
|
6
|
+
var newBlock = (schema, content, children) => schema.nodes.block.create({ id: null }, children?.size ? [content, schema.nodes.blockGroup.create(null, children)] : content);
|
|
7
|
+
/** The block and content around the cursor, when the selection is inside one textblock. */
|
|
8
|
+
function cursorBlock(state) {
|
|
9
|
+
const { $from, $to } = state.selection;
|
|
10
|
+
if (!$from.parent.isTextblock || !$from.sameParent($to)) return null;
|
|
11
|
+
const info = blockAround($from);
|
|
12
|
+
return info && info.content === $from.parent ? info : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Turn every block the selection touches into `type`. Text formatting is
|
|
16
|
+
* dropped when it can't be kept (in a code block); a type without text
|
|
17
|
+
* (a divider, an embed) replaces the content and adds an empty paragraph
|
|
18
|
+
* after it to keep typing in.
|
|
19
|
+
*/
|
|
20
|
+
function setBlockType(type, props = {}) {
|
|
21
|
+
return (state, dispatch) => {
|
|
22
|
+
const range = selectedRange(state);
|
|
23
|
+
if (!range) return false;
|
|
24
|
+
const schema = state.schema;
|
|
25
|
+
const nodeType = schema.nodes[type];
|
|
26
|
+
if (!nodeType) return false;
|
|
27
|
+
if (!dispatch) return true;
|
|
28
|
+
const tr = state.tr;
|
|
29
|
+
const created = createContentNode(schema, type, props);
|
|
30
|
+
const attrs = created.attrs;
|
|
31
|
+
if (!nodeType.isTextblock) {
|
|
32
|
+
const blocks = blocksIn(range);
|
|
33
|
+
const last = blocks.at(-1);
|
|
34
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
35
|
+
const block = blocks[index];
|
|
36
|
+
tr.replaceWith(block.contentPos, block.contentPos + block.content.nodeSize, created);
|
|
37
|
+
}
|
|
38
|
+
const after = tr.mapping.map(last.pos + last.node.nodeSize);
|
|
39
|
+
tr.insert(after, newBlock(schema, schema.nodes.paragraph.create()));
|
|
40
|
+
tr.setSelection(TextSelection.create(tr.doc, after + 2));
|
|
41
|
+
dispatch(tr.scrollIntoView());
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
for (const block of blocksIn(range)) {
|
|
45
|
+
const from = tr.mapping.map(block.contentPos);
|
|
46
|
+
const content = tr.doc.nodeAt(from);
|
|
47
|
+
if (!content.isAtom && !content.inlineContent) continue;
|
|
48
|
+
if (content.isAtom) tr.replaceWith(from, from + content.nodeSize, nodeType.create(attrs));
|
|
49
|
+
else {
|
|
50
|
+
const align = content.attrs.align;
|
|
51
|
+
const kept = align != null && nodeType.spec.attrs?.align ? {
|
|
52
|
+
...attrs,
|
|
53
|
+
align
|
|
54
|
+
} : attrs;
|
|
55
|
+
tr.setBlockType(from + 1, from + content.nodeSize - 1, nodeType, kept);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const { from, to } = state.selection;
|
|
59
|
+
if (state.selection instanceof TextSelection) tr.setSelection(TextSelection.create(tr.doc, tr.mapping.map(from), tr.mapping.map(to)));
|
|
60
|
+
dispatch(tr.scrollIntoView());
|
|
61
|
+
return true;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Split the block at the cursor. An empty list item turns back into a
|
|
66
|
+
* paragraph, an empty nested paragraph moves up a level, and a block with
|
|
67
|
+
* children gets its new block as the first child.
|
|
68
|
+
*/
|
|
69
|
+
var splitBlock = (state, dispatch) => {
|
|
70
|
+
const schema = state.schema;
|
|
71
|
+
let tr = state.tr;
|
|
72
|
+
if (!state.selection.empty) {
|
|
73
|
+
if (!cursorBlockAfterDelete(state)) return false;
|
|
74
|
+
tr = tr.deleteSelection();
|
|
75
|
+
}
|
|
76
|
+
const $from = tr.selection.$from;
|
|
77
|
+
const info = blockAround($from);
|
|
78
|
+
if (!info || info.content !== $from.parent || !$from.parent.isTextblock) return false;
|
|
79
|
+
const content = info.content;
|
|
80
|
+
const type = content.type.name;
|
|
81
|
+
if (content.content.size === 0) {
|
|
82
|
+
if (type !== "paragraph" && !content.type.spec.code) {
|
|
83
|
+
if (dispatch) dispatch(tr.setBlockType($from.pos, $from.pos, schema.nodes.paragraph).scrollIntoView());
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (type === "paragraph" && info.depth > 1) return liftBlocks(state, dispatch);
|
|
87
|
+
}
|
|
88
|
+
if (!dispatch) return true;
|
|
89
|
+
const offset = $from.parentOffset;
|
|
90
|
+
const nextType = content.type.spec.continues ? type : "paragraph";
|
|
91
|
+
const nextAttrs = null;
|
|
92
|
+
const contentEnd = info.contentPos + content.nodeSize - 1;
|
|
93
|
+
if (offset === 0 && content.content.size > 0) {
|
|
94
|
+
const above = newBlock(schema, schema.nodes[nextType].create(nextAttrs));
|
|
95
|
+
tr.insert(info.pos, above);
|
|
96
|
+
dispatch(tr.scrollIntoView());
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const rest = content.content.cut(offset);
|
|
100
|
+
const moved = schema.nodes[nextType].create(nextAttrs, rest);
|
|
101
|
+
tr.delete($from.pos, contentEnd);
|
|
102
|
+
const block = newBlock(schema, moved);
|
|
103
|
+
let insertAt;
|
|
104
|
+
if (info.group) insertAt = tr.mapping.map(info.contentPos + content.nodeSize) + 1;
|
|
105
|
+
else insertAt = tr.mapping.map(info.pos + info.node.nodeSize);
|
|
106
|
+
tr.insert(insertAt, block);
|
|
107
|
+
tr.setSelection(TextSelection.create(tr.doc, insertAt + 2));
|
|
108
|
+
dispatch(tr.scrollIntoView());
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
function cursorBlockAfterDelete(state) {
|
|
112
|
+
return !!blockAround(state.selection.$from);
|
|
113
|
+
}
|
|
114
|
+
/** `Shift+Enter`: a line break, or a newline in code. */
|
|
115
|
+
var insertBreak = (state, dispatch) => {
|
|
116
|
+
const { $from } = state.selection;
|
|
117
|
+
if (!$from.parent.isTextblock) return false;
|
|
118
|
+
if (dispatch) dispatch(($from.parent.type.spec.code ? state.tr.insertText("\n") : state.tr.replaceSelectionWith(state.schema.nodes.hardBreak.create())).scrollIntoView());
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
/** In a code block, `Enter` adds a newline. */
|
|
122
|
+
var newlineInCode = (state, dispatch) => {
|
|
123
|
+
const { $from } = state.selection;
|
|
124
|
+
if (!$from.parent.type.spec.code) return false;
|
|
125
|
+
if (dispatch) dispatch(state.tr.insertText("\n").scrollIntoView());
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
/** `Mod+Enter` in a code block: leave it for a new paragraph below. */
|
|
129
|
+
var exitCode = (state, dispatch) => {
|
|
130
|
+
const info = cursorBlock(state);
|
|
131
|
+
if (!info || !info.content.type.spec.code) return false;
|
|
132
|
+
if (dispatch) {
|
|
133
|
+
const at = info.pos + info.node.nodeSize;
|
|
134
|
+
const tr = state.tr.insert(at, newBlock(state.schema, state.schema.nodes.paragraph.create()));
|
|
135
|
+
tr.setSelection(TextSelection.create(tr.doc, at + 2));
|
|
136
|
+
dispatch(tr.scrollIntoView());
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Move `source`'s text to the end of `target`'s content and put its
|
|
142
|
+
* children where `source` was.
|
|
143
|
+
*/
|
|
144
|
+
function mergeInto(tr, target, source) {
|
|
145
|
+
const joinAt = target.contentPos + target.content.nodeSize - 1;
|
|
146
|
+
const children = source.group ? source.group.content : Fragment.empty;
|
|
147
|
+
tr.replaceWith(source.pos, source.pos + source.node.nodeSize, children);
|
|
148
|
+
let inline = source.content.content;
|
|
149
|
+
if (target.content.type.spec.code) {
|
|
150
|
+
const text = source.content.textContent;
|
|
151
|
+
inline = text ? Fragment.from(tr.doc.type.schema.text(text)) : Fragment.empty;
|
|
152
|
+
}
|
|
153
|
+
tr.insert(joinAt, inline);
|
|
154
|
+
tr.setSelection(TextSelection.create(tr.doc, joinAt));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `Backspace` at the start of a block: first turn it into a paragraph, then
|
|
158
|
+
* move a nested block up a level, then join it with the block above.
|
|
159
|
+
*/
|
|
160
|
+
var joinBackward = (state, dispatch) => {
|
|
161
|
+
const info = cursorBlock(state);
|
|
162
|
+
if (!info || !state.selection.empty || state.selection.$from.parentOffset > 0) return false;
|
|
163
|
+
if (info.content.type.name !== "paragraph") {
|
|
164
|
+
if (dispatch) {
|
|
165
|
+
const pos = state.selection.from;
|
|
166
|
+
dispatch(state.tr.setBlockType(pos, pos, state.schema.nodes.paragraph));
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (info.depth > 1) return liftBlocks(state, dispatch);
|
|
171
|
+
const previous = previousInDocument(state.doc, info);
|
|
172
|
+
if (!previous) return false;
|
|
173
|
+
if (!dispatch) return true;
|
|
174
|
+
const tr = state.tr;
|
|
175
|
+
if (previous.content.isAtom) {
|
|
176
|
+
tr.delete(previous.pos, previous.pos + previous.node.nodeSize);
|
|
177
|
+
dispatch(tr.scrollIntoView());
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
mergeInto(tr, previous, info);
|
|
181
|
+
dispatch(tr.scrollIntoView());
|
|
182
|
+
return true;
|
|
183
|
+
};
|
|
184
|
+
/** `Delete` at the end of a block: pull the next block's text up. */
|
|
185
|
+
var joinForward = (state, dispatch) => {
|
|
186
|
+
const info = cursorBlock(state);
|
|
187
|
+
const { $from } = state.selection;
|
|
188
|
+
if (!info || !state.selection.empty || $from.parentOffset < $from.parent.content.size) return false;
|
|
189
|
+
const next = nextInDocument(state.doc, info);
|
|
190
|
+
if (!next) return false;
|
|
191
|
+
if (!dispatch) return true;
|
|
192
|
+
const tr = state.tr;
|
|
193
|
+
if (next.content.isAtom && !next.group) tr.delete(next.pos, next.pos + next.node.nodeSize);
|
|
194
|
+
else mergeInto(tr, info, next);
|
|
195
|
+
dispatch(tr.scrollIntoView());
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
/** With a divider selected, `Backspace` or `Delete` removes its block. */
|
|
199
|
+
var deleteSelectedAtom = (state, dispatch) => {
|
|
200
|
+
const selection = state.selection;
|
|
201
|
+
if (!(selection instanceof NodeSelection) || !selection.node.isAtom) return false;
|
|
202
|
+
const info = blockAround(selection.$from);
|
|
203
|
+
if (!info) return false;
|
|
204
|
+
if (dispatch) dispatch(deleteBlocks(state.tr, [info]).scrollIntoView());
|
|
205
|
+
return true;
|
|
206
|
+
};
|
|
207
|
+
/** `Tab`: make the selected blocks children of the block above them. */
|
|
208
|
+
var sinkBlocks = (state, dispatch) => {
|
|
209
|
+
const range = selectedRange(state);
|
|
210
|
+
if (!range || range.from === 0) return false;
|
|
211
|
+
if (!dispatch) return true;
|
|
212
|
+
const schema = state.schema;
|
|
213
|
+
const blocks = blocksIn(range);
|
|
214
|
+
const previousNode = range.parent.child(range.from - 1);
|
|
215
|
+
const previous = infoAt(previousNode, range.start - previousNode.nodeSize, range.depth);
|
|
216
|
+
const moving = Fragment.fromArray(blocks.map((block) => block.node));
|
|
217
|
+
const tr = state.tr;
|
|
218
|
+
const { from, to } = state.selection;
|
|
219
|
+
tr.delete(range.start, range.end);
|
|
220
|
+
let shift;
|
|
221
|
+
if (previous.group) {
|
|
222
|
+
const at = previous.pos + previous.node.nodeSize - 2;
|
|
223
|
+
tr.insert(at, moving);
|
|
224
|
+
shift = at - range.start;
|
|
225
|
+
} else {
|
|
226
|
+
const at = previous.pos + previous.node.nodeSize - 1;
|
|
227
|
+
tr.insert(at, schema.nodes.blockGroup.create(null, moving));
|
|
228
|
+
shift = at + 1 - range.start;
|
|
229
|
+
}
|
|
230
|
+
restoreSelection(tr, state, from + shift, to + shift);
|
|
231
|
+
dispatch(tr.scrollIntoView());
|
|
232
|
+
return true;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* `Shift+Tab`: move the selected blocks up a level, right after their
|
|
236
|
+
* parent. The siblings that followed them become children of the last one,
|
|
237
|
+
* so nothing changes order.
|
|
238
|
+
*/
|
|
239
|
+
var liftBlocks = (state, dispatch) => {
|
|
240
|
+
const range = selectedRange(state);
|
|
241
|
+
if (!range || range.depth <= 1) return false;
|
|
242
|
+
if (!dispatch) return true;
|
|
243
|
+
const schema = state.schema;
|
|
244
|
+
const $start = state.doc.resolve(range.start);
|
|
245
|
+
const parentDepth = range.depth - 2;
|
|
246
|
+
const parent = infoAt($start.node(parentDepth), $start.before(parentDepth), parentDepth);
|
|
247
|
+
const group = range.parent;
|
|
248
|
+
const blocks = blocksIn(range).map((block) => block.node);
|
|
249
|
+
const following = [];
|
|
250
|
+
for (let index = range.to + 1; index < group.childCount; index++) following.push(group.child(index));
|
|
251
|
+
if (following.length) {
|
|
252
|
+
const last = blocks.pop();
|
|
253
|
+
const children = (last.childCount > 1 ? last.child(1).content : Fragment.empty).append(Fragment.fromArray(following));
|
|
254
|
+
blocks.push(last.copy(Fragment.from(last.firstChild).addToEnd(schema.nodes.blockGroup.create(null, children))));
|
|
255
|
+
}
|
|
256
|
+
const kept = [];
|
|
257
|
+
for (let index = 0; index < range.from; index++) kept.push(group.child(index));
|
|
258
|
+
const parentContent = parent.node.firstChild;
|
|
259
|
+
const newParent = parent.node.copy(kept.length ? Fragment.from(parentContent).addToEnd(group.copy(Fragment.fromArray(kept))) : Fragment.from(parentContent));
|
|
260
|
+
const tr = state.tr;
|
|
261
|
+
const { from, to } = state.selection;
|
|
262
|
+
tr.replaceWith(parent.pos, parent.pos + parent.node.nodeSize, [newParent, ...blocks]);
|
|
263
|
+
const shift = parent.pos + newParent.nodeSize - range.start;
|
|
264
|
+
restoreSelection(tr, state, from + shift, to + shift);
|
|
265
|
+
dispatch(tr.scrollIntoView());
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
function restoreSelection(tr, state, from, to) {
|
|
269
|
+
const clamp = (pos) => Math.max(0, Math.min(pos, tr.doc.content.size));
|
|
270
|
+
if (state.selection instanceof NodeSelection) {
|
|
271
|
+
const $pos = tr.doc.resolve(clamp(from));
|
|
272
|
+
if ($pos.nodeAfter) {
|
|
273
|
+
tr.setSelection(NodeSelection.create(tr.doc, $pos.pos));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
tr.setSelection(TextSelection.between(tr.doc.resolve(clamp(from)), tr.doc.resolve(clamp(to))));
|
|
278
|
+
}
|
|
279
|
+
/** `Mod+Shift+↑/↓`: swap the selected blocks with their neighbour. */
|
|
280
|
+
function swapBlocks(direction) {
|
|
281
|
+
return (state, dispatch) => {
|
|
282
|
+
const range = selectedRange(state);
|
|
283
|
+
if (!range) return false;
|
|
284
|
+
const neighbourIndex = direction < 0 ? range.from - 1 : range.to + 1;
|
|
285
|
+
if (neighbourIndex < 0 || neighbourIndex >= range.parent.childCount) return false;
|
|
286
|
+
if (!dispatch) return true;
|
|
287
|
+
const neighbour = range.parent.child(neighbourIndex);
|
|
288
|
+
const tr = state.tr;
|
|
289
|
+
const { from, to } = state.selection;
|
|
290
|
+
let shift;
|
|
291
|
+
if (direction < 0) {
|
|
292
|
+
const at = range.start - neighbour.nodeSize;
|
|
293
|
+
tr.delete(at, range.start);
|
|
294
|
+
tr.insert(range.end - neighbour.nodeSize, neighbour);
|
|
295
|
+
shift = -neighbour.nodeSize;
|
|
296
|
+
} else {
|
|
297
|
+
tr.delete(range.end, range.end + neighbour.nodeSize);
|
|
298
|
+
tr.insert(range.start, neighbour);
|
|
299
|
+
shift = neighbour.nodeSize;
|
|
300
|
+
}
|
|
301
|
+
restoreSelection(tr, state, from + shift, to + shift);
|
|
302
|
+
dispatch(tr.scrollIntoView());
|
|
303
|
+
return true;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Drag and drop: move the blocks of `range` to `target`, a position right
|
|
308
|
+
* before or after a block, possibly at another level. Fails when the
|
|
309
|
+
* target is inside the moved blocks. The cursor goes to the first one.
|
|
310
|
+
*/
|
|
311
|
+
function moveRangeTo(range, target) {
|
|
312
|
+
return (state, dispatch) => {
|
|
313
|
+
if (target >= range.start && target <= range.end) return false;
|
|
314
|
+
if (dispatch) {
|
|
315
|
+
const nodes = blocksIn(range).map((block) => block.node);
|
|
316
|
+
const tr = state.tr;
|
|
317
|
+
if (range.parent.type.name === "blockGroup" && range.from === 0 && range.to === range.parent.childCount - 1) tr.delete(range.start - 1, range.end + 1);
|
|
318
|
+
else tr.delete(range.start, range.end);
|
|
319
|
+
const at = tr.mapping.map(target);
|
|
320
|
+
tr.insert(at, nodes);
|
|
321
|
+
tr.setSelection(Selection.near(tr.doc.resolve(at + 2)));
|
|
322
|
+
dispatch(tr.scrollIntoView());
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** Remove blocks, keeping at least one empty paragraph in the document. */
|
|
328
|
+
function deleteBlocks(tr, blocks) {
|
|
329
|
+
const sorted = [...blocks].sort((a, b) => b.pos - a.pos);
|
|
330
|
+
for (const block of sorted) {
|
|
331
|
+
const from = tr.mapping.map(block.pos);
|
|
332
|
+
const to = tr.mapping.map(block.pos + block.node.nodeSize);
|
|
333
|
+
const $from = tr.doc.resolve(from);
|
|
334
|
+
if ($from.parent.type.name === "blockGroup" && $from.parent.childCount === 1) tr.delete($from.before(), $from.after());
|
|
335
|
+
else tr.delete(from, to);
|
|
336
|
+
}
|
|
337
|
+
if (tr.doc.childCount === 0 || tr.doc.content.size === 0) tr.insert(0, newBlock(tr.doc.type.schema, tr.doc.type.schema.nodes.paragraph.create()));
|
|
338
|
+
const near = Math.min(tr.mapping.map(sorted.at(-1).pos), tr.doc.content.size);
|
|
339
|
+
tr.setSelection(Selection.near(tr.doc.resolve(near)));
|
|
340
|
+
return tr;
|
|
341
|
+
}
|
|
342
|
+
/** Copies of blocks with fresh ids (the id plugin assigns them). */
|
|
343
|
+
function withoutIds(node) {
|
|
344
|
+
if (node.type.name !== "block" && node.type.name !== "blockGroup") return node;
|
|
345
|
+
const children = [];
|
|
346
|
+
node.forEach((child) => children.push(withoutIds(child)));
|
|
347
|
+
return node.type.name === "block" ? node.type.create({
|
|
348
|
+
...node.attrs,
|
|
349
|
+
id: null
|
|
350
|
+
}, children) : node.type.create(node.attrs, children);
|
|
351
|
+
}
|
|
352
|
+
/** Insert copies of the selected blocks right after them. */
|
|
353
|
+
var duplicateBlocks = (state, dispatch) => {
|
|
354
|
+
const range = selectedRange(state);
|
|
355
|
+
if (!range) return false;
|
|
356
|
+
if (dispatch) {
|
|
357
|
+
const copies = blocksIn(range).map((block) => withoutIds(block.node));
|
|
358
|
+
dispatch(state.tr.insert(range.end, copies).scrollIntoView());
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
/** Insert an empty block of `type` after the block at `pos` and put the cursor in it. */
|
|
363
|
+
function insertBlockAfter(pos, type = "paragraph") {
|
|
364
|
+
return (state, dispatch) => {
|
|
365
|
+
const info = blockAround(state.doc.resolve(Math.min(pos + 1, state.doc.content.size)));
|
|
366
|
+
if (!info) return false;
|
|
367
|
+
if (dispatch) {
|
|
368
|
+
const at = info.pos + info.node.nodeSize;
|
|
369
|
+
const content = createContentNode(state.schema, type);
|
|
370
|
+
const tr = state.tr.insert(at, newBlock(state.schema, content));
|
|
371
|
+
tr.setSelection(Selection.near(tr.doc.resolve(at + 2)));
|
|
372
|
+
dispatch(tr.scrollIntoView());
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export { deleteBlocks, deleteSelectedAtom, duplicateBlocks, exitCode, insertBlockAfter, insertBreak, joinBackward, joinForward, liftBlocks, moveRangeTo, newlineInCode, setBlockType, sinkBlocks, splitBlock, swapBlocks, withoutIds };
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","names":[],"sources":["../../src/core/commands.ts"],"sourcesContent":["import { Fragment, type Node as PMNode, type Schema } from 'prosemirror-model'\nimport {\n NodeSelection,\n Selection,\n TextSelection,\n type Command,\n type EditorState,\n type Transaction,\n} from 'prosemirror-state'\nimport {\n blockAround,\n blocksIn,\n infoAt,\n nextInDocument,\n previousInDocument,\n selectedRange,\n type BlockInfo,\n type SiblingRange,\n} from './blocks'\nimport { createContentNode, type BlockProps } from './model'\nimport type { ContentType } from './schema'\n\nconst newBlock = (schema: Schema, content: PMNode, children?: Fragment) =>\n schema.nodes.block!.create(\n { id: null },\n children?.size ? [content, schema.nodes.blockGroup!.create(null, children)] : content,\n )\n\n/** The block and content around the cursor, when the selection is inside one textblock. */\nfunction cursorBlock(state: EditorState) {\n const { $from, $to } = state.selection\n if (!$from.parent.isTextblock || !$from.sameParent($to)) return null\n const info = blockAround($from)\n return info && info.content === $from.parent ? info : null\n}\n\n/* turning blocks into another type */\n\n/**\n * Turn every block the selection touches into `type`. Text formatting is\n * dropped when it can't be kept (in a code block); a type without text\n * (a divider, an embed) replaces the content and adds an empty paragraph\n * after it to keep typing in.\n */\nexport function setBlockType(type: ContentType, props: BlockProps = {}): Command {\n return (state, dispatch) => {\n const range = selectedRange(state)\n if (!range) return false\n const schema = state.schema\n const nodeType = schema.nodes[type]\n if (!nodeType) return false\n if (!dispatch) return true\n const tr = state.tr\n const created = createContentNode(schema, type, props)\n const attrs = created.attrs\n if (!nodeType.isTextblock) {\n const blocks = blocksIn(range)\n const last = blocks.at(-1)!\n // last first, so earlier positions stay valid\n for (let index = blocks.length - 1; index >= 0; index--) {\n const block = blocks[index]!\n tr.replaceWith(block.contentPos, block.contentPos + block.content.nodeSize, created)\n }\n const after = tr.mapping.map(last.pos + last.node.nodeSize)\n tr.insert(after, newBlock(schema, schema.nodes.paragraph!.create()))\n tr.setSelection(TextSelection.create(tr.doc, after + 2))\n dispatch(tr.scrollIntoView())\n return true\n }\n for (const block of blocksIn(range)) {\n const from = tr.mapping.map(block.contentPos)\n const content = tr.doc.nodeAt(from)!\n // a block holding other nodes (a table) can't become text\n if (!content.isAtom && !content.inlineContent) continue\n if (content.isAtom) {\n tr.replaceWith(from, from + content.nodeSize, nodeType.create(attrs))\n } else {\n // only this content node: the block's children keep their type; alignment stays\n const align = content.attrs.align\n const kept = align != null && nodeType.spec.attrs?.align ? { ...attrs, align } : attrs\n tr.setBlockType(from + 1, from + content.nodeSize - 1, nodeType, kept)\n }\n }\n const { from, to } = state.selection\n if (state.selection instanceof TextSelection)\n tr.setSelection(TextSelection.create(tr.doc, tr.mapping.map(from), tr.mapping.map(to)))\n dispatch(tr.scrollIntoView())\n return true\n }\n}\n\n/** Set attributes on the content of the block at `pos`, e.g. tick a to-do. */\nexport function setContentAttrs(pos: number, attrs: Record<string, unknown>): Command {\n return (state, dispatch) => {\n const info = blockAround(state.doc.resolve(pos + 1))\n if (!info) return false\n if (dispatch) {\n const tr = state.tr.setNodeMarkup(info.contentPos, undefined, {\n ...info.content.attrs,\n ...attrs,\n })\n dispatch(tr)\n }\n return true\n }\n}\n\n/* Enter */\n\n/**\n * Split the block at the cursor. An empty list item turns back into a\n * paragraph, an empty nested paragraph moves up a level, and a block with\n * children gets its new block as the first child.\n */\nexport const splitBlock: Command = (state, dispatch) => {\n const schema = state.schema\n let tr = state.tr\n if (!state.selection.empty) {\n if (!cursorBlockAfterDelete(state)) return false\n tr = tr.deleteSelection()\n }\n const $from = tr.selection.$from\n const info = blockAround($from)\n if (!info || info.content !== $from.parent || !$from.parent.isTextblock) return false\n const content = info.content\n const type = content.type.name\n\n if (content.content.size === 0) {\n if (type !== 'paragraph' && !content.type.spec.code) {\n if (dispatch)\n dispatch(tr.setBlockType($from.pos, $from.pos, schema.nodes.paragraph!).scrollIntoView())\n return true\n }\n if (type === 'paragraph' && info.depth > 1) return liftBlocks(state, dispatch)\n }\n\n if (!dispatch) return true\n const offset = $from.parentOffset\n // lists continue themselves (with their default attributes: a to-do starts unticked)\n const nextType = content.type.spec.continues ? type : 'paragraph'\n const nextAttrs = null\n const contentEnd = info.contentPos + content.nodeSize - 1\n\n // at the very start of a non-empty block: open an empty block above it\n if (offset === 0 && content.content.size > 0) {\n const above = newBlock(schema, schema.nodes[nextType]!.create(nextAttrs))\n tr.insert(info.pos, above)\n dispatch(tr.scrollIntoView())\n return true\n }\n\n const rest = content.content.cut(offset)\n const moved = schema.nodes[nextType]!.create(nextAttrs, rest)\n tr.delete($from.pos, contentEnd)\n const block = newBlock(schema, moved)\n let insertAt: number\n if (info.group) {\n // first child: right below the line the cursor was on\n insertAt = tr.mapping.map(info.contentPos + content.nodeSize) + 1\n } else {\n insertAt = tr.mapping.map(info.pos + info.node.nodeSize)\n }\n tr.insert(insertAt, block)\n tr.setSelection(TextSelection.create(tr.doc, insertAt + 2))\n dispatch(tr.scrollIntoView())\n return true\n}\n\nfunction cursorBlockAfterDelete(state: EditorState) {\n return !!blockAround(state.selection.$from)\n}\n\n/** `Shift+Enter`: a line break, or a newline in code. */\nexport const insertBreak: Command = (state, dispatch) => {\n const { $from } = state.selection\n if (!$from.parent.isTextblock) return false\n if (dispatch) {\n const tr = $from.parent.type.spec.code\n ? state.tr.insertText('\\n')\n : state.tr.replaceSelectionWith(state.schema.nodes.hardBreak!.create())\n dispatch(tr.scrollIntoView())\n }\n return true\n}\n\n/** In a code block, `Enter` adds a newline. */\nexport const newlineInCode: Command = (state, dispatch) => {\n const { $from } = state.selection\n if (!$from.parent.type.spec.code) return false\n if (dispatch) dispatch(state.tr.insertText('\\n').scrollIntoView())\n return true\n}\n\n/** `Mod+Enter` in a code block: leave it for a new paragraph below. */\nexport const exitCode: Command = (state, dispatch) => {\n const info = cursorBlock(state)\n if (!info || !info.content.type.spec.code) return false\n if (dispatch) {\n const at = info.pos + info.node.nodeSize\n const tr = state.tr.insert(at, newBlock(state.schema, state.schema.nodes.paragraph!.create()))\n tr.setSelection(TextSelection.create(tr.doc, at + 2))\n dispatch(tr.scrollIntoView())\n }\n return true\n}\n\n/* Backspace and Delete */\n\n/**\n * Move `source`'s text to the end of `target`'s content and put its\n * children where `source` was.\n */\nfunction mergeInto(tr: Transaction, target: BlockInfo, source: BlockInfo) {\n const joinAt = target.contentPos + target.content.nodeSize - 1\n const children = source.group ? source.group.content : Fragment.empty\n tr.replaceWith(source.pos, source.pos + source.node.nodeSize, children)\n let inline = source.content.content\n if (target.content.type.spec.code) {\n const text = source.content.textContent\n inline = text ? Fragment.from(tr.doc.type.schema.text(text)) : Fragment.empty\n }\n tr.insert(joinAt, inline)\n tr.setSelection(TextSelection.create(tr.doc, joinAt))\n}\n\n/**\n * `Backspace` at the start of a block: first turn it into a paragraph, then\n * move a nested block up a level, then join it with the block above.\n */\nexport const joinBackward: Command = (state, dispatch) => {\n const info = cursorBlock(state)\n if (!info || !state.selection.empty || state.selection.$from.parentOffset > 0) return false\n const type = info.content.type.name\n if (type !== 'paragraph') {\n if (dispatch) {\n const pos = state.selection.from\n dispatch(state.tr.setBlockType(pos, pos, state.schema.nodes.paragraph!))\n }\n return true\n }\n if (info.depth > 1) return liftBlocks(state, dispatch)\n const previous = previousInDocument(state.doc, info)\n if (!previous) return false\n if (!dispatch) return true\n const tr = state.tr\n if (previous.content.isAtom) {\n // the block above is a divider: remove it\n tr.delete(previous.pos, previous.pos + previous.node.nodeSize)\n dispatch(tr.scrollIntoView())\n return true\n }\n mergeInto(tr, previous, info)\n dispatch(tr.scrollIntoView())\n return true\n}\n\n/** `Delete` at the end of a block: pull the next block's text up. */\nexport const joinForward: Command = (state, dispatch) => {\n const info = cursorBlock(state)\n const { $from } = state.selection\n if (!info || !state.selection.empty || $from.parentOffset < $from.parent.content.size)\n return false\n const next = nextInDocument(state.doc, info)\n if (!next) return false\n if (!dispatch) return true\n const tr = state.tr\n if (next.content.isAtom && !next.group) {\n tr.delete(next.pos, next.pos + next.node.nodeSize)\n } else {\n mergeInto(tr, info, next)\n }\n dispatch(tr.scrollIntoView())\n return true\n}\n\n/** With a divider selected, `Backspace` or `Delete` removes its block. */\nexport const deleteSelectedAtom: Command = (state, dispatch) => {\n const selection = state.selection\n if (!(selection instanceof NodeSelection) || !selection.node.isAtom) return false\n const info = blockAround(selection.$from)\n if (!info) return false\n if (dispatch) dispatch(deleteBlocks(state.tr, [info]).scrollIntoView())\n return true\n}\n\n/* indenting */\n\n/** `Tab`: make the selected blocks children of the block above them. */\nexport const sinkBlocks: Command = (state, dispatch) => {\n const range = selectedRange(state)\n if (!range || range.from === 0) return false\n if (!dispatch) return true\n const schema = state.schema\n const blocks = blocksIn(range)\n const previousNode = range.parent.child(range.from - 1)\n const previous = infoAt(previousNode, range.start - previousNode.nodeSize, range.depth)\n const moving = Fragment.fromArray(blocks.map((block) => block.node))\n const tr = state.tr\n const { from, to } = state.selection\n tr.delete(range.start, range.end)\n let shift: number\n if (previous.group) {\n const at = previous.pos + previous.node.nodeSize - 2\n tr.insert(at, moving)\n shift = at - range.start\n } else {\n const at = previous.pos + previous.node.nodeSize - 1\n tr.insert(at, schema.nodes.blockGroup!.create(null, moving))\n shift = at + 1 - range.start\n }\n restoreSelection(tr, state, from + shift, to + shift)\n dispatch(tr.scrollIntoView())\n return true\n}\n\n/**\n * `Shift+Tab`: move the selected blocks up a level, right after their\n * parent. The siblings that followed them become children of the last one,\n * so nothing changes order.\n */\nexport const liftBlocks: Command = (state, dispatch) => {\n const range = selectedRange(state)\n if (!range || range.depth <= 1) return false\n if (!dispatch) return true\n const schema = state.schema\n const doc = state.doc\n const $start = doc.resolve(range.start)\n const parentDepth = range.depth - 2\n const parent = infoAt($start.node(parentDepth), $start.before(parentDepth), parentDepth)\n const group = range.parent\n const blocks = blocksIn(range).map((block) => block.node)\n // siblings after the range go under the last moved block\n const following: PMNode[] = []\n for (let index = range.to + 1; index < group.childCount; index++)\n following.push(group.child(index))\n if (following.length) {\n const last = blocks.pop()!\n const ownGroup = last.childCount > 1 ? last.child(1).content : Fragment.empty\n const children = ownGroup.append(Fragment.fromArray(following))\n blocks.push(\n last.copy(\n Fragment.from(last.firstChild!).addToEnd(schema.nodes.blockGroup!.create(null, children)),\n ),\n )\n }\n const kept: PMNode[] = []\n for (let index = 0; index < range.from; index++) kept.push(group.child(index))\n const parentContent = parent.node.firstChild!\n const newParent = parent.node.copy(\n kept.length\n ? Fragment.from(parentContent).addToEnd(group.copy(Fragment.fromArray(kept)))\n : Fragment.from(parentContent),\n )\n const tr = state.tr\n const { from, to } = state.selection\n tr.replaceWith(parent.pos, parent.pos + parent.node.nodeSize, [newParent, ...blocks])\n // the moved blocks now start right after the rebuilt parent\n const shift = parent.pos + newParent.nodeSize - range.start\n restoreSelection(tr, state, from + shift, to + shift)\n dispatch(tr.scrollIntoView())\n return true\n}\n\nfunction restoreSelection(tr: Transaction, state: EditorState, from: number, to: number) {\n const clamp = (pos: number) => Math.max(0, Math.min(pos, tr.doc.content.size))\n if (state.selection instanceof NodeSelection) {\n const $pos = tr.doc.resolve(clamp(from))\n if ($pos.nodeAfter) {\n tr.setSelection(NodeSelection.create(tr.doc, $pos.pos))\n return\n }\n }\n tr.setSelection(TextSelection.between(tr.doc.resolve(clamp(from)), tr.doc.resolve(clamp(to))))\n}\n\n/* moving, duplicating and removing blocks */\n\n/** `Mod+Shift+↑/↓`: swap the selected blocks with their neighbour. */\nexport function swapBlocks(direction: -1 | 1): Command {\n return (state, dispatch) => {\n const range = selectedRange(state)\n if (!range) return false\n const neighbourIndex = direction < 0 ? range.from - 1 : range.to + 1\n if (neighbourIndex < 0 || neighbourIndex >= range.parent.childCount) return false\n if (!dispatch) return true\n const neighbour = range.parent.child(neighbourIndex)\n const tr = state.tr\n const { from, to } = state.selection\n let shift: number\n if (direction < 0) {\n const at = range.start - neighbour.nodeSize\n tr.delete(at, range.start)\n tr.insert(range.end - neighbour.nodeSize, neighbour)\n shift = -neighbour.nodeSize\n } else {\n tr.delete(range.end, range.end + neighbour.nodeSize)\n tr.insert(range.start, neighbour)\n shift = neighbour.nodeSize\n }\n restoreSelection(tr, state, from + shift, to + shift)\n dispatch(tr.scrollIntoView())\n return true\n }\n}\n\n/**\n * Drag and drop: move the blocks of `range` to `target`, a position right\n * before or after a block, possibly at another level. Fails when the\n * target is inside the moved blocks. The cursor goes to the first one.\n */\nexport function moveRangeTo(range: SiblingRange, target: number): Command {\n return (state, dispatch) => {\n if (target >= range.start && target <= range.end) return false\n if (dispatch) {\n const nodes = blocksIn(range).map((block) => block.node)\n const tr = state.tr\n // a group may not be empty: take the whole group when every child moves\n const whole =\n range.parent.type.name === 'blockGroup' &&\n range.from === 0 &&\n range.to === range.parent.childCount - 1\n if (whole) tr.delete(range.start - 1, range.end + 1)\n else tr.delete(range.start, range.end)\n const at = tr.mapping.map(target)\n tr.insert(at, nodes)\n tr.setSelection(Selection.near(tr.doc.resolve(at + 2)))\n dispatch(tr.scrollIntoView())\n }\n return true\n }\n}\n\n/** Remove blocks, keeping at least one empty paragraph in the document. */\nexport function deleteBlocks(tr: Transaction, blocks: BlockInfo[]): Transaction {\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a copy\n const sorted = [...blocks].sort((a, b) => b.pos - a.pos)\n for (const block of sorted) {\n const from = tr.mapping.map(block.pos)\n const to = tr.mapping.map(block.pos + block.node.nodeSize)\n const $from = tr.doc.resolve(from)\n // a group may not be empty: remove the whole group when this was its only block\n if ($from.parent.type.name === 'blockGroup' && $from.parent.childCount === 1)\n tr.delete($from.before(), $from.after())\n else tr.delete(from, to)\n }\n if (tr.doc.childCount === 0 || tr.doc.content.size === 0) {\n tr.insert(0, newBlock(tr.doc.type.schema, tr.doc.type.schema.nodes.paragraph!.create()))\n }\n const near = Math.min(tr.mapping.map(sorted.at(-1)!.pos), tr.doc.content.size)\n tr.setSelection(Selection.near(tr.doc.resolve(near)))\n return tr\n}\n\n/** Copies of blocks with fresh ids (the id plugin assigns them). */\nexport function withoutIds(node: PMNode): PMNode {\n if (node.type.name !== 'block' && node.type.name !== 'blockGroup') return node\n const children: PMNode[] = []\n node.forEach((child) => children.push(withoutIds(child)))\n return node.type.name === 'block'\n ? node.type.create({ ...node.attrs, id: null }, children)\n : node.type.create(node.attrs, children)\n}\n\n/** Insert copies of the selected blocks right after them. */\nexport const duplicateBlocks: Command = (state, dispatch) => {\n const range = selectedRange(state)\n if (!range) return false\n if (dispatch) {\n const copies = blocksIn(range).map((block) => withoutIds(block.node))\n const tr = state.tr.insert(range.end, copies)\n dispatch(tr.scrollIntoView())\n }\n return true\n}\n\n/** Insert an empty block of `type` after the block at `pos` and put the cursor in it. */\nexport function insertBlockAfter(pos: number, type: ContentType = 'paragraph'): Command {\n return (state, dispatch) => {\n const info = blockAround(state.doc.resolve(Math.min(pos + 1, state.doc.content.size)))\n if (!info) return false\n if (dispatch) {\n const at = info.pos + info.node.nodeSize\n const content = createContentNode(state.schema, type)\n const tr = state.tr.insert(at, newBlock(state.schema, content))\n tr.setSelection(Selection.near(tr.doc.resolve(at + 2)))\n dispatch(tr.scrollIntoView())\n }\n return true\n }\n}\n\n/** The selection's blocks, for menus and the block selection. */\nexport function selectedBlocks(state: EditorState): BlockInfo[] {\n const range = selectedRange(state)\n return range ? blocksIn(range) : []\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { blockAround } from "./blocks.js";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
4
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
+
// src/core/comments.ts
|
|
6
|
+
var commentKey = new PluginKey("dou-editor-comments");
|
|
7
|
+
var pendingDecorations = (doc, range) => range && range.to > range.from ? DecorationSet.create(doc, [Decoration.inline(range.from, range.to, { class: "dou-editor__comment-pending" })]) : DecorationSet.empty;
|
|
8
|
+
/** The ids of the threads at `pos`, the shortest (innermost) first. */
|
|
9
|
+
function commentsAt(state, pos) {
|
|
10
|
+
const $pos = state.doc.resolve(pos);
|
|
11
|
+
const node = $pos.parent.childAfter($pos.parentOffset).node;
|
|
12
|
+
if (!node) return [];
|
|
13
|
+
const ids = node.marks.filter((mark) => mark.type.name === "comment").map((mark) => mark.attrs.id);
|
|
14
|
+
if (ids.length < 2) return ids;
|
|
15
|
+
const length = new Map(ids.map((id) => [id, rangeLength(state.doc, id, pos)]));
|
|
16
|
+
return ids.sort((a, b) => length.get(a) - length.get(b));
|
|
17
|
+
}
|
|
18
|
+
/** How long the run of `id`'s text around `pos` is, within its textblock. */
|
|
19
|
+
function rangeLength(doc, id, pos) {
|
|
20
|
+
const $pos = doc.resolve(pos);
|
|
21
|
+
const start = $pos.start();
|
|
22
|
+
let runStart = -1;
|
|
23
|
+
let around = false;
|
|
24
|
+
let length = Infinity;
|
|
25
|
+
$pos.parent.forEach((child, offset) => {
|
|
26
|
+
const at = start + offset;
|
|
27
|
+
const end = at + child.nodeSize;
|
|
28
|
+
if (child.marks.some((mark) => mark.type.name === "comment" && mark.attrs.id === id)) {
|
|
29
|
+
if (runStart < 0) runStart = at;
|
|
30
|
+
if (at <= pos && pos <= end) around = true;
|
|
31
|
+
if (around) length = end - runStart;
|
|
32
|
+
} else {
|
|
33
|
+
if (around) around = false;
|
|
34
|
+
runStart = -1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return length;
|
|
38
|
+
}
|
|
39
|
+
/** Every thread's anchor in the document, by id: the first and last positions of its text. */
|
|
40
|
+
function commentAnchors(doc) {
|
|
41
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
42
|
+
doc.descendants((node, pos) => {
|
|
43
|
+
if (!node.isText) return true;
|
|
44
|
+
for (const mark of node.marks) {
|
|
45
|
+
if (mark.type.name !== "comment") continue;
|
|
46
|
+
const id = mark.attrs.id;
|
|
47
|
+
const end = pos + node.nodeSize;
|
|
48
|
+
const anchor = anchors.get(id);
|
|
49
|
+
if (anchor) {
|
|
50
|
+
anchor.to = end;
|
|
51
|
+
anchor.text += node.text;
|
|
52
|
+
} else {
|
|
53
|
+
const block = blockAround(doc.resolve(pos));
|
|
54
|
+
anchors.set(id, {
|
|
55
|
+
block: block?.node.attrs.id ?? "",
|
|
56
|
+
text: node.text ?? "",
|
|
57
|
+
from: pos,
|
|
58
|
+
to: end
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
});
|
|
64
|
+
return anchors;
|
|
65
|
+
}
|
|
66
|
+
/** The text of `id`'s anchor that a thread card points at: its first and last positions. */
|
|
67
|
+
function commentRange(doc, id) {
|
|
68
|
+
let from = -1;
|
|
69
|
+
let to = -1;
|
|
70
|
+
doc.descendants((node, pos) => {
|
|
71
|
+
if (!node.isText) return true;
|
|
72
|
+
if (node.marks.some((mark) => mark.type.name === "comment" && mark.attrs.id === id)) {
|
|
73
|
+
if (from < 0) from = pos;
|
|
74
|
+
to = pos + node.nodeSize;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
return from < 0 ? null : {
|
|
79
|
+
from,
|
|
80
|
+
to
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Marks `range` as the text of a new thread. Not undone with the edits around it. */
|
|
84
|
+
function anchorComment(view, range, id) {
|
|
85
|
+
const type = view.state.schema.marks.comment;
|
|
86
|
+
const tr = view.state.tr.addMark(range.from, range.to, type.create({ id }));
|
|
87
|
+
tr.setMeta(commentKey, { pending: null });
|
|
88
|
+
tr.setMeta("addToHistory", false);
|
|
89
|
+
view.dispatch(tr);
|
|
90
|
+
}
|
|
91
|
+
/** Takes a removed thread's anchor off the text. */
|
|
92
|
+
function unanchorComment(view, id) {
|
|
93
|
+
const type = view.state.schema.marks.comment;
|
|
94
|
+
const tr = view.state.tr;
|
|
95
|
+
view.state.doc.descendants((node, pos) => {
|
|
96
|
+
if (!node.isText) return true;
|
|
97
|
+
const mark = node.marks.find((m) => m.type === type && m.attrs.id === id);
|
|
98
|
+
if (mark) tr.removeMark(pos, pos + node.nodeSize, mark);
|
|
99
|
+
return false;
|
|
100
|
+
});
|
|
101
|
+
if (!tr.docChanged) return;
|
|
102
|
+
tr.setMeta("addToHistory", false);
|
|
103
|
+
view.dispatch(tr);
|
|
104
|
+
}
|
|
105
|
+
function setPendingComment(view, range) {
|
|
106
|
+
view.dispatch(view.state.tr.setMeta(commentKey, { pending: range }));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A pasted copy of commented text doesn't take the thread along: the
|
|
110
|
+
* thread keeps its one anchor. Text that was cut is no longer in the
|
|
111
|
+
* document, so its threads come along with it.
|
|
112
|
+
*/
|
|
113
|
+
function dropCopiedComments(slice, state, known) {
|
|
114
|
+
const inDoc = /* @__PURE__ */ new Set();
|
|
115
|
+
let any = false;
|
|
116
|
+
slice.content.descendants((node) => {
|
|
117
|
+
if (node.marks.some((mark) => mark.type.name === "comment")) any = true;
|
|
118
|
+
return !any;
|
|
119
|
+
});
|
|
120
|
+
if (!any) return slice;
|
|
121
|
+
state.doc.descendants((node) => {
|
|
122
|
+
for (const mark of node.marks) if (mark.type.name === "comment") inDoc.add(mark.attrs.id);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
const keep = (id) => !inDoc.has(id) && (!known || known.has(id));
|
|
126
|
+
const strip = (fragment) => {
|
|
127
|
+
const nodes = [];
|
|
128
|
+
fragment.forEach((node) => {
|
|
129
|
+
if (node.isText) nodes.push(node.mark(node.marks.filter((m) => m.type.name !== "comment" || keep(m.attrs.id))));
|
|
130
|
+
else nodes.push(node.copy(strip(node.content)));
|
|
131
|
+
});
|
|
132
|
+
return Fragment.from(nodes);
|
|
133
|
+
};
|
|
134
|
+
return new Slice(strip(slice.content), slice.openStart, slice.openEnd);
|
|
135
|
+
}
|
|
136
|
+
function commentPlugin(options) {
|
|
137
|
+
return new Plugin({
|
|
138
|
+
key: commentKey,
|
|
139
|
+
state: {
|
|
140
|
+
init: () => ({
|
|
141
|
+
pending: null,
|
|
142
|
+
decorations: DecorationSet.empty
|
|
143
|
+
}),
|
|
144
|
+
apply(tr, value) {
|
|
145
|
+
const meta = tr.getMeta(commentKey);
|
|
146
|
+
let pending = value.pending;
|
|
147
|
+
if (meta) pending = meta.pending;
|
|
148
|
+
else if (pending && tr.docChanged) {
|
|
149
|
+
const from = tr.mapping.map(pending.from, 1);
|
|
150
|
+
const to = tr.mapping.map(pending.to, -1);
|
|
151
|
+
pending = to > from ? {
|
|
152
|
+
from,
|
|
153
|
+
to
|
|
154
|
+
} : null;
|
|
155
|
+
}
|
|
156
|
+
if (pending === value.pending && !tr.docChanged) return value;
|
|
157
|
+
return {
|
|
158
|
+
pending,
|
|
159
|
+
decorations: pendingDecorations(tr.doc, pending)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
view: () => {
|
|
164
|
+
let last = null;
|
|
165
|
+
return { update(view, previous) {
|
|
166
|
+
const pending = commentKey.getState(view.state).pending;
|
|
167
|
+
if (pending !== last) options.onPending(last = pending);
|
|
168
|
+
const { selection } = view.state;
|
|
169
|
+
if (!selection.empty && view.state.doc === previous.doc && !selection.eq(previous.selection)) options.onSelect();
|
|
170
|
+
} };
|
|
171
|
+
},
|
|
172
|
+
props: {
|
|
173
|
+
decorations: (state) => commentKey.getState(state).decorations,
|
|
174
|
+
transformPasted: (slice, view) => dropCopiedComments(slice, view.state, options.known()),
|
|
175
|
+
handleClick(view, pos, event) {
|
|
176
|
+
if (event.button !== 0 || !options.known()) return false;
|
|
177
|
+
const known = options.known();
|
|
178
|
+
options.onClick(commentsAt(view.state, pos).filter((id) => known.has(id)));
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export { anchorComment, commentAnchors, commentKey, commentPlugin, commentRange, commentsAt, setPendingComment, unanchorComment };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","names":[],"sources":["../../src/core/comments.ts"],"sourcesContent":["import { Fragment, Slice, type Node as PMNode } from 'prosemirror-model'\nimport { Plugin, PluginKey, type EditorState, type Transaction } from 'prosemirror-state'\nimport { Decoration, DecorationSet, type EditorView } from 'prosemirror-view'\nimport { blockAround } from './blocks'\n\n/*\n * Comment threads are anchored by a `comment` mark carrying the thread's\n * id on the text they are about. The threads themselves (comments,\n * replies, resolved) are kept outside the document.\n */\n\n/** A range of the document, as positions. */\nexport interface CommentRange {\n from: number\n to: number\n}\n\n/** Where a thread is anchored: the block and the text it is about. */\nexport interface CommentAnchor {\n /** The id of the (first) block holding the text. */\n block: string\n /** The text the thread is about. */\n text: string\n from: number\n to: number\n}\n\nexport interface CommentPluginState {\n /** The text a thread is being started on, until its first comment is sent. */\n pending: CommentRange | null\n decorations: DecorationSet\n}\n\nexport const commentKey = new PluginKey<CommentPluginState>('dou-editor-comments')\n\ntype CommentMeta = { pending: CommentRange | null }\n\nexport interface CommentPluginOptions {\n /** The ids of the threads there are, or `null` when comments are off. */\n known: () => ReadonlySet<string> | null\n /** A click on text that is in threads, with their ids, innermost first; `[]` elsewhere. */\n onClick: (ids: string[]) => void\n onPending: (range: CommentRange | null) => void\n /** Text got selected (a range, not a cursor). */\n onSelect: () => void\n}\n\nconst pendingDecorations = (doc: PMNode, range: CommentRange | null) =>\n range && range.to > range.from\n ? DecorationSet.create(doc, [\n Decoration.inline(range.from, range.to, { class: 'dou-editor__comment-pending' }),\n ])\n : DecorationSet.empty\n\n/** The ids of the threads at `pos`, the shortest (innermost) first. */\nexport function commentsAt(state: EditorState, pos: number): string[] {\n const $pos = state.doc.resolve(pos)\n const node = $pos.parent.childAfter($pos.parentOffset).node\n if (!node) return []\n const ids = node.marks\n .filter((mark) => mark.type.name === 'comment')\n .map((mark) => mark.attrs.id as string)\n if (ids.length < 2) return ids\n const length = new Map(ids.map((id) => [id, rangeLength(state.doc, id, pos)]))\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts the fresh list\n return ids.sort((a, b) => length.get(a)! - length.get(b)!)\n}\n\n/** How long the run of `id`'s text around `pos` is, within its textblock. */\nfunction rangeLength(doc: PMNode, id: string, pos: number): number {\n const $pos = doc.resolve(pos)\n const start = $pos.start()\n let runStart = -1\n let around = false\n let length = Infinity\n $pos.parent.forEach((child, offset) => {\n const at = start + offset\n const end = at + child.nodeSize\n if (child.marks.some((mark) => mark.type.name === 'comment' && mark.attrs.id === id)) {\n if (runStart < 0) runStart = at\n if (at <= pos && pos <= end) around = true\n if (around) length = end - runStart\n } else {\n if (around) around = false\n runStart = -1\n }\n })\n return length\n}\n\n/** Every thread's anchor in the document, by id: the first and last positions of its text. */\nexport function commentAnchors(doc: PMNode): Map<string, CommentAnchor> {\n const anchors = new Map<string, CommentAnchor>()\n doc.descendants((node, pos) => {\n if (!node.isText) return true\n for (const mark of node.marks) {\n if (mark.type.name !== 'comment') continue\n const id = mark.attrs.id as string\n const end = pos + node.nodeSize\n const anchor = anchors.get(id)\n if (anchor) {\n anchor.to = end\n anchor.text += node.text\n } else {\n const block = blockAround(doc.resolve(pos))\n anchors.set(id, {\n block: (block?.node.attrs.id as string | undefined) ?? '',\n text: node.text ?? '',\n from: pos,\n to: end,\n })\n }\n }\n return false\n })\n return anchors\n}\n\n/** The text of `id`'s anchor that a thread card points at: its first and last positions. */\nexport function commentRange(doc: PMNode, id: string): CommentRange | null {\n let from = -1\n let to = -1\n doc.descendants((node, pos) => {\n if (!node.isText) return true\n if (node.marks.some((mark) => mark.type.name === 'comment' && mark.attrs.id === id)) {\n if (from < 0) from = pos\n to = pos + node.nodeSize\n }\n return false\n })\n return from < 0 ? null : { from, to }\n}\n\n/** Marks `range` as the text of a new thread. Not undone with the edits around it. */\nexport function anchorComment(view: EditorView, range: CommentRange, id: string) {\n const type = view.state.schema.marks.comment!\n const tr = view.state.tr.addMark(range.from, range.to, type.create({ id }))\n tr.setMeta(commentKey, { pending: null } satisfies CommentMeta)\n tr.setMeta('addToHistory', false)\n view.dispatch(tr)\n}\n\n/** Takes a removed thread's anchor off the text. */\nexport function unanchorComment(view: EditorView, id: string) {\n const type = view.state.schema.marks.comment!\n const tr = view.state.tr\n view.state.doc.descendants((node, pos) => {\n if (!node.isText) return true\n const mark = node.marks.find((m) => m.type === type && m.attrs.id === id)\n if (mark) tr.removeMark(pos, pos + node.nodeSize, mark)\n return false\n })\n if (!tr.docChanged) return\n tr.setMeta('addToHistory', false)\n view.dispatch(tr)\n}\n\nexport function setPendingComment(view: EditorView, range: CommentRange | null) {\n view.dispatch(view.state.tr.setMeta(commentKey, { pending: range } satisfies CommentMeta))\n}\n\n/**\n * A pasted copy of commented text doesn't take the thread along: the\n * thread keeps its one anchor. Text that was cut is no longer in the\n * document, so its threads come along with it.\n */\nfunction dropCopiedComments(slice: Slice, state: EditorState, known: ReadonlySet<string> | null) {\n const inDoc = new Set<string>()\n let any = false\n slice.content.descendants((node) => {\n if (node.marks.some((mark) => mark.type.name === 'comment')) any = true\n return !any\n })\n if (!any) return slice\n state.doc.descendants((node) => {\n for (const mark of node.marks) if (mark.type.name === 'comment') inDoc.add(mark.attrs.id)\n return true\n })\n const keep = (id: string) => !inDoc.has(id) && (!known || known.has(id))\n const strip = (fragment: Fragment): Fragment => {\n const nodes: PMNode[] = []\n fragment.forEach((node) => {\n if (node.isText) {\n nodes.push(\n node.mark(node.marks.filter((m) => m.type.name !== 'comment' || keep(m.attrs.id))),\n )\n } else nodes.push(node.copy(strip(node.content)))\n })\n return Fragment.from(nodes)\n }\n return new Slice(strip(slice.content), slice.openStart, slice.openEnd)\n}\n\nexport function commentPlugin(options: CommentPluginOptions): Plugin<CommentPluginState> {\n return new Plugin<CommentPluginState>({\n key: commentKey,\n state: {\n init: () => ({ pending: null, decorations: DecorationSet.empty }),\n apply(tr: Transaction, value: CommentPluginState): CommentPluginState {\n const meta = tr.getMeta(commentKey) as CommentMeta | undefined\n let pending = value.pending\n if (meta) pending = meta.pending\n else if (pending && tr.docChanged) {\n const from = tr.mapping.map(pending.from, 1)\n const to = tr.mapping.map(pending.to, -1)\n pending = to > from ? { from, to } : null\n }\n if (pending === value.pending && !tr.docChanged) return value\n return { pending, decorations: pendingDecorations(tr.doc, pending) }\n },\n },\n view: () => {\n let last: CommentRange | null = null\n return {\n update(view, previous) {\n const pending = commentKey.getState(view.state)!.pending\n if (pending !== last) options.onPending((last = pending))\n const { selection } = view.state\n // a selection made by the user, not one moved along by an edit\n if (\n !selection.empty &&\n view.state.doc === previous.doc &&\n !selection.eq(previous.selection)\n )\n options.onSelect()\n },\n }\n },\n props: {\n decorations: (state) => commentKey.getState(state)!.decorations,\n transformPasted: (slice, view) => dropCopiedComments(slice, view.state, options.known()),\n handleClick(view, pos, event) {\n if (event.button !== 0 || !options.known()) return false\n const known = options.known()!\n options.onClick(commentsAt(view.state, pos).filter((id) => known.has(id)))\n return false\n },\n },\n })\n}\n"],"mappings":";;;;;AAiCA,IAAa,aAAa,IAAI,UAA8B,qBAAqB;AAcjF,IAAM,sBAAsB,KAAa,UACvC,SAAS,MAAM,KAAK,MAAM,OACtB,cAAc,OAAO,KAAK,CACxB,WAAW,OAAO,MAAM,MAAM,MAAM,IAAI,EAAE,OAAO,8BAA8B,CAAC,CAClF,CAAC,IACD,cAAc;;AAGpB,SAAgB,WAAW,OAAoB,KAAuB;CACpE,MAAM,OAAO,MAAM,IAAI,QAAQ,GAAG;CAClC,MAAM,OAAO,KAAK,OAAO,WAAW,KAAK,YAAY,CAAC,CAAC;CACvD,IAAI,CAAC,MAAM,OAAO,CAAC;CACnB,MAAM,MAAM,KAAK,MACd,QAAQ,SAAS,KAAK,KAAK,SAAS,SAAS,CAAC,CAC9C,KAAK,SAAS,KAAK,MAAM,EAAY;CACxC,IAAI,IAAI,SAAS,GAAG,OAAO;CAC3B,MAAM,SAAS,IAAI,IAAI,IAAI,KAAK,OAAO,CAAC,IAAI,YAAY,MAAM,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC;CAE7E,OAAO,IAAI,MAAM,GAAG,MAAM,OAAO,IAAI,CAAC,IAAK,OAAO,IAAI,CAAC,CAAE;AAC3D;;AAGA,SAAS,YAAY,KAAa,IAAY,KAAqB;CACjE,MAAM,OAAO,IAAI,QAAQ,GAAG;CAC5B,MAAM,QAAQ,KAAK,MAAM;CACzB,IAAI,WAAW;CACf,IAAI,SAAS;CACb,IAAI,SAAS;CACb,KAAK,OAAO,SAAS,OAAO,WAAW;EACrC,MAAM,KAAK,QAAQ;EACnB,MAAM,MAAM,KAAK,MAAM;EACvB,IAAI,MAAM,MAAM,MAAM,SAAS,KAAK,KAAK,SAAS,aAAa,KAAK,MAAM,OAAO,EAAE,GAAG;GACpF,IAAI,WAAW,GAAG,WAAW;GAC7B,IAAI,MAAM,OAAO,OAAO,KAAK,SAAS;GACtC,IAAI,QAAQ,SAAS,MAAM;EAC7B,OAAO;GACL,IAAI,QAAQ,SAAS;GACrB,WAAW;EACb;CACF,CAAC;CACD,OAAO;AACT;;AAGA,SAAgB,eAAe,KAAyC;CACtE,MAAM,0BAAU,IAAI,IAA2B;CAC/C,IAAI,aAAa,MAAM,QAAQ;EAC7B,IAAI,CAAC,KAAK,QAAQ,OAAO;EACzB,KAAK,MAAM,QAAQ,KAAK,OAAO;GAC7B,IAAI,KAAK,KAAK,SAAS,WAAW;GAClC,MAAM,KAAK,KAAK,MAAM;GACtB,MAAM,MAAM,MAAM,KAAK;GACvB,MAAM,SAAS,QAAQ,IAAI,EAAE;GAC7B,IAAI,QAAQ;IACV,OAAO,KAAK;IACZ,OAAO,QAAQ,KAAK;GACtB,OAAO;IACL,MAAM,QAAQ,YAAY,IAAI,QAAQ,GAAG,CAAC;IAC1C,QAAQ,IAAI,IAAI;KACd,OAAQ,OAAO,KAAK,MAAM,MAA6B;KACvD,MAAM,KAAK,QAAQ;KACnB,MAAM;KACN,IAAI;IACN,CAAC;GACH;EACF;EACA,OAAO;CACT,CAAC;CACD,OAAO;AACT;;AAGA,SAAgB,aAAa,KAAa,IAAiC;CACzE,IAAI,OAAO;CACX,IAAI,KAAK;CACT,IAAI,aAAa,MAAM,QAAQ;EAC7B,IAAI,CAAC,KAAK,QAAQ,OAAO;EACzB,IAAI,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,SAAS,aAAa,KAAK,MAAM,OAAO,EAAE,GAAG;GACnF,IAAI,OAAO,GAAG,OAAO;GACrB,KAAK,MAAM,KAAK;EAClB;EACA,OAAO;CACT,CAAC;CACD,OAAO,OAAO,IAAI,OAAO;EAAE;EAAM;CAAG;AACtC;;AAGA,SAAgB,cAAc,MAAkB,OAAqB,IAAY;CAC/E,MAAM,OAAO,KAAK,MAAM,OAAO,MAAM;CACrC,MAAM,KAAK,KAAK,MAAM,GAAG,QAAQ,MAAM,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,GAAG,CAAC,CAAC;CAC1E,GAAG,QAAQ,YAAY,EAAE,SAAS,KAAK,CAAuB;CAC9D,GAAG,QAAQ,gBAAgB,KAAK;CAChC,KAAK,SAAS,EAAE;AAClB;;AAGA,SAAgB,gBAAgB,MAAkB,IAAY;CAC5D,MAAM,OAAO,KAAK,MAAM,OAAO,MAAM;CACrC,MAAM,KAAK,KAAK,MAAM;CACtB,KAAK,MAAM,IAAI,aAAa,MAAM,QAAQ;EACxC,IAAI,CAAC,KAAK,QAAQ,OAAO;EACzB,MAAM,OAAO,KAAK,MAAM,MAAM,MAAM,EAAE,SAAS,QAAQ,EAAE,MAAM,OAAO,EAAE;EACxE,IAAI,MAAM,GAAG,WAAW,KAAK,MAAM,KAAK,UAAU,IAAI;EACtD,OAAO;CACT,CAAC;CACD,IAAI,CAAC,GAAG,YAAY;CACpB,GAAG,QAAQ,gBAAgB,KAAK;CAChC,KAAK,SAAS,EAAE;AAClB;AAEA,SAAgB,kBAAkB,MAAkB,OAA4B;CAC9E,KAAK,SAAS,KAAK,MAAM,GAAG,QAAQ,YAAY,EAAE,SAAS,MAAM,CAAuB,CAAC;AAC3F;;;;;;AAOA,SAAS,mBAAmB,OAAc,OAAoB,OAAmC;CAC/F,MAAM,wBAAQ,IAAI,IAAY;CAC9B,IAAI,MAAM;CACV,MAAM,QAAQ,aAAa,SAAS;EAClC,IAAI,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,SAAS,SAAS,GAAG,MAAM;EACnE,OAAO,CAAC;CACV,CAAC;CACD,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,IAAI,aAAa,SAAS;EAC9B,KAAK,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK,SAAS,WAAW,MAAM,IAAI,KAAK,MAAM,EAAE;EACxF,OAAO;CACT,CAAC;CACD,MAAM,QAAQ,OAAe,CAAC,MAAM,IAAI,EAAE,MAAM,CAAC,SAAS,MAAM,IAAI,EAAE;CACtE,MAAM,SAAS,aAAiC;EAC9C,MAAM,QAAkB,CAAC;EACzB,SAAS,SAAS,SAAS;GACzB,IAAI,KAAK,QACP,MAAM,KACJ,KAAK,KAAK,KAAK,MAAM,QAAQ,MAAM,EAAE,KAAK,SAAS,aAAa,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CACnF;QACK,MAAM,KAAK,KAAK,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC;EAClD,CAAC;EACD,OAAO,SAAS,KAAK,KAAK;CAC5B;CACA,OAAO,IAAI,MAAM,MAAM,MAAM,OAAO,GAAG,MAAM,WAAW,MAAM,OAAO;AACvE;AAEA,SAAgB,cAAc,SAA2D;CACvF,OAAO,IAAI,OAA2B;EACpC,KAAK;EACL,OAAO;GACL,aAAa;IAAE,SAAS;IAAM,aAAa,cAAc;GAAM;GAC/D,MAAM,IAAiB,OAA+C;IACpE,MAAM,OAAO,GAAG,QAAQ,UAAU;IAClC,IAAI,UAAU,MAAM;IACpB,IAAI,MAAM,UAAU,KAAK;SACpB,IAAI,WAAW,GAAG,YAAY;KACjC,MAAM,OAAO,GAAG,QAAQ,IAAI,QAAQ,MAAM,CAAC;KAC3C,MAAM,KAAK,GAAG,QAAQ,IAAI,QAAQ,IAAI,EAAE;KACxC,UAAU,KAAK,OAAO;MAAE;MAAM;KAAG,IAAI;IACvC;IACA,IAAI,YAAY,MAAM,WAAW,CAAC,GAAG,YAAY,OAAO;IACxD,OAAO;KAAE;KAAS,aAAa,mBAAmB,GAAG,KAAK,OAAO;IAAE;GACrE;EACF;EACA,YAAY;GACV,IAAI,OAA4B;GAChC,OAAO,EACL,OAAO,MAAM,UAAU;IACrB,MAAM,UAAU,WAAW,SAAS,KAAK,KAAK,CAAC,CAAE;IACjD,IAAI,YAAY,MAAM,QAAQ,UAAW,OAAO,OAAQ;IACxD,MAAM,EAAE,cAAc,KAAK;IAE3B,IACE,CAAC,UAAU,SACX,KAAK,MAAM,QAAQ,SAAS,OAC5B,CAAC,UAAU,GAAG,SAAS,SAAS,GAEhC,QAAQ,SAAS;GACrB,EACF;EACF;EACA,OAAO;GACL,cAAc,UAAU,WAAW,SAAS,KAAK,CAAC,CAAE;GACpD,kBAAkB,OAAO,SAAS,mBAAmB,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC;GACvF,YAAY,MAAM,KAAK,OAAO;IAC5B,IAAI,MAAM,WAAW,KAAK,CAAC,QAAQ,MAAM,GAAG,OAAO;IACnD,MAAM,QAAQ,QAAQ,MAAM;IAC5B,QAAQ,QAAQ,WAAW,KAAK,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,MAAM,IAAI,EAAE,CAAC,CAAC;IACzE,OAAO;GACT;EACF;CACF,CAAC;AACH"}
|