@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.
Files changed (53) hide show
  1. package/dist/blocknote.js +2603 -2267
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +7 -7
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +2 -2
  7. package/src/BlockNoteEditor.ts +44 -12
  8. package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +21 -21
  9. package/src/api/blockManipulation/blockManipulation.test.ts +8 -11
  10. package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +3 -3
  11. package/src/api/formatConversions/formatConversions.test.ts +5 -5
  12. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +3 -3
  13. package/src/api/nodeConversions/nodeConversions.test.ts +10 -4
  14. package/src/api/nodeConversions/nodeConversions.ts +9 -7
  15. package/src/api/nodeConversions/testUtil.ts +3 -3
  16. package/src/editor.module.css +1 -1
  17. package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +5 -3
  18. package/src/extensions/BackgroundColor/BackgroundColorMark.ts +2 -1
  19. package/src/extensions/Blocks/NonEditableBlockPlugin.ts +17 -0
  20. package/src/extensions/Blocks/api/block.ts +62 -17
  21. package/src/extensions/Blocks/api/blockTypes.ts +79 -27
  22. package/src/extensions/Blocks/api/defaultBlocks.ts +13 -41
  23. package/src/extensions/Blocks/api/defaultProps.ts +16 -0
  24. package/src/extensions/Blocks/nodes/Block.module.css +78 -24
  25. package/src/extensions/Blocks/nodes/BlockContainer.ts +66 -42
  26. package/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +59 -13
  27. package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +305 -0
  28. package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +13 -0
  29. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +24 -2
  30. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +146 -120
  31. package/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +12 -2
  32. package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +239 -0
  33. package/src/extensions/SlashMenu/defaultSlashMenuItems.ts +47 -6
  34. package/src/extensions/TextColor/TextColorExtension.ts +4 -3
  35. package/src/extensions/TextColor/TextColorMark.ts +2 -1
  36. package/src/index.ts +4 -0
  37. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +4 -0
  38. package/types/src/BlockNoteEditor.d.ts +9 -0
  39. package/types/src/BlockNoteExtensions.d.ts +1 -1
  40. package/types/src/extensions/Blocks/api/block.d.ts +7 -8
  41. package/types/src/extensions/Blocks/api/blockTypes.d.ts +29 -20
  42. package/types/src/extensions/Blocks/api/defaultBlocks.d.ts +55 -51
  43. package/types/src/extensions/Blocks/api/defaultProps.d.ts +2 -2
  44. package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.d.ts +43 -9
  45. package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.d.ts +2 -2
  46. package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +1 -0
  47. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.d.ts +35 -9
  48. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.d.ts +35 -9
  49. package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.d.ts +36 -1
  50. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +1 -1
  51. package/types/src/index.d.ts +4 -0
  52. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +1 -1
  53. 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
- state.tr.replaceWith(
209
- startPos,
210
- endPos,
211
- state.schema.nodes[newType].create({
212
- ...contentNode.attrs,
213
- ...block.props,
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, false)
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 styles from "../../Block.module.css";
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
- export const HeadingBlockContent = createTipTapBlock<"heading">({
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: "1",
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
- ...["1", "2", "3"].map((level) => {
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(`^(#{${parseInt(level)}})\\s$`),
37
+ find: new RegExp(`^(#{${level}})\\s$`),
31
38
  handler: ({ state, chain, range }) => {
32
39
  chain()
33
- .BNUpdateBlock(state.selection.from, {
40
+ .BNUpdateBlock<{
41
+ heading: BlockSpec<"heading", typeof headingPropSchema, true>;
42
+ }>(state.selection.from, {
34
43
  type: "heading",
35
44
  props: {
36
- level: level as "1" | "2" | "3",
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: "1" },
92
+ attrs: { level: 1 },
52
93
  node: "heading",
53
94
  },
54
95
  {
55
96
  tag: "h2",
56
- attrs: { level: "2" },
97
+ attrs: { level: 2 },
57
98
  node: "heading",
58
99
  },
59
100
  {
60
101
  tag: "h3",
61
- attrs: { level: "3" },
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
- "h" + node.attrs.level,
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 BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
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>;