Skip to content

Commit 5adf609

Browse files
feat(core): support h4, h5, and h6 (#1634)
Co-authored-by: Nick the Sick <[email protected]>
1 parent 788c5b8 commit 5adf609

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+971
-224
lines changed

docs/pages/docs/editor-basics/setup.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ type BlockNoteEditorOptions = {
3232
width?: number;
3333
class?: string;
3434
}) => Plugin;
35+
heading?: {
36+
levels?: number[];
37+
};
3538
initialContent?: PartialBlock[];
3639
pasteHandler?: (context: {
3740
event: ClipboardEvent;
@@ -73,6 +76,8 @@ The hook takes two optional parameters:
7376

7477
`initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).
7578

79+
`heading`: Configuration for headings. Allows you to configure the number of levels of headings that should be available in the editor. Defaults to `[1, 2, 3]`. Configurable up to 6 levels of headings.
80+
7681
`pasteHandler`: A function that can be used to override the default paste behavior. See [Paste Handling](/docs/advanced/paste-handling) for more.
7782

7883
`resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.

packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts

Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import {
1111
import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js";
1212
import { defaultProps } from "../defaultProps.js";
1313
import { createToggleWrapper } from "../ToggleWrapper/createToggleWrapper.js";
14+
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
15+
16+
const HEADING_LEVELS = [1, 2, 3, 4, 5, 6] as const;
1417

1518
export const headingPropSchema = {
1619
...defaultProps,
17-
level: { default: 1, values: [1, 2, 3] as const },
20+
level: { default: 1, values: HEADING_LEVELS },
1821
isToggleable: { default: false },
1922
} satisfies PropSchema;
2023

@@ -28,8 +31,9 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({
2831
},
2932

3033
addInputRules() {
34+
const editor = this.options.editor as BlockNoteEditor<any, any, any>;
3135
return [
32-
...[1, 2, 3].map((level) => {
36+
...editor.settings.heading.levels.map((level) => {
3337
// Creates a heading of appropriate level when starting with "#", "##", or "###".
3438
return new InputRule({
3539
find: new RegExp(`^(#{${level}})\\s$`),
@@ -61,87 +65,46 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({
6165
},
6266

6367
addKeyboardShortcuts() {
64-
return {
65-
"Mod-Alt-1": () => {
66-
const blockInfo = getBlockInfoFromSelection(this.editor.state);
67-
if (
68-
!blockInfo.isBlockContainer ||
69-
blockInfo.blockContent.node.type.spec.content !== "inline*"
70-
) {
71-
return true;
72-
}
73-
74-
// call updateBlockCommand
75-
return this.editor.commands.command(
76-
updateBlockCommand(blockInfo.bnBlock.beforePos, {
77-
type: "heading",
78-
props: {
79-
level: 1 as any,
80-
},
81-
}),
82-
);
83-
},
84-
"Mod-Alt-2": () => {
85-
const blockInfo = getBlockInfoFromSelection(this.editor.state);
86-
if (
87-
!blockInfo.isBlockContainer ||
88-
blockInfo.blockContent.node.type.spec.content !== "inline*"
89-
) {
90-
return true;
91-
}
92-
93-
return this.editor.commands.command(
94-
updateBlockCommand(blockInfo.bnBlock.beforePos, {
95-
type: "heading",
96-
props: {
97-
level: 2 as any,
98-
},
99-
}),
100-
);
101-
},
102-
"Mod-Alt-3": () => {
103-
const blockInfo = getBlockInfoFromSelection(this.editor.state);
104-
if (
105-
!blockInfo.isBlockContainer ||
106-
blockInfo.blockContent.node.type.spec.content !== "inline*"
107-
) {
108-
return true;
109-
}
110-
111-
return this.editor.commands.command(
112-
updateBlockCommand(blockInfo.bnBlock.beforePos, {
113-
type: "heading",
114-
props: {
115-
level: 3 as any,
116-
},
117-
}),
118-
);
119-
},
120-
};
68+
const editor = this.options.editor as BlockNoteEditor<any, any, any>;
69+
70+
return Object.fromEntries(
71+
editor.settings.heading.levels.map((level) => [
72+
`Mod-Alt-${level}`,
73+
() => {
74+
const blockInfo = getBlockInfoFromSelection(this.editor.state);
75+
if (
76+
!blockInfo.isBlockContainer ||
77+
blockInfo.blockContent.node.type.spec.content !== "inline*"
78+
) {
79+
return true;
80+
}
81+
82+
return this.editor.commands.command(
83+
updateBlockCommand(blockInfo.bnBlock.beforePos, {
84+
type: "heading",
85+
props: {
86+
level: level as any,
87+
},
88+
}),
89+
);
90+
},
91+
]),
92+
);
12193
},
12294
parseHTML() {
95+
const editor = this.options.editor as BlockNoteEditor<any, any, any>;
96+
12397
return [
12498
// Parse from internal HTML.
12599
{
126100
tag: "div[data-content-type=" + this.name + "]",
127101
contentElement: ".bn-inline-content",
128102
},
129-
// Parse from external HTML.
130-
{
131-
tag: "h1",
132-
attrs: { level: 1 },
133-
node: "heading",
134-
},
135-
{
136-
tag: "h2",
137-
attrs: { level: 2 },
103+
...editor.settings.heading.levels.map((level) => ({
104+
tag: `h${level}`,
105+
attrs: { level },
138106
node: "heading",
139-
},
140-
{
141-
tag: "h3",
142-
attrs: { level: 3 },
143-
node: "heading",
144-
},
107+
})),
145108
];
146109
},
147110

packages/core/src/editor/Block.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ NESTED BLOCKS
130130
[data-content-type="heading"][data-level="3"] {
131131
--level: 1.3em;
132132
}
133+
[data-content-type="heading"][data-level="4"] {
134+
--level: 1em;
135+
}
136+
[data-content-type="heading"][data-level="5"] {
137+
--level: 0.9em;
138+
}
139+
[data-content-type="heading"][data-level="6"] {
140+
--level: 0.8em;
141+
}
133142

134143
[data-prev-level="1"] {
135144
--prev-level: 3em;
@@ -140,6 +149,15 @@ NESTED BLOCKS
140149
[data-prev-level="3"] {
141150
--prev-level: 1.3em;
142151
}
152+
[data-prev-level="4"] {
153+
--prev-level: 1em;
154+
}
155+
[data-prev-level="5"] {
156+
--prev-level: 0.9em;
157+
}
158+
[data-prev-level="6"] {
159+
--prev-level: 0.8em;
160+
}
143161

144162
.bn-block-outer[data-prev-type="heading"] > .bn-block > .bn-block-content {
145163
font-size: var(--prev-level);

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ export type BlockNoteEditorOptions<
229229
class?: string;
230230
}) => Plugin;
231231

232+
/**
233+
* Configuration for headings
234+
*/
235+
heading?: {
236+
/**
237+
* The levels of headings that should be available in the editor.
238+
* @note Configurable up to 6 levels of headings.
239+
* @default [1, 2, 3]
240+
*/
241+
levels?: (1 | 2 | 3 | 4 | 5 | 6)[];
242+
};
243+
232244
/**
233245
* The content that should be in the editor when it's created, represented as an array of partial block objects.
234246
*/
@@ -527,6 +539,9 @@ export class BlockNoteEditor<
527539
headers: boolean;
528540
};
529541
codeBlock: CodeBlockOptions;
542+
heading: {
543+
levels: (1 | 2 | 3 | 4 | 5 | 6)[];
544+
};
530545
};
531546

532547
public static create<
@@ -580,6 +595,9 @@ export class BlockNoteEditor<
580595
supportedLanguages: options?.codeBlock?.supportedLanguages ?? {},
581596
createHighlighter: options?.codeBlock?.createHighlighter ?? undefined,
582597
},
598+
heading: {
599+
levels: options?.heading?.levels ?? [1, 2, 3],
600+
},
583601
};
584602

585603
// apply defaults

packages/core/src/editor/editor.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Tippy popups that are appended to document.body directly
4444
.bn-default-styles h1,
4545
.bn-default-styles h2,
4646
.bn-default-styles h3,
47+
.bn-default-styles h4,
48+
.bn-default-styles h5,
49+
.bn-default-styles h6,
4750
.bn-default-styles li {
4851
margin: 0;
4952
padding: 0;

packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -122,37 +122,6 @@ export function getDefaultSlashMenuItems<
122122
key: "heading_3",
123123
...editor.dictionary.slash_menu.heading_3,
124124
},
125-
{
126-
onItemClick: () => {
127-
insertOrUpdateBlock(editor, {
128-
type: "heading",
129-
props: { level: 1, isToggleable: true },
130-
});
131-
},
132-
key: "toggle_heading",
133-
...editor.dictionary.slash_menu.toggle_heading,
134-
},
135-
{
136-
onItemClick: () => {
137-
insertOrUpdateBlock(editor, {
138-
type: "heading",
139-
props: { level: 2, isToggleable: true },
140-
});
141-
},
142-
143-
key: "toggle_heading_2",
144-
...editor.dictionary.slash_menu.toggle_heading_2,
145-
},
146-
{
147-
onItemClick: () => {
148-
insertOrUpdateBlock(editor, {
149-
type: "heading",
150-
props: { level: 3, isToggleable: true },
151-
});
152-
},
153-
key: "toggle_heading_3",
154-
...editor.dictionary.slash_menu.toggle_heading_3,
155-
},
156125
);
157126
}
158127

@@ -346,6 +315,57 @@ export function getDefaultSlashMenuItems<
346315
});
347316
}
348317

318+
if (checkDefaultBlockTypeInSchema("heading", editor)) {
319+
items.push(
320+
{
321+
onItemClick: () => {
322+
insertOrUpdateBlock(editor, {
323+
type: "heading",
324+
props: { level: 1, isToggleable: true },
325+
});
326+
},
327+
key: "toggle_heading",
328+
...editor.dictionary.slash_menu.toggle_heading,
329+
},
330+
{
331+
onItemClick: () => {
332+
insertOrUpdateBlock(editor, {
333+
type: "heading",
334+
props: { level: 2, isToggleable: true },
335+
});
336+
},
337+
338+
key: "toggle_heading_2",
339+
...editor.dictionary.slash_menu.toggle_heading_2,
340+
},
341+
{
342+
onItemClick: () => {
343+
insertOrUpdateBlock(editor, {
344+
type: "heading",
345+
props: { level: 3, isToggleable: true },
346+
});
347+
},
348+
key: "toggle_heading_3",
349+
...editor.dictionary.slash_menu.toggle_heading_3,
350+
},
351+
);
352+
353+
editor.settings.heading.levels
354+
.filter((level): level is 4 | 5 | 6 => level > 3)
355+
.forEach((level) => {
356+
items.push({
357+
onItemClick: () => {
358+
insertOrUpdateBlock(editor, {
359+
type: "heading",
360+
props: { level: level },
361+
});
362+
},
363+
key: `heading_${level}`,
364+
...editor.dictionary.slash_menu[`heading_${level}`],
365+
});
366+
});
367+
}
368+
349369
items.push({
350370
onItemClick: () => {
351371
editor.openSuggestionMenu(":", {

packages/core/src/i18n/locales/ar.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,41 @@ export const ar: Dictionary = {
2020
aliases: ["ع3", "عنوان3", "عنوان فرعي"],
2121
group: "العناوين",
2222
},
23+
heading_4: {
24+
title: "عنوان 4",
25+
subtext: "عنوان فرعي ثانوي صغير",
26+
aliases: ["ع4", "عنوان4", "عنوان فرعي صغير"],
27+
group: "العناوين الفرعية",
28+
},
29+
heading_5: {
30+
title: "عنوان 5",
31+
subtext: "عنوان فرعي صغير",
32+
aliases: ["ع5", "عنوان5", "عنوان فرعي صغير"],
33+
group: "العناوين الفرعية",
34+
},
35+
heading_6: {
36+
title: "عنوان 6",
37+
subtext: "أدنى مستوى للعناوين",
38+
aliases: ["ع6", "عنوان6", "العنوان الفرعي الأدنى"],
39+
group: "العناوين الفرعية",
40+
},
2341
toggle_heading: {
2442
title: "عنوان قابل للطي 1",
2543
subtext: "عنوان قابل للطي لإظهار وإخفاء المحتوى",
2644
aliases: ["ع", "عنوان1", "ع1", "قابل للطي", "طي"],
27-
group: "العناوين",
45+
group: "العناوين الفرعية",
2846
},
2947
toggle_heading_2: {
3048
title: "عنوان قابل للطي 2",
3149
subtext: "عنوان فرعي قابل للطي لإظهار وإخفاء المحتوى",
3250
aliases: ["ع2", "عنوان2", "عنوان فرعي", "قابل للطي", "طي"],
33-
group: "العناوين",
51+
group: "العناوين الفرعية",
3452
},
3553
toggle_heading_3: {
3654
title: "عنوان قابل للطي 3",
3755
subtext: "عنوان فرعي ثانوي قابل للطي لإظهار وإخفاء المحتوى",
3856
aliases: ["ع3", "عنوان3", "عنوان فرعي", "قابل للطي", "طي"],
39-
group: "العناوين",
57+
group: "العناوين الفرعية",
4058
},
4159
quote: {
4260
title: "اقتباس",

0 commit comments

Comments
 (0)