Skip to content

Commit 08c03a6

Browse files
Ahtesham QuraishAhtesham Quraish
authored andcommitted
add quote node as per design along with its button
1 parent 2feb214 commit 08c03a6

File tree

8 files changed

+375
-0
lines changed

8 files changed

+375
-0
lines changed

frontends/ol-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@remixicon/react": "^4.2.0",
3232
"@testing-library/dom": "^10.4.0",
3333
"@tiptap/core": "^3.11.0",
34+
"@tiptap/extension-blockquote": "^3.11.0",
3435
"@tiptap/extension-document": "^3.11.1",
3536
"@tiptap/extension-heading": "^3.11.1",
3637
"@tiptap/extension-highlight": "^3.11.0",

frontends/ol-components/src/components/TiptapEditor/ArticleEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { Alert, Button, ButtonLink } from "@mitodl/smoot-design"
5454
import Typography from "@mui/material/Typography"
5555
import { useUserHasPermission, Permission } from "api/hooks/user"
5656
import { BannerNode } from "./extensions/node/Banner/BannerNode"
57+
import { Quote } from "./extensions/node/Quote/Quote"
5758
import {
5859
HEADER_HEIGHT,
5960
HEADER_HEIGHT_MD,
@@ -274,6 +275,7 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {
274275
Subscript,
275276
Selection,
276277
Image,
278+
Quote,
277279
MediaEmbedNode,
278280
DividerNode,
279281
ArticleByLineInfoBarNode,

frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { UndoRedoButton } from "./vendor/components/tiptap-ui/undo-redo-button"
3737
import { LearningResourceButton } from "./extensions/ui/LearningResource/LearningResourceButton"
3838
import { Button } from "./vendor/components/tiptap-ui-primitive/button"
3939
import { DividerButton } from "./extensions/ui/Divider/DividerButton"
40+
import { QuoteButton } from "./extensions/ui/Quote/QuoteButton"
4041
import {
4142
DropdownMenu,
4243
DropdownMenuTrigger,
@@ -135,6 +136,32 @@ const StyledEditorContent = styled(EditorContent, {
135136
marginBottom: 0,
136137
},
137138
},
139+
quote: {
140+
backgroundColor: theme.custom.colors.lightGray1,
141+
padding: "40px",
142+
borderRadius: "8px",
143+
marginBottom: "40px",
144+
display: "block",
145+
borderLeft: `2px solid ${theme.custom.colors.red}`,
146+
"::before": {
147+
content: '"“"', // opening inverted comma
148+
position: "absolute",
149+
left: "17px",
150+
fontSize: "64px",
151+
lineHeight: 1,
152+
fontWeight: theme.typography.fontWeightRegular,
153+
top: "-15px",
154+
color: theme.custom.colors.red,
155+
fontFamily: theme.typography.fontFamily,
156+
},
157+
p: {
158+
position: "relative",
159+
},
160+
"p:last-child": {
161+
marginBottom: 0,
162+
marginTop: 0,
163+
},
164+
},
138165
},
139166
}))
140167

@@ -180,6 +207,9 @@ export function InsertDropdownMenu({ editor }: TiptapEditorToolbarProps) {
180207
<DropdownMenuItem asChild>
181208
<DividerButton editor={editor} text="Divider" />
182209
</DropdownMenuItem>
210+
<DropdownMenuItem asChild>
211+
<QuoteButton text="Quote" />
212+
</DropdownMenuItem>
183213
</StyledDropdownMenuWrapper>
184214
</DropdownMenu>
185215
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Blockquote from "@tiptap/extension-blockquote"
2+
3+
export const Quote = Blockquote.extend({
4+
name: "quote",
5+
6+
parseHTML() {
7+
return [
8+
{ tag: "quote" }, // Custom tag
9+
{ tag: "blockquote" }, // Fallback for pasted content
10+
]
11+
},
12+
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
renderHTML({ HTMLAttributes }: { HTMLAttributes: { [key: string]: any } }) {
15+
return ["quote", HTMLAttributes, 0]
16+
},
17+
})
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { forwardRef, useCallback } from "react"
2+
3+
// --- Tiptap UI ---
4+
import type { UseQuoteConfig } from "./"
5+
import { QUOTE_SHORTCUT_KEY, useQuote } from "./"
6+
7+
// --- Hooks ---
8+
import { useTiptapEditor } from "../../../vendor/hooks/use-tiptap-editor"
9+
10+
// --- Lib ---
11+
import { parseShortcutKeys } from "../../../vendor/lib/tiptap-utils"
12+
13+
// --- UI Primitives ---
14+
import type { ButtonProps } from "../../../vendor/components/tiptap-ui-primitive/button"
15+
import { Button } from "../../../vendor/components/tiptap-ui-primitive/button"
16+
import { Badge } from "../../../vendor/components/tiptap-ui-primitive/badge"
17+
18+
export interface QuoteButtonProps
19+
extends Omit<ButtonProps, "type">,
20+
UseQuoteConfig {
21+
/**
22+
* Optional text to display alongside the icon.
23+
*/
24+
text?: string
25+
/**
26+
* Optional show shortcut keys in the button.
27+
* @default false
28+
*/
29+
showShortcut?: boolean
30+
}
31+
32+
export function QuoteShortcutBadge({
33+
shortcutKeys = QUOTE_SHORTCUT_KEY,
34+
}: {
35+
shortcutKeys?: string
36+
}) {
37+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
38+
}
39+
40+
/**
41+
* Button component for toggling blockquote in a Tiptap editor.
42+
*
43+
* For custom button implementations, use the `useQuote` hook instead.
44+
*/
45+
export const QuoteButton = forwardRef<HTMLButtonElement, QuoteButtonProps>(
46+
(
47+
{
48+
editor: providedEditor,
49+
text,
50+
hideWhenUnavailable = false,
51+
onToggled,
52+
showShortcut = false,
53+
onClick,
54+
children,
55+
...buttonProps
56+
},
57+
ref,
58+
) => {
59+
const { editor } = useTiptapEditor(providedEditor)
60+
const {
61+
isVisible,
62+
canToggle,
63+
isActive,
64+
handleToggle,
65+
label,
66+
shortcutKeys,
67+
Icon,
68+
} = useQuote({
69+
editor,
70+
hideWhenUnavailable,
71+
onToggled,
72+
})
73+
74+
const handleClick = useCallback(
75+
(event: React.MouseEvent<HTMLButtonElement>) => {
76+
onClick?.(event)
77+
if (event.defaultPrevented) return
78+
handleToggle()
79+
},
80+
[handleToggle, onClick],
81+
)
82+
83+
if (!isVisible) {
84+
return null
85+
}
86+
87+
return (
88+
<Button
89+
type="button"
90+
data-style="ghost"
91+
data-active-state={isActive ? "on" : "off"}
92+
tabIndex={-1}
93+
disabled={!canToggle}
94+
data-disabled={!canToggle}
95+
aria-label={label}
96+
aria-pressed={isActive}
97+
tooltip="Quote"
98+
onClick={handleClick}
99+
{...buttonProps}
100+
ref={ref}
101+
>
102+
{children ?? (
103+
<>
104+
<Icon className="tiptap-button-icon" />
105+
{text && <span className="tiptap-button-text">{text}</span>}
106+
{showShortcut && <QuoteShortcutBadge shortcutKeys={shortcutKeys} />}
107+
</>
108+
)}
109+
</Button>
110+
)
111+
},
112+
)
113+
114+
QuoteButton.displayName = "QuoteButton"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./QuoteButton"
2+
export * from "./useQuote"

0 commit comments

Comments
 (0)