(null)
+
+ const editor = useEditor({
+ immediatelyRender: false,
+ shouldRerenderOnTransaction: false,
+ content: value || {
+ type: "doc",
+ content: [{ type: "paragraph", content: [] }],
+ },
+ editable: !readOnly,
+ onUpdate: ({ editor }) => {
+ if (!readOnly) {
+ const json = editor.getJSON()
+ onChange?.(json)
+ }
+ },
+ editorProps: {
+ attributes: {
+ autocomplete: "off",
+ autocorrect: "off",
+ autocapitalize: "off",
+ "aria-label": "Main content area, start typing to enter text.",
+ class: "simple-editor",
+ },
+ },
+ extensions: [
+ StarterKit.configure({
+ horizontalRule: false,
+ link: {
+ openOnClick: false,
+ enableClickSelection: true,
+ },
+ }),
+ HorizontalRule,
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
+ TaskList,
+ TaskItem.configure({ nested: true }),
+ Highlight.configure({ multicolor: true }),
+ Typography,
+ Superscript,
+ Subscript,
+ Selection,
+ Image,
+ ImageUploadNode.configure({
+ accept: "image/*",
+ maxSize: MAX_FILE_SIZE,
+ limit: 3,
+ upload: handleImageUpload,
+ onError: (error) => console.error("Upload failed:", error),
+ }),
+ ],
+ })
+
+ // 👇 Important: update content when fetched JSON changes
+ useEffect(() => {
+ if (editor && value) {
+ editor.commands.setContent(value)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [editor])
+
+ return (
+
+
+ {!readOnly && (
+
+
+
+ )}
+
+ {!readOnly && (
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx
index a45ee5d91d..9b0220ea83 100644
--- a/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx
+++ b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx
@@ -2,30 +2,19 @@
// Based on ./components/tiptap-templates/simple/simple-editor.tsx
-import React, { useRef } from "react"
-import { EditorContent, EditorContext, useEditor } from "@tiptap/react"
-
-// --- Tiptap Core Extensions ---
-import { StarterKit } from "@tiptap/starter-kit"
-import { TaskItem, TaskList } from "@tiptap/extension-list"
-import { TextAlign } from "@tiptap/extension-text-align"
-import { Typography } from "@tiptap/extension-typography"
-import { Highlight } from "@tiptap/extension-highlight"
-import { Subscript } from "@tiptap/extension-subscript"
-import { Superscript } from "@tiptap/extension-superscript"
-import { Selection } from "@tiptap/extensions"
+import React from "react"
+import { EditorContent } from "@tiptap/react"
+
+import { ImageUploadButton } from "./components/tiptap-ui/image-upload-button"
// --- UI Primitives ---
import { Spacer } from "./components/tiptap-ui-primitive/spacer"
import {
- Toolbar,
ToolbarGroup,
ToolbarSeparator,
} from "./components/tiptap-ui-primitive/toolbar"
// --- Tiptap Node ---
-import { ImageUploadNode } from "./components/tiptap-node/image-upload-node/image-upload-node-extension"
-import { HorizontalRule } from "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension"
import "./components/tiptap-node/blockquote-node/blockquote-node.scss"
import "./components/tiptap-node/code-block-node/code-block-node.scss"
import "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss"
@@ -45,15 +34,12 @@ import { MarkButton } from "./components/tiptap-ui/mark-button"
import { TextAlignButton } from "./components/tiptap-ui/text-align-button"
import { UndoRedoButton } from "./components/tiptap-ui/undo-redo-button"
-// --- Lib ---
-import { handleImageUpload, MAX_FILE_SIZE } from "./lib/tiptap-utils"
-
// --- Styles ---
import "./styles/_keyframe-animations.scss"
import "./styles/_variables.scss"
import "./components/tiptap-templates/simple/simple-editor.scss"
-const MainToolbarContent = () => {
+export const MainToolbarContent = () => {
return (
<>
@@ -100,75 +86,27 @@ const MainToolbarContent = () => {
+
+
+
>
)
}
-export default function SimpleEditor() {
- const toolbarRef = useRef(null)
-
- const editor = useEditor({
- immediatelyRender: false,
- shouldRerenderOnTransaction: false,
- editorProps: {
- attributes: {
- autocomplete: "off",
- autocorrect: "off",
- autocapitalize: "off",
- "aria-label": "Main content area, start typing to enter text.",
- class: "simple-editor",
- },
- },
- extensions: [
- StarterKit.configure({
- horizontalRule: false,
- link: {
- openOnClick: false,
- enableClickSelection: true,
- },
- }),
- HorizontalRule,
- TextAlign.configure({ types: ["heading", "paragraph"] }),
- TaskList,
- TaskItem.configure({ nested: true }),
- Highlight.configure({ multicolor: true }),
- Typography,
- Superscript,
- Subscript,
- Selection,
- ImageUploadNode.configure({
- accept: "image/*",
- maxSize: MAX_FILE_SIZE,
- limit: 3,
- upload: handleImageUpload,
- onError: (error) => console.error("Upload failed:", error),
- }),
- ],
- content: {
- type: "doc",
- content: [
- {
- type: "paragraph",
- content: [],
- },
- ],
- },
- })
-
+interface SimpleEditorProps {
+ value?: object
+ onChange?: (json: object) => void
+ readOnly?: boolean
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ editor?: any
+}
+export default function SimpleEditor({ readOnly, editor }: SimpleEditorProps) {
return (
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx
index c5b5ead26d..75179eb7bb 100644
--- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx
+++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx
@@ -6,7 +6,11 @@ import { NodeViewWrapper } from "@tiptap/react"
import { Button } from "../../tiptap-ui-primitive/button"
import { CloseIcon } from "../../tiptap-icons/close-icon"
import "./image-upload-node.scss"
-import { focusNextNode, isValidPosition } from "../../../lib/tiptap-utils"
+import {
+ focusNextNode,
+ isValidPosition,
+ generateUUID,
+} from "../../../lib/tiptap-utils"
export interface FileItem {
/**
@@ -95,7 +99,7 @@ function useFileUpload(options: UploadOptions) {
}
const abortController = new AbortController()
- const fileId = crypto.randomUUID()
+ const fileId = generateUUID()
const newFileItem: FileItem = {
id: fileId,
@@ -471,12 +475,16 @@ export const ImageUploadNode: React.FC = (props) => {
}
})
- props.editor
- .chain()
- .focus()
- .deleteRange({ from: pos, to: pos + props.node.nodeSize })
- .insertContentAt(pos, imageNodes)
- .run()
+ const chain = props.editor.chain().focus()
+
+ // Remove the upload placeholder node
+ chain.deleteRange({ from: pos, to: pos + props.node.nodeSize })
+
+ // ✅ Insert each image node one by one
+ imageNodes.forEach((node, index) => {
+ chain.insertContentAt(pos + index, node)
+ })
+ chain.run()
focusNextNode(props.editor)
}
diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss
index 8faf836440..a32eacc49f 100644
--- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss
+++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss
@@ -29,7 +29,6 @@ body,
#root,
#app {
height: 100%;
- background-color: var(--tt-bg-color);
}
::-webkit-scrollbar {
@@ -55,9 +54,9 @@ body,
}
.simple-editor-wrapper {
+ overflow: auto;
width: 100vw;
height: 100vh;
- overflow: auto;
}
.simple-editor-content {
@@ -68,6 +67,11 @@ body,
display: flex;
flex-direction: column;
flex: 1;
+ margin-top: 10px;
+}
+
+.simple-editor-content-background {
+ background-color: white;
}
.simple-editor-content .tiptap.ProseMirror.simple-editor {
diff --git a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts
index 2c0f1bb813..4181ff0bae 100644
--- a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts
+++ b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts
@@ -379,7 +379,7 @@ export const handleImageUpload = async (
onProgress?.({ progress })
}
- return "/images/tiptap-ui-placeholder-image.jpg"
+ return "/mit-dome-2.jpg"
}
type ProtocolOptions = {
@@ -553,3 +553,16 @@ export function selectCurrentBlockContent(editor: Editor) {
}
}
}
+
+export function generateUUID(): string {
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
+ return crypto.randomUUID()
+ }
+
+ // Fallback: UUIDv4 using random numbers
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0
+ const v = c === "x" ? r : (r & 0x3) | 0x8
+ return v.toString(16)
+ })
+}
diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts
index a04bded224..8528e62f4c 100644
--- a/frontends/ol-components/src/index.ts
+++ b/frontends/ol-components/src/index.ts
@@ -172,6 +172,8 @@ export * from "./components/ThemeProvider/MITLearnGlobalStyles"
export { AppRouterCacheProvider as NextJsAppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter"
export { default as TiptapEditor } from "./components/TiptapEditor/TiptapEditor"
+export { default as TiptapEditorContainer } from "./components/TiptapEditor/EditorContainer"
+export type { JSONContent } from "@tiptap/core"
// /**
// * @deprecated Please use component from @mitodl/smoot-design instead