Skip to content

Commit 34ae5f6

Browse files
feat: Block quote (#1563)
* Implemented block quote * Updated screenshots * Added input rule * Reverted `showNestingIndicator`
1 parent 69e3fa5 commit 34ae5f6

File tree

38 files changed

+315
-3
lines changed

38 files changed

+315
-3
lines changed

docs/pages/docs/editor-basics/default-schema.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ type HeadingBlock = {
5656

5757
`level:` The heading level, representing a title (`level: 1`), heading (`level: 2`), and subheading (`level: 3`).
5858

59+
#### Quote
60+
61+
**Type & Props**
62+
63+
```typescript
64+
type QuoteBlock = {
65+
id: string;
66+
type: "quote";
67+
props: DefaultProps;
68+
content: InlineContent[];
69+
children: Block[];
70+
};
71+
```
72+
5973
#### Bullet List Item
6074

6175
**Type & Props**

examples/01-basic/04-default-blocks/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export default function App() {
3232
type: "heading",
3333
content: "Heading",
3434
},
35+
{
36+
type: "quote",
37+
content: "Quote",
38+
},
3539
{
3640
type: "bulletListItem",
3741
content: "Bullet List Item",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
createBlockSpecFromStronglyTypedTiptapNode,
3+
createStronglyTypedTiptapNode,
4+
} from "../../schema/index.js";
5+
import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js";
6+
import { defaultProps } from "../defaultProps.js";
7+
import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js";
8+
import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
9+
import { InputRule } from "@tiptap/core";
10+
11+
export const quotePropSchema = {
12+
...defaultProps,
13+
};
14+
15+
export const QuoteBlockContent = createStronglyTypedTiptapNode({
16+
name: "quote",
17+
content: "inline*",
18+
group: "blockContent",
19+
20+
addInputRules() {
21+
return [
22+
// Creates a block quote when starting with ">".
23+
new InputRule({
24+
find: new RegExp(`^>\\s$`),
25+
handler: ({ state, chain, range }) => {
26+
const blockInfo = getBlockInfoFromSelection(state);
27+
if (
28+
!blockInfo.isBlockContainer ||
29+
blockInfo.blockContent.node.type.spec.content !== "inline*"
30+
) {
31+
return;
32+
}
33+
34+
chain()
35+
.command(
36+
updateBlockCommand(
37+
this.options.editor,
38+
blockInfo.bnBlock.beforePos,
39+
{
40+
type: "quote",
41+
props: {},
42+
}
43+
)
44+
)
45+
// Removes the ">" character used to set the list.
46+
.deleteRange({ from: range.from, to: range.to });
47+
},
48+
}),
49+
];
50+
},
51+
52+
addKeyboardShortcuts() {
53+
return {
54+
"Mod-Alt-q": () => {
55+
const blockInfo = getBlockInfoFromSelection(this.editor.state);
56+
if (
57+
!blockInfo.isBlockContainer ||
58+
blockInfo.blockContent.node.type.spec.content !== "inline*"
59+
) {
60+
return true;
61+
}
62+
63+
return this.editor.commands.command(
64+
updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, {
65+
type: "quote",
66+
})
67+
);
68+
},
69+
};
70+
},
71+
72+
parseHTML() {
73+
return [
74+
{ tag: "div[data-content-type=" + this.name + "]" },
75+
{
76+
tag: "blockquote",
77+
node: "quote",
78+
},
79+
];
80+
},
81+
82+
renderHTML({ HTMLAttributes }) {
83+
return createDefaultBlockDOMOutputSpec(
84+
this.name,
85+
"blockquote",
86+
{
87+
...(this.options.domAttributes?.blockContent || {}),
88+
...HTMLAttributes,
89+
},
90+
this.options.domAttributes?.inlineContent || {}
91+
);
92+
},
93+
});
94+
95+
export const Quote = createBlockSpecFromStronglyTypedTiptapNode(
96+
QuoteBlockContent,
97+
quotePropSchema
98+
);

packages/core/src/blocks/defaultBlocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockConten
2929
import { CheckListItem } from "./ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.js";
3030
import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.js";
3131
import { Paragraph } from "./ParagraphBlockContent/ParagraphBlockContent.js";
32+
import { Quote } from "./QuoteBlockContent/QuoteBlockContent.js";
3233
import { Table } from "./TableBlockContent/TableBlockContent.js";
3334
import { VideoBlock } from "./VideoBlockContent/VideoBlockContent.js";
3435

3536
export const defaultBlockSpecs = {
3637
paragraph: Paragraph,
3738
heading: Heading,
39+
quote: Quote,
3840
codeBlock: CodeBlock,
3941
bulletListItem: BulletListItem,
4042
numberedListItem: NumberedListItem,

packages/core/src/editor/Block.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ NESTED BLOCKS
150150
font-weight: bold;
151151
}
152152

153+
/* QUOTES */
154+
[data-content-type="quote"] blockquote {
155+
border-left: 2px solid rgb(125, 121, 122);
156+
color: rgb(125, 121, 122);
157+
margin: 0;
158+
padding-left: 1em;
159+
}
160+
153161
/* LISTS */
154162

155163
.bn-block-content::before {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ export function getDefaultSlashMenuItems<
125125
);
126126
}
127127

128+
if (checkDefaultBlockTypeInSchema("quote", editor)) {
129+
items.push({
130+
onItemClick: () => {
131+
insertOrUpdateBlock(editor, {
132+
type: "quote",
133+
});
134+
},
135+
key: "quote",
136+
...editor.dictionary.slash_menu.quote,
137+
});
138+
}
139+
128140
if (checkDefaultBlockTypeInSchema("numberedListItem", editor)) {
129141
items.push({
130142
onItemClick: () => {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export const ar: Dictionary = {
2020
aliases: ["ع3", "عنوان3", "عنوان فرعي"],
2121
group: "العناوين",
2222
},
23+
quote: {
24+
title: "اقتباس",
25+
subtext: "اقتباس أو مقتطف",
26+
aliases: ["quotation", "blockquote", "bq"],
27+
group: "الكتل الأساسية",
28+
},
2329
numbered_list: {
2430
title: "قائمة مرقمة",
2531
subtext: "تستخدم لعرض قائمة مرقمة",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export const de: Dictionary = {
2020
aliases: ["h3", "überschrift3", "unterüberschrift"],
2121
group: "Überschriften",
2222
},
23+
quote: {
24+
title: "Zitat",
25+
subtext: "Zitat oder Auszug",
26+
aliases: ["quotation", "blockquote", "bq"],
27+
group: "Grundlegende blöcke",
28+
},
2329
numbered_list: {
2430
title: "Nummerierte Liste",
2531
subtext: "Liste mit nummerierten Elementen",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export const en = {
1818
aliases: ["h3", "heading3", "subheading"],
1919
group: "Headings",
2020
},
21+
quote: {
22+
title: "Quote",
23+
subtext: "Quote or excerpt",
24+
aliases: ["quotation", "blockquote", "bq"],
25+
group: "Basic blocks",
26+
},
2127
numbered_list: {
2228
title: "Numbered List",
2329
subtext: "List with ordered items",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export const es: Dictionary = {
2020
aliases: ["h3", "encabezado3", "subencabezado"],
2121
group: "Encabezados",
2222
},
23+
quote: {
24+
title: "Cita",
25+
subtext: "Cita o extracto",
26+
aliases: ["quotation", "blockquote", "bq"],
27+
group: "Bloques básicos",
28+
},
2329
numbered_list: {
2430
title: "Lista Numerada",
2531
subtext: "Lista con elementos ordenados",

0 commit comments

Comments
 (0)