@blocknote/core 0.9.3 → 0.9.5
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/blocknote.js +2603 -2267
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +7 -7
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +2 -2
- package/src/BlockNoteEditor.ts +44 -12
- package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +21 -21
- package/src/api/blockManipulation/blockManipulation.test.ts +8 -11
- package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +3 -3
- package/src/api/formatConversions/formatConversions.test.ts +5 -5
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +3 -3
- package/src/api/nodeConversions/nodeConversions.test.ts +10 -4
- package/src/api/nodeConversions/nodeConversions.ts +9 -7
- package/src/api/nodeConversions/testUtil.ts +3 -3
- package/src/editor.module.css +1 -1
- package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +5 -3
- package/src/extensions/BackgroundColor/BackgroundColorMark.ts +2 -1
- package/src/extensions/Blocks/NonEditableBlockPlugin.ts +17 -0
- package/src/extensions/Blocks/api/block.ts +62 -17
- package/src/extensions/Blocks/api/blockTypes.ts +79 -27
- package/src/extensions/Blocks/api/defaultBlocks.ts +13 -41
- package/src/extensions/Blocks/api/defaultProps.ts +16 -0
- package/src/extensions/Blocks/nodes/Block.module.css +78 -24
- package/src/extensions/Blocks/nodes/BlockContainer.ts +66 -42
- package/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +59 -13
- package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +305 -0
- package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +13 -0
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +24 -2
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +146 -120
- package/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +12 -2
- package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +239 -0
- package/src/extensions/SlashMenu/defaultSlashMenuItems.ts +47 -6
- package/src/extensions/TextColor/TextColorExtension.ts +4 -3
- package/src/extensions/TextColor/TextColorMark.ts +2 -1
- package/src/index.ts +4 -0
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +4 -0
- package/types/src/BlockNoteEditor.d.ts +9 -0
- package/types/src/BlockNoteExtensions.d.ts +1 -1
- package/types/src/extensions/Blocks/api/block.d.ts +7 -8
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +29 -20
- package/types/src/extensions/Blocks/api/defaultBlocks.d.ts +55 -51
- package/types/src/extensions/Blocks/api/defaultProps.d.ts +2 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.d.ts +43 -9
- package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.d.ts +2 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +1 -0
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.d.ts +35 -9
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.d.ts +35 -9
- package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.d.ts +36 -1
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +1 -1
- package/types/src/index.d.ts +4 -0
- package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +1 -1
- package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/Image.d.ts +0 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
2
|
import { Fragment, Node as PMNode, Slice } from "prosemirror-model";
|
|
3
|
-
import { TextSelection } from "prosemirror-state";
|
|
3
|
+
import { NodeSelection, TextSelection } from "prosemirror-state";
|
|
4
4
|
import {
|
|
5
5
|
blockToNode,
|
|
6
6
|
inlineContentToNodes,
|
|
@@ -16,6 +16,7 @@ import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
|
|
|
16
16
|
import styles from "./Block.module.css";
|
|
17
17
|
import BlockAttributes from "./BlockAttributes";
|
|
18
18
|
import { mergeCSSClasses } from "../../../shared/utils";
|
|
19
|
+
import { NonEditableBlockPlugin } from "../NonEditableBlockPlugin";
|
|
19
20
|
|
|
20
21
|
declare module "@tiptap/core" {
|
|
21
22
|
interface Commands<ReturnType> {
|
|
@@ -205,14 +206,20 @@ export const BlockContainer = Node.create<{
|
|
|
205
206
|
// Replaces the blockContent node with one of the new type and
|
|
206
207
|
// adds the provided props as attributes. Also preserves all
|
|
207
208
|
// existing attributes that are compatible with the new type.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
// Need to reset the selection since replacing the block content
|
|
210
|
+
// sets it to the next block.
|
|
211
|
+
state.tr
|
|
212
|
+
.replaceWith(
|
|
213
|
+
startPos,
|
|
214
|
+
endPos,
|
|
215
|
+
state.schema.nodes[newType].create({
|
|
216
|
+
...contentNode.attrs,
|
|
217
|
+
...block.props,
|
|
218
|
+
})
|
|
219
|
+
)
|
|
220
|
+
.setSelection(
|
|
221
|
+
new NodeSelection(state.tr.doc.resolve(startPos))
|
|
222
|
+
);
|
|
216
223
|
} else {
|
|
217
224
|
// Changes the blockContent node type and adds the provided props
|
|
218
225
|
// as attributes. Also preserves all existing attributes that are
|
|
@@ -404,7 +411,7 @@ export const BlockContainer = Node.create<{
|
|
|
404
411
|
},
|
|
405
412
|
|
|
406
413
|
addProseMirrorPlugins() {
|
|
407
|
-
return [PreviousBlockTypePlugin()];
|
|
414
|
+
return [PreviousBlockTypePlugin(), NonEditableBlockPlugin()];
|
|
408
415
|
},
|
|
409
416
|
|
|
410
417
|
addKeyboardShortcuts() {
|
|
@@ -478,6 +485,51 @@ export const BlockContainer = Node.create<{
|
|
|
478
485
|
}),
|
|
479
486
|
]);
|
|
480
487
|
|
|
488
|
+
const handleDelete = () =>
|
|
489
|
+
this.editor.commands.first(({ commands }) => [
|
|
490
|
+
// Deletes the selection if it's not empty.
|
|
491
|
+
() => commands.deleteSelection(),
|
|
492
|
+
// Merges block with the next one (at the same nesting level or lower),
|
|
493
|
+
// if one exists, the block has no children, and the selection is at the
|
|
494
|
+
// end of the block.
|
|
495
|
+
() =>
|
|
496
|
+
commands.command(({ state }) => {
|
|
497
|
+
const { node, contentNode, depth, endPos } = getBlockInfoFromPos(
|
|
498
|
+
state.doc,
|
|
499
|
+
state.selection.from
|
|
500
|
+
)!;
|
|
501
|
+
|
|
502
|
+
const blockAtDocEnd = false;
|
|
503
|
+
const selectionAtBlockEnd =
|
|
504
|
+
state.selection.$anchor.parentOffset ===
|
|
505
|
+
contentNode.firstChild!.nodeSize;
|
|
506
|
+
const selectionEmpty =
|
|
507
|
+
state.selection.anchor === state.selection.head;
|
|
508
|
+
const hasChildBlocks = node.childCount === 2;
|
|
509
|
+
|
|
510
|
+
if (
|
|
511
|
+
!blockAtDocEnd &&
|
|
512
|
+
selectionAtBlockEnd &&
|
|
513
|
+
selectionEmpty &&
|
|
514
|
+
!hasChildBlocks
|
|
515
|
+
) {
|
|
516
|
+
let oldDepth = depth;
|
|
517
|
+
let newPos = endPos + 2;
|
|
518
|
+
let newDepth = state.doc.resolve(newPos).depth;
|
|
519
|
+
|
|
520
|
+
while (newDepth < oldDepth) {
|
|
521
|
+
oldDepth = newDepth;
|
|
522
|
+
newPos += 2;
|
|
523
|
+
newDepth = state.doc.resolve(newPos).depth;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return commands.BNMergeBlocks(newPos - 1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return false;
|
|
530
|
+
}),
|
|
531
|
+
]);
|
|
532
|
+
|
|
481
533
|
const handleEnter = () =>
|
|
482
534
|
this.editor.commands.first(({ commands }) => [
|
|
483
535
|
// Removes a level of nesting if the block is empty & indented, while the selection is also empty & at the start
|
|
@@ -545,12 +597,14 @@ export const BlockContainer = Node.create<{
|
|
|
545
597
|
state.selection.from
|
|
546
598
|
)!;
|
|
547
599
|
|
|
600
|
+
const selectionAtBlockStart =
|
|
601
|
+
state.selection.$anchor.parentOffset === 0;
|
|
548
602
|
const blockEmpty = node.textContent.length === 0;
|
|
549
603
|
|
|
550
604
|
if (!blockEmpty) {
|
|
551
605
|
chain()
|
|
552
606
|
.deleteSelection()
|
|
553
|
-
.BNSplitBlock(state.selection.from,
|
|
607
|
+
.BNSplitBlock(state.selection.from, selectionAtBlockStart)
|
|
554
608
|
.run();
|
|
555
609
|
|
|
556
610
|
return true;
|
|
@@ -562,6 +616,7 @@ export const BlockContainer = Node.create<{
|
|
|
562
616
|
|
|
563
617
|
return {
|
|
564
618
|
Backspace: handleBackspace,
|
|
619
|
+
Delete: handleDelete,
|
|
565
620
|
Enter: handleEnter,
|
|
566
621
|
// Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the
|
|
567
622
|
// editor since the browser will try to use tab for keyboard navigation.
|
|
@@ -577,37 +632,6 @@ export const BlockContainer = Node.create<{
|
|
|
577
632
|
this.editor.commands.BNCreateBlock(
|
|
578
633
|
this.editor.state.selection.anchor + 2
|
|
579
634
|
),
|
|
580
|
-
"Mod-Alt-1": () =>
|
|
581
|
-
this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, {
|
|
582
|
-
type: "heading",
|
|
583
|
-
props: {
|
|
584
|
-
level: "1",
|
|
585
|
-
},
|
|
586
|
-
}),
|
|
587
|
-
"Mod-Alt-2": () =>
|
|
588
|
-
this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, {
|
|
589
|
-
type: "heading",
|
|
590
|
-
props: {
|
|
591
|
-
level: "2",
|
|
592
|
-
},
|
|
593
|
-
}),
|
|
594
|
-
"Mod-Alt-3": () =>
|
|
595
|
-
this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, {
|
|
596
|
-
type: "heading",
|
|
597
|
-
props: {
|
|
598
|
-
level: "3",
|
|
599
|
-
},
|
|
600
|
-
}),
|
|
601
|
-
"Mod-Shift-7": () =>
|
|
602
|
-
this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, {
|
|
603
|
-
type: "bulletListItem",
|
|
604
|
-
props: {},
|
|
605
|
-
}),
|
|
606
|
-
"Mod-Shift-8": () =>
|
|
607
|
-
this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, {
|
|
608
|
-
type: "numberedListItem",
|
|
609
|
-
props: {},
|
|
610
|
-
}),
|
|
611
635
|
};
|
|
612
636
|
},
|
|
613
637
|
});
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { InputRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { defaultProps } from "../../../api/defaultProps";
|
|
2
3
|
import { createTipTapBlock } from "../../../api/block";
|
|
3
|
-
import
|
|
4
|
+
import { BlockSpec, PropSchema } from "../../../api/blockTypes";
|
|
4
5
|
import { mergeCSSClasses } from "../../../../../shared/utils";
|
|
6
|
+
import styles from "../../Block.module.css";
|
|
7
|
+
|
|
8
|
+
export const headingPropSchema = {
|
|
9
|
+
...defaultProps,
|
|
10
|
+
level: { default: 1, values: [1, 2, 3] as const },
|
|
11
|
+
} satisfies PropSchema;
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
const HeadingBlockContent = createTipTapBlock<"heading", true>({
|
|
7
14
|
name: "heading",
|
|
8
15
|
content: "inline*",
|
|
9
16
|
|
|
10
17
|
addAttributes() {
|
|
11
18
|
return {
|
|
12
19
|
level: {
|
|
13
|
-
default:
|
|
20
|
+
default: 1,
|
|
14
21
|
// instead of "level" attributes, use "data-level"
|
|
15
|
-
parseHTML: (element) => element.getAttribute("data-level")
|
|
22
|
+
parseHTML: (element) => element.getAttribute("data-level")!,
|
|
16
23
|
renderHTML: (attributes) => {
|
|
17
24
|
return {
|
|
18
|
-
"data-level": attributes.level,
|
|
25
|
+
"data-level": (attributes.level as number).toString(),
|
|
19
26
|
};
|
|
20
27
|
},
|
|
21
28
|
},
|
|
@@ -24,16 +31,18 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
|
|
|
24
31
|
|
|
25
32
|
addInputRules() {
|
|
26
33
|
return [
|
|
27
|
-
...[
|
|
34
|
+
...[1, 2, 3].map((level) => {
|
|
28
35
|
// Creates a heading of appropriate level when starting with "#", "##", or "###".
|
|
29
36
|
return new InputRule({
|
|
30
|
-
find: new RegExp(`^(#{${
|
|
37
|
+
find: new RegExp(`^(#{${level}})\\s$`),
|
|
31
38
|
handler: ({ state, chain, range }) => {
|
|
32
39
|
chain()
|
|
33
|
-
.BNUpdateBlock
|
|
40
|
+
.BNUpdateBlock<{
|
|
41
|
+
heading: BlockSpec<"heading", typeof headingPropSchema, true>;
|
|
42
|
+
}>(state.selection.from, {
|
|
34
43
|
type: "heading",
|
|
35
44
|
props: {
|
|
36
|
-
level: level as
|
|
45
|
+
level: level as 1 | 2 | 3,
|
|
37
46
|
},
|
|
38
47
|
})
|
|
39
48
|
// Removes the "#" character(s) used to set the heading.
|
|
@@ -44,21 +53,53 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
|
|
|
44
53
|
];
|
|
45
54
|
},
|
|
46
55
|
|
|
56
|
+
addKeyboardShortcuts() {
|
|
57
|
+
return {
|
|
58
|
+
"Mod-Alt-1": () =>
|
|
59
|
+
this.editor.commands.BNUpdateBlock<{
|
|
60
|
+
heading: BlockSpec<"heading", typeof headingPropSchema, true>;
|
|
61
|
+
}>(this.editor.state.selection.anchor, {
|
|
62
|
+
type: "heading",
|
|
63
|
+
props: {
|
|
64
|
+
level: 1,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
"Mod-Alt-2": () =>
|
|
68
|
+
this.editor.commands.BNUpdateBlock<{
|
|
69
|
+
heading: BlockSpec<"heading", typeof headingPropSchema, true>;
|
|
70
|
+
}>(this.editor.state.selection.anchor, {
|
|
71
|
+
type: "heading",
|
|
72
|
+
props: {
|
|
73
|
+
level: 2,
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
"Mod-Alt-3": () =>
|
|
77
|
+
this.editor.commands.BNUpdateBlock<{
|
|
78
|
+
heading: BlockSpec<"heading", typeof headingPropSchema, true>;
|
|
79
|
+
}>(this.editor.state.selection.anchor, {
|
|
80
|
+
type: "heading",
|
|
81
|
+
props: {
|
|
82
|
+
level: 3,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
|
|
47
88
|
parseHTML() {
|
|
48
89
|
return [
|
|
49
90
|
{
|
|
50
91
|
tag: "h1",
|
|
51
|
-
attrs: { level:
|
|
92
|
+
attrs: { level: 1 },
|
|
52
93
|
node: "heading",
|
|
53
94
|
},
|
|
54
95
|
{
|
|
55
96
|
tag: "h2",
|
|
56
|
-
attrs: { level:
|
|
97
|
+
attrs: { level: 2 },
|
|
57
98
|
node: "heading",
|
|
58
99
|
},
|
|
59
100
|
{
|
|
60
101
|
tag: "h3",
|
|
61
|
-
attrs: { level:
|
|
102
|
+
attrs: { level: 3 },
|
|
62
103
|
node: "heading",
|
|
63
104
|
},
|
|
64
105
|
];
|
|
@@ -81,7 +122,7 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
|
|
|
81
122
|
"data-content-type": this.name,
|
|
82
123
|
}),
|
|
83
124
|
[
|
|
84
|
-
|
|
125
|
+
`h${node.attrs.level}`,
|
|
85
126
|
{
|
|
86
127
|
...inlineContentDOMAttributes,
|
|
87
128
|
class: mergeCSSClasses(
|
|
@@ -94,3 +135,8 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
|
|
|
94
135
|
];
|
|
95
136
|
},
|
|
96
137
|
});
|
|
138
|
+
|
|
139
|
+
export const Heading = {
|
|
140
|
+
node: HeadingBlockContent,
|
|
141
|
+
propSchema: headingPropSchema,
|
|
142
|
+
} satisfies BlockSpec<"heading", typeof headingPropSchema, true>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createBlockSpec } from "../../../api/block";
|
|
2
|
+
import { defaultProps } from "../../../api/defaultProps";
|
|
3
|
+
import { BlockSpec, PropSchema, SpecificBlock } from "../../../api/blockTypes";
|
|
4
|
+
import { BlockNoteEditor } from "../../../../../BlockNoteEditor";
|
|
5
|
+
import { imageToolbarPluginKey } from "../../../../ImageToolbar/ImageToolbarPlugin";
|
|
6
|
+
import styles from "../../Block.module.css";
|
|
7
|
+
|
|
8
|
+
export const imagePropSchema = {
|
|
9
|
+
textAlignment: defaultProps.textAlignment,
|
|
10
|
+
backgroundColor: defaultProps.backgroundColor,
|
|
11
|
+
// Image url.
|
|
12
|
+
url: {
|
|
13
|
+
default: "" as const,
|
|
14
|
+
},
|
|
15
|
+
// Image caption.
|
|
16
|
+
caption: {
|
|
17
|
+
default: "" as const,
|
|
18
|
+
},
|
|
19
|
+
// Image width in px.
|
|
20
|
+
width: {
|
|
21
|
+
default: 512 as const,
|
|
22
|
+
},
|
|
23
|
+
} satisfies PropSchema;
|
|
24
|
+
|
|
25
|
+
// Converts text alignment prop values to the flexbox `align-items` values.
|
|
26
|
+
const textAlignmentToAlignItems = (
|
|
27
|
+
textAlignment: "left" | "center" | "right" | "justify"
|
|
28
|
+
): "flex-start" | "center" | "flex-end" => {
|
|
29
|
+
switch (textAlignment) {
|
|
30
|
+
case "left":
|
|
31
|
+
return "flex-start";
|
|
32
|
+
case "center":
|
|
33
|
+
return "center";
|
|
34
|
+
case "right":
|
|
35
|
+
return "flex-end";
|
|
36
|
+
default:
|
|
37
|
+
return "flex-start";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Min image width in px.
|
|
42
|
+
const minWidth = 64;
|
|
43
|
+
|
|
44
|
+
const renderImage = (
|
|
45
|
+
block: SpecificBlock<
|
|
46
|
+
{ image: BlockSpec<"image", typeof imagePropSchema, false> },
|
|
47
|
+
"image"
|
|
48
|
+
>,
|
|
49
|
+
editor: BlockNoteEditor<{
|
|
50
|
+
image: BlockSpec<"image", typeof imagePropSchema, false>;
|
|
51
|
+
}>
|
|
52
|
+
) => {
|
|
53
|
+
// Wrapper element to set the image alignment, contains both image/image
|
|
54
|
+
// upload dashboard and caption.
|
|
55
|
+
const wrapper = document.createElement("div");
|
|
56
|
+
wrapper.className = styles.wrapper;
|
|
57
|
+
wrapper.style.alignItems = textAlignmentToAlignItems(
|
|
58
|
+
block.props.textAlignment
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Button element that acts as a placeholder for images with no src.
|
|
62
|
+
const addImageButton = document.createElement("div");
|
|
63
|
+
addImageButton.className = styles.addImageButton;
|
|
64
|
+
addImageButton.style.display = block.props.url === "" ? "" : "none";
|
|
65
|
+
|
|
66
|
+
// Icon for the add image button.
|
|
67
|
+
const addImageButtonIcon = document.createElement("div");
|
|
68
|
+
addImageButtonIcon.className = styles.addImageButtonIcon;
|
|
69
|
+
|
|
70
|
+
// Text for the add image button.
|
|
71
|
+
const addImageButtonText = document.createElement("p");
|
|
72
|
+
addImageButtonText.className = styles.addImageButtonText;
|
|
73
|
+
addImageButtonText.innerText = "Add Image";
|
|
74
|
+
|
|
75
|
+
// Wrapper element for the image, resize handles and caption.
|
|
76
|
+
const imageAndCaptionWrapper = document.createElement("div");
|
|
77
|
+
imageAndCaptionWrapper.className = styles.imageAndCaptionWrapper;
|
|
78
|
+
imageAndCaptionWrapper.style.display = block.props.url !== "" ? "" : "none";
|
|
79
|
+
|
|
80
|
+
// Wrapper element for the image and resize handles.
|
|
81
|
+
const imageWrapper = document.createElement("div");
|
|
82
|
+
imageWrapper.className = styles.imageWrapper;
|
|
83
|
+
imageWrapper.style.display = block.props.url !== "" ? "" : "none";
|
|
84
|
+
|
|
85
|
+
// Image element.
|
|
86
|
+
const image = document.createElement("img");
|
|
87
|
+
image.className = styles.image;
|
|
88
|
+
image.src = block.props.url;
|
|
89
|
+
image.alt = "placeholder";
|
|
90
|
+
image.contentEditable = "false";
|
|
91
|
+
image.draggable = false;
|
|
92
|
+
image.style.width = `${Math.min(
|
|
93
|
+
block.props.width,
|
|
94
|
+
editor.domElement.firstElementChild!.clientWidth
|
|
95
|
+
)}px`;
|
|
96
|
+
|
|
97
|
+
// Resize handle elements.
|
|
98
|
+
const leftResizeHandle = document.createElement("div");
|
|
99
|
+
leftResizeHandle.className = styles.resizeHandle;
|
|
100
|
+
leftResizeHandle.style.left = "4px";
|
|
101
|
+
const rightResizeHandle = document.createElement("div");
|
|
102
|
+
rightResizeHandle.className = styles.resizeHandle;
|
|
103
|
+
rightResizeHandle.style.right = "4px";
|
|
104
|
+
|
|
105
|
+
// Caption element.
|
|
106
|
+
const caption = document.createElement("p");
|
|
107
|
+
caption.className = styles.caption;
|
|
108
|
+
caption.innerText = block.props.caption;
|
|
109
|
+
caption.style.padding = block.props.caption ? "4px" : "";
|
|
110
|
+
|
|
111
|
+
// Adds a light blue outline to selected image blocks.
|
|
112
|
+
const handleEditorUpdate = () => {
|
|
113
|
+
const selection = editor.getSelection()?.blocks || [];
|
|
114
|
+
const currentBlock = editor.getTextCursorPosition().block;
|
|
115
|
+
|
|
116
|
+
const isSelected =
|
|
117
|
+
[currentBlock, ...selection].find(
|
|
118
|
+
(selectedBlock) => selectedBlock.id === block.id
|
|
119
|
+
) !== undefined;
|
|
120
|
+
|
|
121
|
+
if (isSelected) {
|
|
122
|
+
addImageButton.style.outline = "4px solid rgb(100, 160, 255)";
|
|
123
|
+
imageAndCaptionWrapper.style.outline = "4px solid rgb(100, 160, 255)";
|
|
124
|
+
} else {
|
|
125
|
+
addImageButton.style.outline = "";
|
|
126
|
+
imageAndCaptionWrapper.style.outline = "";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
editor.onEditorContentChange(handleEditorUpdate);
|
|
130
|
+
editor.onEditorSelectionChange(handleEditorUpdate);
|
|
131
|
+
|
|
132
|
+
// Temporary parameters set when the user begins resizing the image, used to
|
|
133
|
+
// calculate the new width of the image.
|
|
134
|
+
let resizeParams:
|
|
135
|
+
| {
|
|
136
|
+
handleUsed: "left" | "right";
|
|
137
|
+
initialWidth: number;
|
|
138
|
+
initialClientX: number;
|
|
139
|
+
}
|
|
140
|
+
| undefined;
|
|
141
|
+
|
|
142
|
+
// Updates the image width with an updated width depending on the cursor X
|
|
143
|
+
// offset from when the resize began, and which resize handle is being used.
|
|
144
|
+
const windowMouseMoveHandler = (event: MouseEvent) => {
|
|
145
|
+
if (!resizeParams) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let newWidth: number;
|
|
150
|
+
|
|
151
|
+
if (textAlignmentToAlignItems(block.props.textAlignment) === "center") {
|
|
152
|
+
if (resizeParams.handleUsed === "left") {
|
|
153
|
+
newWidth =
|
|
154
|
+
resizeParams.initialWidth +
|
|
155
|
+
(resizeParams.initialClientX - event.clientX) * 2;
|
|
156
|
+
} else {
|
|
157
|
+
newWidth =
|
|
158
|
+
resizeParams.initialWidth +
|
|
159
|
+
(event.clientX - resizeParams.initialClientX) * 2;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
if (resizeParams.handleUsed === "left") {
|
|
163
|
+
newWidth =
|
|
164
|
+
resizeParams.initialWidth +
|
|
165
|
+
resizeParams.initialClientX -
|
|
166
|
+
event.clientX;
|
|
167
|
+
} else {
|
|
168
|
+
newWidth =
|
|
169
|
+
resizeParams.initialWidth +
|
|
170
|
+
event.clientX -
|
|
171
|
+
resizeParams.initialClientX;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Ensures the image is not wider than the editor and not smaller than a
|
|
176
|
+
// predetermined minimum width.
|
|
177
|
+
if (newWidth < minWidth) {
|
|
178
|
+
image.style.width = `${minWidth}px`;
|
|
179
|
+
} else if (newWidth > editor.domElement.firstElementChild!.clientWidth) {
|
|
180
|
+
image.style.width = `${
|
|
181
|
+
editor.domElement.firstElementChild!.clientWidth
|
|
182
|
+
}px`;
|
|
183
|
+
} else {
|
|
184
|
+
image.style.width = `${newWidth}px`;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
// Stops mouse movements from resizing the image and updates the block's
|
|
188
|
+
// `width` prop to the new value.
|
|
189
|
+
const windowMouseUpHandler = (event: MouseEvent) => {
|
|
190
|
+
if (!resizeParams) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Hides the drag handles if the cursor is no longer over the image.
|
|
195
|
+
if (
|
|
196
|
+
(!event.target || !imageWrapper.contains(event.target as Node)) &&
|
|
197
|
+
imageWrapper.contains(leftResizeHandle) &&
|
|
198
|
+
imageWrapper.contains(rightResizeHandle)
|
|
199
|
+
) {
|
|
200
|
+
leftResizeHandle.style.display = "none";
|
|
201
|
+
rightResizeHandle.style.display = "none";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
resizeParams = undefined;
|
|
205
|
+
|
|
206
|
+
editor.updateBlock(block, {
|
|
207
|
+
type: "image",
|
|
208
|
+
props: {
|
|
209
|
+
// Removes "px" from the end of the width string and converts to float.
|
|
210
|
+
width: parseFloat(image.style.width.slice(0, -2)),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Prevents focus from moving to the button.
|
|
216
|
+
const addImageButtonMouseDownHandler = (event: MouseEvent) => {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
};
|
|
219
|
+
// Opens the image toolbar.
|
|
220
|
+
const addImageButtonClickHandler = () => {
|
|
221
|
+
editor._tiptapEditor.view.dispatch(
|
|
222
|
+
editor._tiptapEditor.state.tr.setMeta(imageToolbarPluginKey, {
|
|
223
|
+
block: block,
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Sets the resize params, allowing the user to begin resizing the image by
|
|
229
|
+
// moving the cursor left or right.
|
|
230
|
+
const leftResizeHandleMouseDownHandler = (event: MouseEvent) => {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
|
|
233
|
+
leftResizeHandle.style.display = "block";
|
|
234
|
+
rightResizeHandle.style.display = "block";
|
|
235
|
+
|
|
236
|
+
resizeParams = {
|
|
237
|
+
handleUsed: "left",
|
|
238
|
+
initialWidth: block.props.width,
|
|
239
|
+
initialClientX: event.clientX,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const rightResizeHandleMouseDownHandler = (event: MouseEvent) => {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
|
|
245
|
+
leftResizeHandle.style.display = "block";
|
|
246
|
+
rightResizeHandle.style.display = "block";
|
|
247
|
+
|
|
248
|
+
resizeParams = {
|
|
249
|
+
handleUsed: "right",
|
|
250
|
+
initialWidth: block.props.width,
|
|
251
|
+
initialClientX: event.clientX,
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
wrapper.appendChild(addImageButton);
|
|
256
|
+
addImageButton.appendChild(addImageButtonIcon);
|
|
257
|
+
addImageButton.appendChild(addImageButtonText);
|
|
258
|
+
wrapper.appendChild(imageAndCaptionWrapper);
|
|
259
|
+
imageAndCaptionWrapper.appendChild(imageWrapper);
|
|
260
|
+
imageWrapper.appendChild(image);
|
|
261
|
+
imageWrapper.appendChild(leftResizeHandle);
|
|
262
|
+
imageWrapper.appendChild(rightResizeHandle);
|
|
263
|
+
imageAndCaptionWrapper.appendChild(caption);
|
|
264
|
+
|
|
265
|
+
window.addEventListener("mousemove", windowMouseMoveHandler);
|
|
266
|
+
window.addEventListener("mouseup", windowMouseUpHandler);
|
|
267
|
+
addImageButton.addEventListener("mousedown", addImageButtonMouseDownHandler);
|
|
268
|
+
addImageButton.addEventListener("click", addImageButtonClickHandler);
|
|
269
|
+
leftResizeHandle.addEventListener(
|
|
270
|
+
"mousedown",
|
|
271
|
+
leftResizeHandleMouseDownHandler
|
|
272
|
+
);
|
|
273
|
+
rightResizeHandle.addEventListener(
|
|
274
|
+
"mousedown",
|
|
275
|
+
rightResizeHandleMouseDownHandler
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
dom: wrapper,
|
|
280
|
+
destroy: () => {
|
|
281
|
+
window.removeEventListener("mousemove", windowMouseMoveHandler);
|
|
282
|
+
window.removeEventListener("mouseup", windowMouseUpHandler);
|
|
283
|
+
addImageButton.removeEventListener(
|
|
284
|
+
"mousedown",
|
|
285
|
+
addImageButtonMouseDownHandler
|
|
286
|
+
);
|
|
287
|
+
addImageButton.removeEventListener("click", addImageButtonClickHandler);
|
|
288
|
+
leftResizeHandle.removeEventListener(
|
|
289
|
+
"mousedown",
|
|
290
|
+
leftResizeHandleMouseDownHandler
|
|
291
|
+
);
|
|
292
|
+
rightResizeHandle.removeEventListener(
|
|
293
|
+
"mousedown",
|
|
294
|
+
rightResizeHandleMouseDownHandler
|
|
295
|
+
);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const Image = createBlockSpec({
|
|
301
|
+
type: "image",
|
|
302
|
+
propSchema: imagePropSchema,
|
|
303
|
+
containsInlineContent: false,
|
|
304
|
+
render: renderImage,
|
|
305
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const uploadToTmpFilesDotOrg_DEV_ONLY = async (file: File) => {
|
|
2
|
+
const body = new FormData();
|
|
3
|
+
body.append("file", file);
|
|
4
|
+
|
|
5
|
+
const ret = await fetch("https://tmpfiles.org/api/v1/upload", {
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: body,
|
|
8
|
+
});
|
|
9
|
+
return (await ret.json()).data.url.replace(
|
|
10
|
+
"tmpfiles.org/",
|
|
11
|
+
"tmpfiles.org/dl/"
|
|
12
|
+
);
|
|
13
|
+
};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { InputRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { defaultProps } from "../../../../api/defaultProps";
|
|
2
3
|
import { createTipTapBlock } from "../../../../api/block";
|
|
4
|
+
import { BlockSpec, PropSchema } from "../../../../api/blockTypes";
|
|
5
|
+
import { mergeCSSClasses } from "../../../../../../shared/utils";
|
|
3
6
|
import { handleEnter } from "../ListItemKeyboardShortcuts";
|
|
4
7
|
import styles from "../../../Block.module.css";
|
|
5
|
-
import { mergeCSSClasses } from "../../../../../../shared/utils";
|
|
6
8
|
|
|
7
|
-
export const
|
|
9
|
+
export const bulletListItemPropSchema = {
|
|
10
|
+
...defaultProps,
|
|
11
|
+
} satisfies PropSchema;
|
|
12
|
+
|
|
13
|
+
const BulletListItemBlockContent = createTipTapBlock<"bulletListItem", true>({
|
|
8
14
|
name: "bulletListItem",
|
|
9
15
|
content: "inline*",
|
|
10
16
|
|
|
@@ -29,6 +35,17 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
|
|
|
29
35
|
addKeyboardShortcuts() {
|
|
30
36
|
return {
|
|
31
37
|
Enter: () => handleEnter(this.editor),
|
|
38
|
+
"Mod-Shift-7": () =>
|
|
39
|
+
this.editor.commands.BNUpdateBlock<{
|
|
40
|
+
bulletListItem: BlockSpec<
|
|
41
|
+
"bulletListItem",
|
|
42
|
+
typeof bulletListItemPropSchema,
|
|
43
|
+
true
|
|
44
|
+
>;
|
|
45
|
+
}>(this.editor.state.selection.anchor, {
|
|
46
|
+
type: "bulletListItem",
|
|
47
|
+
props: {},
|
|
48
|
+
}),
|
|
32
49
|
};
|
|
33
50
|
},
|
|
34
51
|
|
|
@@ -112,3 +129,8 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
|
|
|
112
129
|
];
|
|
113
130
|
},
|
|
114
131
|
});
|
|
132
|
+
|
|
133
|
+
export const BulletListItem = {
|
|
134
|
+
node: BulletListItemBlockContent,
|
|
135
|
+
propSchema: bulletListItemPropSchema,
|
|
136
|
+
} satisfies BlockSpec<"bulletListItem", typeof bulletListItemPropSchema, true>;
|