@blocknote/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.js +12508 -1276
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +50 -1
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +16 -5
- package/src/BlockNoteEditor.test.ts +12 -0
- package/src/BlockNoteEditor.ts +38 -15
- package/src/BlockNoteExtensions.ts +25 -21
- package/src/api/Editor.ts +142 -0
- package/src/api/blockManipulation/blockManipulation.ts +114 -0
- package/src/api/formatConversions/formatConversions.ts +86 -0
- package/src/api/formatConversions/removeUnderlinesRehypePlugin.ts +39 -0
- package/src/api/formatConversions/simplifyBlocksRehypePlugin.ts +125 -0
- package/src/api/nodeConversions/nodeConversions.ts +170 -0
- package/src/editor.module.css +7 -1
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +7 -1
- package/src/extensions/Blocks/api/blockTypes.ts +85 -0
- package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -0
- package/src/extensions/Blocks/api/inlineContentTypes.ts +44 -0
- package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +4 -4
- package/src/extensions/Blocks/nodes/BlockContainer.ts +75 -25
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +23 -5
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +28 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -2
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +3 -3
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +7 -12
- package/src/extensions/SlashMenu/SlashMenuItem.ts +4 -1
- package/src/extensions/SlashMenu/{defaultCommands.tsx → defaultSlashCommands.tsx} +34 -17
- package/src/extensions/SlashMenu/index.ts +7 -4
- package/src/extensions/UniqueID/UniqueID.ts +1 -1
- package/src/index.ts +4 -2
- package/types/src/BlockNoteEditor.d.ts +13 -4
- package/types/src/BlockNoteEditor.test.d.ts +1 -0
- package/types/src/BlockNoteExtensions.d.ts +7 -3
- package/types/src/api/Editor.d.ts +73 -0
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -0
- package/types/src/api/formatConversions/formatConversions.d.ts +6 -0
- package/types/src/api/formatConversions/removeUnderlinesRehypePlugin.d.ts +6 -0
- package/types/src/api/formatConversions/simplifyBlocksRehypePlugin.d.ts +16 -0
- package/types/src/api/nodeConversions/nodeConversions.d.ts +8 -0
- package/types/src/api/removeUnderlinesRehypePlugin.d.ts +6 -0
- package/types/src/api/simplifyBlocksRehypePlugin.d.ts +16 -0
- package/types/src/extensions/Blocks/api/apiTypes.d.ts +18 -0
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +36 -0
- package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +4 -0
- package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +23 -0
- package/types/src/extensions/Blocks/api/styleTypes.d.ts +22 -0
- package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -2
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -3
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -1
- package/types/src/extensions/SlashMenu/index.d.ts +3 -3
- package/types/src/index.d.ts +4 -2
- package/src/extensions/Blocks/apiTypes.ts +0 -48
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Element as HASTElement, Parent as HASTParent } from "hast";
|
|
2
|
+
import { fromDom } from "hast-util-from-dom";
|
|
3
|
+
|
|
4
|
+
type SimplifyBlocksOptions = {
|
|
5
|
+
orderedListItemBlockTypes: Set<string>;
|
|
6
|
+
unorderedListItemBlockTypes: Set<string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rehype plugin which converts the HTML output string rendered by BlockNote into a simplified structure which better
|
|
11
|
+
* follows HTML standards. It does several things:
|
|
12
|
+
* - Removes all block related div elements, leaving only the actual content inside the block.
|
|
13
|
+
* - Lifts nested blocks to a higher level for all block types that don't represent list items.
|
|
14
|
+
* - Wraps blocks which represent list items in corresponding ul/ol HTML elements and restructures them to comply
|
|
15
|
+
* with HTML list structure.
|
|
16
|
+
* @param options Options for specifying which block types represent ordered and unordered list items.
|
|
17
|
+
*/
|
|
18
|
+
export function simplifyBlocks(options: SimplifyBlocksOptions) {
|
|
19
|
+
const listItemBlockTypes = new Set<string>([
|
|
20
|
+
...options.orderedListItemBlockTypes,
|
|
21
|
+
...options.unorderedListItemBlockTypes,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const simplifyBlocksHelper = (tree: HASTParent) => {
|
|
25
|
+
let numChildElements = tree.children.length;
|
|
26
|
+
let activeList: HASTElement | undefined;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < numChildElements; i++) {
|
|
29
|
+
const blockOuter = tree.children[i] as HASTElement;
|
|
30
|
+
const blockContainer = blockOuter.children[0] as HASTElement;
|
|
31
|
+
const blockContent = blockContainer.children[0] as HASTElement;
|
|
32
|
+
const blockGroup =
|
|
33
|
+
blockContainer.children.length === 2
|
|
34
|
+
? (blockContainer.children[1] as HASTElement)
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
const isListItemBlock = listItemBlockTypes.has(
|
|
38
|
+
blockContent.properties!["dataContentType"] as string
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const listItemBlockType = isListItemBlock
|
|
42
|
+
? options.orderedListItemBlockTypes.has(
|
|
43
|
+
blockContent.properties!["dataContentType"] as string
|
|
44
|
+
)
|
|
45
|
+
? "ol"
|
|
46
|
+
: "ul"
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
// Plugin runs recursively to process nested blocks.
|
|
50
|
+
if (blockGroup !== null) {
|
|
51
|
+
simplifyBlocksHelper(blockGroup);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checks that there is an active list, but the block can't be added to it as it's of a different type.
|
|
55
|
+
if (activeList && activeList.tagName !== listItemBlockType) {
|
|
56
|
+
// Blocks that were copied into the list are removed and the list is inserted in their place.
|
|
57
|
+
tree.children.splice(
|
|
58
|
+
i - activeList.children.length,
|
|
59
|
+
activeList.children.length,
|
|
60
|
+
activeList
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Updates the current index and number of child elements.
|
|
64
|
+
const numElementsRemoved = activeList.children.length - 1;
|
|
65
|
+
i -= numElementsRemoved;
|
|
66
|
+
numChildElements -= numElementsRemoved;
|
|
67
|
+
|
|
68
|
+
activeList = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Checks if the block represents a list item.
|
|
72
|
+
if (isListItemBlock) {
|
|
73
|
+
// Checks if a list isn't already active. We don't have to check if the block and the list are of the same
|
|
74
|
+
// type as this was already done earlier.
|
|
75
|
+
if (!activeList) {
|
|
76
|
+
// Creates a new list element to represent an active list.
|
|
77
|
+
activeList = fromDom(
|
|
78
|
+
document.createElement(listItemBlockType!)
|
|
79
|
+
) as HASTElement;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Creates a new list item element to represent the block.
|
|
83
|
+
const listItemElement = fromDom(
|
|
84
|
+
document.createElement("li")
|
|
85
|
+
) as HASTElement;
|
|
86
|
+
|
|
87
|
+
// Adds only the content inside the block to the active list.
|
|
88
|
+
listItemElement.children.push(blockContent.children[0]);
|
|
89
|
+
// Nested blocks have already been processed in the recursive function call, so the resulting elements are
|
|
90
|
+
// also added to the active list.
|
|
91
|
+
if (blockGroup !== null) {
|
|
92
|
+
listItemElement.children.push(...blockGroup.children);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Adds the list item representing the block to the active list.
|
|
96
|
+
activeList.children.push(listItemElement);
|
|
97
|
+
} else if (blockGroup !== null) {
|
|
98
|
+
// Lifts all children out of the current block, as only list items should allow nesting.
|
|
99
|
+
tree.children.splice(i + 1, 0, ...blockGroup.children);
|
|
100
|
+
// Replaces the block with only the content inside it.
|
|
101
|
+
tree.children[i] = blockContent.children[0];
|
|
102
|
+
|
|
103
|
+
// Updates the current index and number of child elements.
|
|
104
|
+
const numElementsAdded = blockGroup.children.length;
|
|
105
|
+
i += numElementsAdded;
|
|
106
|
+
numChildElements += numElementsAdded;
|
|
107
|
+
} else {
|
|
108
|
+
// Replaces the block with only the content inside it.
|
|
109
|
+
tree.children[i] = blockContent.children[0];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Since the active list is only inserted after encountering a block which can't be added to it, there are cases
|
|
114
|
+
// where it remains un-inserted after processing all blocks, which are handled here.
|
|
115
|
+
if (activeList) {
|
|
116
|
+
tree.children.splice(
|
|
117
|
+
numChildElements - activeList.children.length,
|
|
118
|
+
activeList.children.length,
|
|
119
|
+
activeList
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return simplifyBlocksHelper;
|
|
125
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Node, Schema } from "prosemirror-model";
|
|
2
|
+
import {
|
|
3
|
+
Block,
|
|
4
|
+
blockProps,
|
|
5
|
+
PartialBlock,
|
|
6
|
+
} from "../../extensions/Blocks/api/blockTypes";
|
|
7
|
+
import {
|
|
8
|
+
InlineContent,
|
|
9
|
+
Style,
|
|
10
|
+
} from "../../extensions/Blocks/api/inlineContentTypes";
|
|
11
|
+
import { getBlockInfoFromPos } from "../../extensions/Blocks/helpers/getBlockInfoFromPos";
|
|
12
|
+
import UniqueID from "../../extensions/UniqueID/UniqueID";
|
|
13
|
+
|
|
14
|
+
export function blockToNode(block: PartialBlock, schema: Schema) {
|
|
15
|
+
let id = block.id;
|
|
16
|
+
|
|
17
|
+
if (id === undefined) {
|
|
18
|
+
id = UniqueID.options.generateID();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let content: Node[] = [];
|
|
22
|
+
|
|
23
|
+
if (typeof block.content === "string") {
|
|
24
|
+
content.push(schema.text(block.content));
|
|
25
|
+
} else if (typeof block.content === "object") {
|
|
26
|
+
for (const styledText of block.content) {
|
|
27
|
+
const marks = [];
|
|
28
|
+
|
|
29
|
+
for (const style of styledText.styles) {
|
|
30
|
+
marks.push(schema.mark(style.type, style.props));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
content.push(schema.text(styledText.text, marks));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const contentNode = schema.nodes[block.type].create(block.props, content);
|
|
38
|
+
|
|
39
|
+
const children: Node[] = [];
|
|
40
|
+
|
|
41
|
+
if (block.children) {
|
|
42
|
+
for (const child of block.children) {
|
|
43
|
+
children.push(blockToNode(child, schema));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const groupNode = schema.nodes["blockGroup"].create({}, children);
|
|
48
|
+
|
|
49
|
+
return schema.nodes["blockContainer"].create(
|
|
50
|
+
{
|
|
51
|
+
id: id,
|
|
52
|
+
...block.props,
|
|
53
|
+
},
|
|
54
|
+
children.length > 0 ? [contentNode, groupNode] : contentNode
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getNodeById(
|
|
59
|
+
id: string,
|
|
60
|
+
doc: Node
|
|
61
|
+
): { node: Node; posBeforeNode: number } {
|
|
62
|
+
let targetNode: Node | undefined = undefined;
|
|
63
|
+
let posBeforeNode: number | undefined = undefined;
|
|
64
|
+
|
|
65
|
+
doc.firstChild!.descendants((node, pos) => {
|
|
66
|
+
// Skips traversing nodes after node with target ID has been found.
|
|
67
|
+
if (targetNode) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Keeps traversing nodes if block with target ID has not been found.
|
|
72
|
+
if (node.type.name !== "blockContainer" || node.attrs.id !== id) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
targetNode = node;
|
|
77
|
+
posBeforeNode = pos + 1;
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (targetNode === undefined || posBeforeNode === undefined) {
|
|
83
|
+
throw Error("Could not find block in the editor with matching ID.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
node: targetNode,
|
|
88
|
+
posBeforeNode: posBeforeNode,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function nodeToBlock(
|
|
93
|
+
node: Node,
|
|
94
|
+
blockCache?: WeakMap<Node, Block>
|
|
95
|
+
): Block {
|
|
96
|
+
if (node.type.name !== "blockContainer") {
|
|
97
|
+
throw Error(
|
|
98
|
+
"Node must be of type blockContainer, but is of type" +
|
|
99
|
+
node.type.name +
|
|
100
|
+
"."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cachedBlock = blockCache?.get(node);
|
|
105
|
+
|
|
106
|
+
if (cachedBlock) {
|
|
107
|
+
return cachedBlock;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const blockInfo = getBlockInfoFromPos(node, 0)!;
|
|
111
|
+
|
|
112
|
+
let id = blockInfo.id;
|
|
113
|
+
|
|
114
|
+
// Only used for blocks converted from other formats.
|
|
115
|
+
if (id === null) {
|
|
116
|
+
id = UniqueID.options.generateID();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const props: any = {};
|
|
120
|
+
for (const [attr, value] of Object.entries({
|
|
121
|
+
...blockInfo.node.attrs,
|
|
122
|
+
...blockInfo.contentNode.attrs,
|
|
123
|
+
})) {
|
|
124
|
+
if (!(blockInfo.contentType.name in blockProps)) {
|
|
125
|
+
throw Error(
|
|
126
|
+
"Block is of an unrecognized type: " + blockInfo.contentType.name
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const validAttrs = blockProps[blockInfo.contentType.name as Block["type"]];
|
|
131
|
+
|
|
132
|
+
if (validAttrs.has(attr)) {
|
|
133
|
+
props[attr] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const content: InlineContent[] = [];
|
|
138
|
+
blockInfo.contentNode.content.forEach((node) => {
|
|
139
|
+
const styles: Style[] = [];
|
|
140
|
+
|
|
141
|
+
for (const mark of node.marks) {
|
|
142
|
+
styles.push({
|
|
143
|
+
type: mark.type.name,
|
|
144
|
+
props: mark.attrs,
|
|
145
|
+
} as Style);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
content.push({
|
|
149
|
+
text: node.textContent,
|
|
150
|
+
styles,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const children: Block[] = [];
|
|
155
|
+
for (let i = 0; i < blockInfo.numChildBlocks; i++) {
|
|
156
|
+
children.push(nodeToBlock(blockInfo.node.lastChild!.child(i)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const block: Block = {
|
|
160
|
+
id,
|
|
161
|
+
type: blockInfo.contentType.name as Block["type"],
|
|
162
|
+
props,
|
|
163
|
+
content,
|
|
164
|
+
children,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
blockCache?.set(node, block);
|
|
168
|
+
|
|
169
|
+
return block;
|
|
170
|
+
}
|
package/src/editor.module.css
CHANGED
|
@@ -24,7 +24,8 @@ Tippy popups that are appended to document.body directly
|
|
|
24
24
|
box-sizing: inherit;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
.bnEditor,
|
|
27
|
+
.bnEditor,
|
|
28
|
+
.dragPreview {
|
|
28
29
|
/* Define a set of colors to be used throughout the app for consistency
|
|
29
30
|
see https://atlassian.design/foundations/color for more info */
|
|
30
31
|
--N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
|
|
@@ -38,3 +39,8 @@ Tippy popups that are appended to document.body directly
|
|
|
38
39
|
|
|
39
40
|
color: rgb(60, 65, 73);
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
.dragPreview {
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: -1000px;
|
|
46
|
+
}
|
|
@@ -24,6 +24,7 @@ const nodeAttributes: Record<string, string> = {
|
|
|
24
24
|
* Solution: When attributes change on a node, this plugin sets a data-* attribute with the "previous" value. This way we can still use CSS transitions. (See block.module.css)
|
|
25
25
|
*/
|
|
26
26
|
export const PreviousBlockTypePlugin = () => {
|
|
27
|
+
let timeout: any;
|
|
27
28
|
return new Plugin({
|
|
28
29
|
key: PLUGIN_KEY,
|
|
29
30
|
view(_editorView) {
|
|
@@ -32,13 +33,18 @@ export const PreviousBlockTypePlugin = () => {
|
|
|
32
33
|
if (this.key?.getState(view.state).updatedBlocks.size > 0) {
|
|
33
34
|
// use setTimeout 0 to clear the decorations so that at least
|
|
34
35
|
// for one DOM-render the decorations have been applied
|
|
35
|
-
setTimeout(() => {
|
|
36
|
+
timeout = setTimeout(() => {
|
|
36
37
|
view.dispatch(
|
|
37
38
|
view.state.tr.setMeta(PLUGIN_KEY, { clearUpdate: true })
|
|
38
39
|
);
|
|
39
40
|
}, 0);
|
|
40
41
|
}
|
|
41
42
|
},
|
|
43
|
+
destroy: () => {
|
|
44
|
+
if (timeout) {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
42
48
|
};
|
|
43
49
|
},
|
|
44
50
|
state: {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Define the main block types **/
|
|
2
|
+
|
|
3
|
+
import { InlineContent } from "./inlineContentTypes";
|
|
4
|
+
|
|
5
|
+
export type BlockTemplate<
|
|
6
|
+
// Type of the block.
|
|
7
|
+
// Examples might include: "paragraph", "heading", or "bulletListItem".
|
|
8
|
+
Type extends string,
|
|
9
|
+
// Changeable props which affect the block's behaviour or appearance.
|
|
10
|
+
// An example might be: { textAlignment: "left" | "right" | "center" } for a paragraph block.
|
|
11
|
+
Props extends Record<string, string>
|
|
12
|
+
> = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: Type;
|
|
15
|
+
props: Props;
|
|
16
|
+
content: InlineContent[];
|
|
17
|
+
children: Block[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type GlobalProps = {
|
|
21
|
+
backgroundColor: string;
|
|
22
|
+
textColor: string;
|
|
23
|
+
textAlignment: "left" | "center" | "right" | "justify";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type NumberedListItemBlock = BlockTemplate<
|
|
27
|
+
"numberedListItem",
|
|
28
|
+
GlobalProps
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export type BulletListItemBlock = BlockTemplate<"bulletListItem", GlobalProps>;
|
|
32
|
+
|
|
33
|
+
export type HeadingBlock = BlockTemplate<
|
|
34
|
+
"heading",
|
|
35
|
+
GlobalProps & {
|
|
36
|
+
level: "1" | "2" | "3";
|
|
37
|
+
}
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export type ParagraphBlock = BlockTemplate<"paragraph", GlobalProps>;
|
|
41
|
+
|
|
42
|
+
export type Block =
|
|
43
|
+
| ParagraphBlock
|
|
44
|
+
| HeadingBlock
|
|
45
|
+
| BulletListItemBlock
|
|
46
|
+
| NumberedListItemBlock;
|
|
47
|
+
|
|
48
|
+
/** Define "Partial Blocks", these are for updating or creating blocks */
|
|
49
|
+
export type PartialBlockTemplate<B extends Block> = B extends Block
|
|
50
|
+
? Partial<Omit<B, "props" | "children" | "content" | "type">> & {
|
|
51
|
+
type: B["type"];
|
|
52
|
+
props?: Partial<B["props"]>;
|
|
53
|
+
content?: string | B["content"];
|
|
54
|
+
children?: PartialBlock[];
|
|
55
|
+
}
|
|
56
|
+
: never;
|
|
57
|
+
|
|
58
|
+
export type PartialBlock = PartialBlockTemplate<Block>;
|
|
59
|
+
|
|
60
|
+
export type BlockPropsTemplate<Props> = Props extends Block["props"]
|
|
61
|
+
? keyof Props
|
|
62
|
+
: never;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Expose blockProps. This is currently not very nice, but it's expected this
|
|
66
|
+
* will change anyway once we allow for custom blocks
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
export const globalProps: Array<keyof GlobalProps> = [
|
|
70
|
+
"backgroundColor",
|
|
71
|
+
"textColor",
|
|
72
|
+
"textAlignment",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const blockProps: Record<Block["type"], Set<string>> = {
|
|
76
|
+
paragraph: new Set<keyof ParagraphBlock["props"]>([...globalProps]),
|
|
77
|
+
heading: new Set<keyof HeadingBlock["props"]>([
|
|
78
|
+
...globalProps,
|
|
79
|
+
"level" as const,
|
|
80
|
+
]),
|
|
81
|
+
numberedListItem: new Set<keyof NumberedListItemBlock["props"]>([
|
|
82
|
+
...globalProps,
|
|
83
|
+
]),
|
|
84
|
+
bulletListItem: new Set<keyof BulletListItemBlock["props"]>([...globalProps]),
|
|
85
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type StyleTemplate<
|
|
2
|
+
// Type of the style.
|
|
3
|
+
// Examples might include: "bold", "italic", or "textColor".
|
|
4
|
+
Type extends string,
|
|
5
|
+
// Changeable props which affect the style's appearance.
|
|
6
|
+
// An example might be: { color: string } for a textColor style.
|
|
7
|
+
Props extends Record<string, string>
|
|
8
|
+
> = {
|
|
9
|
+
type: Type;
|
|
10
|
+
props: Props;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Bold = StyleTemplate<"bold", {}>;
|
|
14
|
+
|
|
15
|
+
export type Italic = StyleTemplate<"italic", {}>;
|
|
16
|
+
|
|
17
|
+
export type Underline = StyleTemplate<"underline", {}>;
|
|
18
|
+
|
|
19
|
+
export type Strikethrough = StyleTemplate<"strike", {}>;
|
|
20
|
+
|
|
21
|
+
export type TextColor = StyleTemplate<"textColor", { color: string }>;
|
|
22
|
+
|
|
23
|
+
export type BackgroundColor = StyleTemplate<
|
|
24
|
+
"backgroundColor",
|
|
25
|
+
{ color: string }
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
export type Link = StyleTemplate<"link", { href: string }>;
|
|
29
|
+
|
|
30
|
+
export type Style =
|
|
31
|
+
| Bold
|
|
32
|
+
| Italic
|
|
33
|
+
| Underline
|
|
34
|
+
| Strikethrough
|
|
35
|
+
| TextColor
|
|
36
|
+
| BackgroundColor
|
|
37
|
+
| Link;
|
|
38
|
+
|
|
39
|
+
export type StyledText = {
|
|
40
|
+
text: string;
|
|
41
|
+
styles: Style[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type InlineContent = StyledText;
|
|
@@ -22,7 +22,7 @@ export function getBlockInfoFromPos(
|
|
|
22
22
|
doc: Node,
|
|
23
23
|
posInBlock: number
|
|
24
24
|
): BlockInfo | undefined {
|
|
25
|
-
if (posInBlock
|
|
25
|
+
if (posInBlock < 0 || posInBlock > doc.nodeSize) {
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,11 +32,11 @@ export function getBlockInfoFromPos(
|
|
|
32
32
|
let node = $pos.node(maxDepth);
|
|
33
33
|
let depth = maxDepth;
|
|
34
34
|
|
|
35
|
-
while (
|
|
36
|
-
|
|
37
|
-
if (depth === 0) {
|
|
35
|
+
while (true) {
|
|
36
|
+
if (depth < 0) {
|
|
38
37
|
return undefined;
|
|
39
38
|
}
|
|
39
|
+
|
|
40
40
|
if (node.type.name === "blockContainer") {
|
|
41
41
|
break;
|
|
42
42
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
-
import { Fragment, Slice } from "prosemirror-model";
|
|
2
|
+
import { Fragment, Node as PMNode, Slice } from "prosemirror-model";
|
|
3
3
|
import { TextSelection } from "prosemirror-state";
|
|
4
|
-
import {
|
|
4
|
+
import { blockToNode } from "../../../api/nodeConversions/nodeConversions";
|
|
5
|
+
import { PartialBlock } from "../api/blockTypes";
|
|
5
6
|
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
|
|
6
7
|
import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
|
|
7
8
|
import styles from "./Block.module.css";
|
|
@@ -19,13 +20,10 @@ declare module "@tiptap/core" {
|
|
|
19
20
|
BNDeleteBlock: (posInBlock: number) => ReturnType;
|
|
20
21
|
BNMergeBlocks: (posBetweenBlocks: number) => ReturnType;
|
|
21
22
|
BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType;
|
|
22
|
-
BNUpdateBlock: (
|
|
23
|
-
posInBlock: number,
|
|
24
|
-
blockUpdate: BlockUpdate
|
|
25
|
-
) => ReturnType;
|
|
23
|
+
BNUpdateBlock: (posInBlock: number, block: PartialBlock) => ReturnType;
|
|
26
24
|
BNCreateOrUpdateBlock: (
|
|
27
25
|
posInBlock: number,
|
|
28
|
-
|
|
26
|
+
block: PartialBlock
|
|
29
27
|
) => ReturnType;
|
|
30
28
|
};
|
|
31
29
|
}
|
|
@@ -197,8 +195,7 @@ export const BlockContainer = Node.create<IBlock>({
|
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
// Deletes next block and adds its text content to the nearest previous block.
|
|
200
|
-
// TODO:
|
|
201
|
-
// is trickier.
|
|
198
|
+
// TODO: Use slices.
|
|
202
199
|
if (dispatch) {
|
|
203
200
|
state.tr.deleteRange(startPos, startPos + contentNode.nodeSize);
|
|
204
201
|
state.tr.insertText(contentNode.textContent, prevBlockEndPos - 1);
|
|
@@ -283,35 +280,88 @@ export const BlockContainer = Node.create<IBlock>({
|
|
|
283
280
|
|
|
284
281
|
return true;
|
|
285
282
|
},
|
|
286
|
-
// Updates
|
|
283
|
+
// Updates a block to the given specification.
|
|
287
284
|
BNUpdateBlock:
|
|
288
|
-
(posInBlock,
|
|
285
|
+
(posInBlock, block) =>
|
|
289
286
|
({ state, dispatch }) => {
|
|
290
287
|
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
|
|
291
288
|
if (blockInfo === undefined) {
|
|
292
289
|
return false;
|
|
293
290
|
}
|
|
294
291
|
|
|
295
|
-
const {
|
|
292
|
+
const { startPos, endPos, node, contentNode } = blockInfo;
|
|
296
293
|
|
|
297
294
|
if (dispatch) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
295
|
+
// Adds blockGroup node with child blocks if necessary.
|
|
296
|
+
if (block.children !== undefined) {
|
|
297
|
+
const childNodes = [];
|
|
298
|
+
|
|
299
|
+
// Creates ProseMirror nodes for each child block, including their descendants.
|
|
300
|
+
for (const child of block.children) {
|
|
301
|
+
childNodes.push(blockToNode(child, state.schema));
|
|
305
302
|
}
|
|
306
|
-
|
|
303
|
+
|
|
304
|
+
// Checks if a blockGroup node already exists.
|
|
305
|
+
if (node.childCount === 2) {
|
|
306
|
+
// Replaces all child nodes in the existing blockGroup with the ones created earlier.
|
|
307
|
+
state.tr.replace(
|
|
308
|
+
startPos + contentNode.nodeSize + 1,
|
|
309
|
+
endPos - 1,
|
|
310
|
+
new Slice(Fragment.from(childNodes), 0, 0)
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
// Inserts a new blockGroup containing the child nodes created earlier.
|
|
314
|
+
state.tr.insert(
|
|
315
|
+
startPos + contentNode.nodeSize,
|
|
316
|
+
state.schema.nodes["blockGroup"].create({}, childNodes)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Replaces the blockContent node's content if necessary.
|
|
322
|
+
if (block.content !== undefined) {
|
|
323
|
+
let content: PMNode[] = [];
|
|
324
|
+
|
|
325
|
+
// Checks if the provided content is a string or InlineContent[] type.
|
|
326
|
+
if (typeof block.content === "string") {
|
|
327
|
+
// Adds a single text node with no marks to the content.
|
|
328
|
+
content.push(state.schema.text(block.content));
|
|
329
|
+
} else {
|
|
330
|
+
// Adds a text node with the provided styles converted into marks to the content, for each InlineContent
|
|
331
|
+
// object.
|
|
332
|
+
for (const styledText of block.content) {
|
|
333
|
+
const marks = [];
|
|
334
|
+
|
|
335
|
+
for (const style of styledText.styles) {
|
|
336
|
+
marks.push(state.schema.mark(style.type, style.props));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
content.push(state.schema.text(styledText.text, marks));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Replaces the contents of the blockContent node with the previously created text node(s).
|
|
344
|
+
state.tr.replace(
|
|
345
|
+
startPos + 1,
|
|
346
|
+
startPos + contentNode.nodeSize - 1,
|
|
347
|
+
new Slice(Fragment.from(content), 0, 0)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Changes the block type and adds the provided props as node attributes. Also preserves all existing node
|
|
352
|
+
// attributes that are compatible with the new type.
|
|
353
|
+
state.tr.setNodeMarkup(startPos, state.schema.nodes[block.type], {
|
|
354
|
+
...contentNode.attrs,
|
|
355
|
+
...block.props,
|
|
356
|
+
});
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
return true;
|
|
310
360
|
},
|
|
311
|
-
//
|
|
312
|
-
//
|
|
361
|
+
// Updates a block to the given specification if it's empty, otherwise creates a new block from that specification
|
|
362
|
+
// below it.
|
|
313
363
|
BNCreateOrUpdateBlock:
|
|
314
|
-
(posInBlock,
|
|
364
|
+
(posInBlock, block) =>
|
|
315
365
|
({ state, chain }) => {
|
|
316
366
|
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
|
|
317
367
|
if (blockInfo === undefined) {
|
|
@@ -324,7 +374,7 @@ export const BlockContainer = Node.create<IBlock>({
|
|
|
324
374
|
const oldBlockContentPos = startPos + 1;
|
|
325
375
|
|
|
326
376
|
return chain()
|
|
327
|
-
.BNUpdateBlock(posInBlock,
|
|
377
|
+
.BNUpdateBlock(posInBlock, block)
|
|
328
378
|
.setTextSelection(oldBlockContentPos)
|
|
329
379
|
.run();
|
|
330
380
|
} else {
|
|
@@ -333,7 +383,7 @@ export const BlockContainer = Node.create<IBlock>({
|
|
|
333
383
|
|
|
334
384
|
return chain()
|
|
335
385
|
.BNCreateBlock(newBlockInsertionPos)
|
|
336
|
-
.BNUpdateBlock(newBlockContentPos,
|
|
386
|
+
.BNUpdateBlock(newBlockContentPos, block)
|
|
337
387
|
.setTextSelection(newBlockContentPos)
|
|
338
388
|
.run();
|
|
339
389
|
}
|