Skip to content

Commit 46266a2

Browse files
jonkaftonpre-commit-ci[bot]Ahtesham Quraish
authored
Initial Tiptap Editor (#2691)
* Initial Tiptap Editor * Update lockfile * Ignore stylelint errors in vendor sheets * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Run fmt-fix * Eslint ignore vendor code * Revert main package.json * Remove aliases from Tiptap template code imports * Prettier fixes * CodeQL fix * React in scope fixes * Format fix * Transform react-hotkeys-hook to resolve esm error --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ahtesham Quraish <ahtesham.quraish@A006-01455.local>
1 parent fc72b43 commit 46266a2

File tree

149 files changed

+15142
-165
lines changed

Some content is hidden

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

149 files changed

+15142
-165
lines changed

frontends/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ module.exports = {
1414
"mit-learn",
1515
"github-pages",
1616
"storybook-static",
17+
"**/TiptapEditor/components/**/*.tsx",
18+
"**/TiptapEditor/components/**/*.ts",
19+
"**/TiptapEditor/hooks/**/*.ts",
20+
"**/TiptapEditor/lib/**/*.ts",
1721
],
1822
settings: {
1923
"import/resolver": {

frontends/.stylelintrc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ rules:
66
- message: "Expected class selector to be kebab-case"
77
ignoreFiles:
88
- "**/*.vendor.css"
9+
- "**/TiptapEditor/components/**/*.scss"
10+
- "**/TiptapEditor/styles/**/*.scss"
911
overrides:
1012
- files:
1113
- "**/*.scss"

frontends/main/jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ const config: Config.InitialOptions = {
77
...baseConfig.setupFilesAfterEnv,
88
"./test-utils/setupJest.tsx",
99
],
10-
transformIgnorePatterns: ["node_modules/(?!@faker-js).+"],
10+
transformIgnorePatterns: [
11+
"node_modules/(?!(@faker-js|react-hotkeys-hook)).+",
12+
],
1113
moduleNameMapper: {
1214
...baseConfig.moduleNameMapper,
1315
"^@/(.*)$": path.resolve(__dirname, "src/$1"),

frontends/main/next.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ const nextConfig = {
128128
allowCollectingMemory: true,
129129
})
130130
}
131-
// Important: return the modified config
131+
132132
return config
133133
},
134134
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { TiptapEditor, theme, styled, HEADER_HEIGHT } from "ol-components"
5+
import { Permission } from "api/hooks/user"
6+
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"
7+
8+
const PageContainer = styled.div({
9+
color: theme.custom.colors.darkGray2,
10+
display: "flex",
11+
height: `calc(100vh - ${HEADER_HEIGHT}px - 132px)`,
12+
})
13+
14+
const EditorContainer = styled.div({
15+
minHeight: 0,
16+
})
17+
18+
const StyledTiptapEditor = styled(TiptapEditor)({
19+
width: "70vw",
20+
height: `calc(100% - ${HEADER_HEIGHT}px - 132px)`,
21+
overscrollBehavior: "contain",
22+
})
23+
24+
const NewArticlePage: React.FC = () => {
25+
return (
26+
<RestrictedRoute requires={Permission.ArticleEditor}>
27+
<PageContainer>
28+
<EditorContainer>
29+
<StyledTiptapEditor />
30+
</EditorContainer>
31+
</PageContainer>
32+
</RestrictedRoute>
33+
)
34+
}
35+
36+
export { NewArticlePage }
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
import { Metadata } from "next"
3+
import { standardizeMetadata } from "@/common/metadata"
4+
import { NewArticlePage } from "@/app-pages/ArticlePage/NewArticlePage"
5+
6+
export const metadata: Metadata = standardizeMetadata({
7+
title: "New Article",
8+
robots: "noindex, nofollow",
9+
})
10+
11+
const Page: React.FC<PageProps<"/article/new">> = () => {
12+
return <NewArticlePage />
13+
}
14+
15+
export default Page

frontends/ol-components/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,43 @@
1919
"@dnd-kit/utilities": "^3.2.1",
2020
"@emotion/react": "^11.11.1",
2121
"@emotion/styled": "^11.11.0",
22+
"@floating-ui/react": "^0.27.16",
2223
"@mui/base": "5.0.0-beta.70",
2324
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
2425
"@mui/material": "^6.4.5",
2526
"@mui/material-nextjs": "^6.4.3",
2627
"@mui/system": "^6.4.3",
28+
"@radix-ui/react-dropdown-menu": "^2.1.16",
29+
"@radix-ui/react-popover": "^1.1.15",
2730
"@remixicon/react": "^4.2.0",
2831
"@testing-library/dom": "^10.4.0",
32+
"@tiptap/extension-highlight": "^3.10.5",
33+
"@tiptap/extension-horizontal-rule": "^3.10.5",
34+
"@tiptap/extension-image": "^3.10.5",
35+
"@tiptap/extension-list": "^3.10.5",
36+
"@tiptap/extension-subscript": "^3.10.5",
37+
"@tiptap/extension-superscript": "^3.10.5",
38+
"@tiptap/extension-text-align": "^3.10.5",
39+
"@tiptap/extension-typography": "^3.10.5",
40+
"@tiptap/extensions": "^3.10.5",
41+
"@tiptap/pm": "^3.10.5",
42+
"@tiptap/react": "^3.10.5",
43+
"@tiptap/starter-kit": "^3.10.5",
2944
"@types/react-dom": "^19",
3045
"@types/tinycolor2": "^1.4.6",
3146
"api": "workspace:*",
3247
"classnames": "^2.5.1",
3348
"embla-carousel-react": "^8.6.0",
3449
"embla-carousel-wheel-gestures": "^8.0.2",
3550
"lodash": "^4.17.21",
51+
"lodash.throttle": "^4.1.1",
3652
"material-ui-popup-state": "^5.1.0",
3753
"next": "^15.5.2",
3854
"ol-test-utilities": "0.0.0",
3955
"ol-utilities": "0.0.0",
4056
"react": "^19.0.0",
4157
"react-dom": "^19.0.0",
58+
"react-hotkeys-hook": "^5.2.1",
4259
"react-select": "^5.7.7",
4360
"react-share": "^5.0.3",
4461
"react-slick": "^0.30.2",
@@ -65,10 +82,13 @@
6582
"@storybook/types": "^8.2.9",
6683
"@testing-library/react": "^16.3.0",
6784
"@testing-library/user-event": "^14.5.2",
85+
"@types/lodash.throttle": "^4.1.9",
6886
"@types/react-slick": "^0",
6987
"dotenv": "^17.0.0",
7088
"lodash": "^4.17.21",
7189
"prop-types": "^15.8.1",
90+
"sass": "^1.93.3",
91+
"sass-embedded": "^1.93.3",
7292
"storybook": "^8.2.9",
7393
"typescript": "^5.5.4",
7494
"webpack": "^5.94.0"
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"use client"
2+
3+
// Based on ./components/tiptap-templates/simple/simple-editor.tsx
4+
5+
import React, { useRef } from "react"
6+
import { EditorContent, EditorContext, useEditor } from "@tiptap/react"
7+
8+
// --- Tiptap Core Extensions ---
9+
import { StarterKit } from "@tiptap/starter-kit"
10+
import { TaskItem, TaskList } from "@tiptap/extension-list"
11+
import { TextAlign } from "@tiptap/extension-text-align"
12+
import { Typography } from "@tiptap/extension-typography"
13+
import { Highlight } from "@tiptap/extension-highlight"
14+
import { Subscript } from "@tiptap/extension-subscript"
15+
import { Superscript } from "@tiptap/extension-superscript"
16+
import { Selection } from "@tiptap/extensions"
17+
18+
// --- UI Primitives ---
19+
import { Spacer } from "./components/tiptap-ui-primitive/spacer"
20+
import {
21+
Toolbar,
22+
ToolbarGroup,
23+
ToolbarSeparator,
24+
} from "./components/tiptap-ui-primitive/toolbar"
25+
26+
// --- Tiptap Node ---
27+
import { ImageUploadNode } from "./components/tiptap-node/image-upload-node/image-upload-node-extension"
28+
import { HorizontalRule } from "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension"
29+
import "./components/tiptap-node/blockquote-node/blockquote-node.scss"
30+
import "./components/tiptap-node/code-block-node/code-block-node.scss"
31+
import "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss"
32+
import "./components/tiptap-node/list-node/list-node.scss"
33+
import "./components/tiptap-node/image-node/image-node.scss"
34+
import "./components/tiptap-node/heading-node/heading-node.scss"
35+
import "./components/tiptap-node/paragraph-node/paragraph-node.scss"
36+
37+
// --- Tiptap UI ---
38+
import { HeadingDropdownMenu } from "./components/tiptap-ui/heading-dropdown-menu"
39+
import { ListDropdownMenu } from "./components/tiptap-ui/list-dropdown-menu"
40+
import { BlockquoteButton } from "./components/tiptap-ui/blockquote-button"
41+
import { CodeBlockButton } from "./components/tiptap-ui/code-block-button"
42+
import { ColorHighlightPopover } from "./components/tiptap-ui/color-highlight-popover"
43+
import { LinkPopover } from "./components/tiptap-ui/link-popover"
44+
import { MarkButton } from "./components/tiptap-ui/mark-button"
45+
import { TextAlignButton } from "./components/tiptap-ui/text-align-button"
46+
import { UndoRedoButton } from "./components/tiptap-ui/undo-redo-button"
47+
48+
// --- Lib ---
49+
import { handleImageUpload, MAX_FILE_SIZE } from "./lib/tiptap-utils"
50+
51+
// --- Styles ---
52+
import "./styles/_keyframe-animations.scss"
53+
import "./styles/_variables.scss"
54+
import "./components/tiptap-templates/simple/simple-editor.scss"
55+
56+
const MainToolbarContent = () => {
57+
return (
58+
<>
59+
<Spacer />
60+
61+
<ToolbarGroup>
62+
<UndoRedoButton action="undo" />
63+
<UndoRedoButton action="redo" />
64+
</ToolbarGroup>
65+
66+
<ToolbarSeparator />
67+
68+
<ToolbarGroup>
69+
<HeadingDropdownMenu levels={[1, 2, 3, 4]} />
70+
<ListDropdownMenu types={["bulletList", "orderedList", "taskList"]} />
71+
<BlockquoteButton />
72+
<CodeBlockButton />
73+
</ToolbarGroup>
74+
75+
<ToolbarSeparator />
76+
77+
<ToolbarGroup>
78+
<MarkButton type="bold" />
79+
<MarkButton type="italic" />
80+
<MarkButton type="strike" />
81+
<MarkButton type="code" />
82+
<MarkButton type="underline" />
83+
<ColorHighlightPopover />
84+
<LinkPopover />
85+
</ToolbarGroup>
86+
87+
<ToolbarSeparator />
88+
89+
<ToolbarGroup>
90+
<MarkButton type="superscript" />
91+
<MarkButton type="subscript" />
92+
</ToolbarGroup>
93+
94+
<ToolbarSeparator />
95+
96+
<ToolbarGroup>
97+
<TextAlignButton align="left" />
98+
<TextAlignButton align="center" />
99+
<TextAlignButton align="right" />
100+
<TextAlignButton align="justify" />
101+
</ToolbarGroup>
102+
103+
<Spacer />
104+
</>
105+
)
106+
}
107+
108+
export default function SimpleEditor() {
109+
const toolbarRef = useRef<HTMLDivElement>(null)
110+
111+
const editor = useEditor({
112+
immediatelyRender: false,
113+
shouldRerenderOnTransaction: false,
114+
editorProps: {
115+
attributes: {
116+
autocomplete: "off",
117+
autocorrect: "off",
118+
autocapitalize: "off",
119+
"aria-label": "Main content area, start typing to enter text.",
120+
class: "simple-editor",
121+
},
122+
},
123+
extensions: [
124+
StarterKit.configure({
125+
horizontalRule: false,
126+
link: {
127+
openOnClick: false,
128+
enableClickSelection: true,
129+
},
130+
}),
131+
HorizontalRule,
132+
TextAlign.configure({ types: ["heading", "paragraph"] }),
133+
TaskList,
134+
TaskItem.configure({ nested: true }),
135+
Highlight.configure({ multicolor: true }),
136+
Typography,
137+
Superscript,
138+
Subscript,
139+
Selection,
140+
ImageUploadNode.configure({
141+
accept: "image/*",
142+
maxSize: MAX_FILE_SIZE,
143+
limit: 3,
144+
upload: handleImageUpload,
145+
onError: (error) => console.error("Upload failed:", error),
146+
}),
147+
],
148+
content: {
149+
type: "doc",
150+
content: [
151+
{
152+
type: "paragraph",
153+
content: [],
154+
},
155+
],
156+
},
157+
})
158+
159+
return (
160+
<div className="simple-editor-wrapper">
161+
<EditorContext.Provider value={{ editor }}>
162+
<Toolbar ref={toolbarRef}>
163+
<MainToolbarContent />
164+
</Toolbar>
165+
166+
<EditorContent
167+
editor={editor}
168+
role="presentation"
169+
className="simple-editor-content"
170+
/>
171+
</EditorContext.Provider>
172+
</div>
173+
)
174+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { memo } from "react"
2+
3+
type SvgProps = React.ComponentPropsWithoutRef<"svg">
4+
5+
export const AlignCenterIcon = memo(({ className, ...props }: SvgProps) => {
6+
return (
7+
<svg
8+
width="24"
9+
height="24"
10+
className={className}
11+
viewBox="0 0 24 24"
12+
fill="currentColor"
13+
xmlns="http://www.w3.org/2000/svg"
14+
{...props}
15+
>
16+
<path
17+
fillRule="evenodd"
18+
clipRule="evenodd"
19+
d="M2 6C2 5.44772 2.44772 5 3 5H21C21.5523 5 22 5.44772 22 6C22 6.55228 21.5523 7 21 7H3C2.44772 7 2 6.55228 2 6Z"
20+
fill="currentColor"
21+
/>
22+
<path
23+
fillRule="evenodd"
24+
clipRule="evenodd"
25+
d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z"
26+
fill="currentColor"
27+
/>
28+
<path
29+
fillRule="evenodd"
30+
clipRule="evenodd"
31+
d="M4 18C4 17.4477 4.44772 17 5 17H19C19.5523 17 20 17.4477 20 18C20 18.5523 19.5523 19 19 19H5C4.44772 19 4 18.5523 4 18Z"
32+
fill="currentColor"
33+
/>
34+
</svg>
35+
)
36+
})
37+
38+
AlignCenterIcon.displayName = "AlignCenterIcon"

0 commit comments

Comments
 (0)