Skip to content

Commit

Permalink
feat(editor): Add basic canvas plugin with tldraw
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikey Stengel committed Dec 23, 2024
1 parent 9c6c86f commit 5699637
Show file tree
Hide file tree
Showing 66 changed files with 962 additions and 4 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 12 additions & 0 deletions apps/web/src/components/pages/editor/education-plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
faSquareRootVariable,
faPencilAlt,
faTarp,
// faPalette as canvasIcon,
faCaretSquareDown,
faCode,
faFilm,
Expand Down Expand Up @@ -217,6 +218,17 @@ const pluginData = [
image: 'video.png',
category: 'basic',
},
// TODO
// {
// title: 'Canvas',
// titleDe: 'Canvas',
// icon: canvasIcon,
// description: 'Draw complex shapes and diagrams',
// descriptionDe: 'Zeichne komplexe Formen und Diagrame',
// example: null,
// image: 'canvas.png',
// category: 'basic',
// },
{
title: 'Code',
titleDe: 'Code',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/serlo-editor-integration/create-plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { articlePlugin } from '@editor/plugins/article'
import { audioPlugin } from '@editor/plugins/audio'
import { createBlanksExercisePlugin } from '@editor/plugins/blanks-exercise'
import { createBoxPlugin } from '@editor/plugins/box'
import { canvasPlugin } from '@editor/plugins/canvas'
import { coursePlugin } from '@editor/plugins/course'
import { createDropzoneImagePlugin } from '@editor/plugins/dropzone-image'
import { equationsPlugin } from '@editor/plugins/equations'
Expand Down Expand Up @@ -55,6 +56,7 @@ export function createPlugins({ lang }: { lang: Instance }): PluginsWithData {
EditorPluginType.Audio,
EditorPluginType.ArticleIntroduction,
EditorPluginType.Box,
EditorPluginType.Canvas,
EditorPluginType.Course,
EditorPluginType.Equations,
EditorPluginType.Geogebra,
Expand Down Expand Up @@ -115,6 +117,7 @@ export function createPlugins({ lang }: { lang: Instance }): PluginsWithData {
},
{ type: EditorPluginType.Spoiler, plugin: createSpoilerPlugin(plugins) },
{ type: EditorPluginType.Box, plugin: createBoxPlugin(plugins) },
{ type: EditorPluginType.Canvas, plugin: canvasPlugin },
{ type: EditorPluginType.SerloTable, plugin: createSerloTablePlugin() },
{ type: EditorPluginType.Injection, plugin: injectionPlugin },
{ type: EditorPluginType.Equations, plugin: equationsPlugin },
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/serlo-editor-integration/create-renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { AnchorStaticRenderer } from '@editor/plugins/anchor/static'
import { ArticleStaticRenderer } from '@editor/plugins/article/static'
import { BoxStaticRenderer } from '@editor/plugins/box/static'
import { CanvasStaticRenderer } from '@editor/plugins/canvas/static'
import { ImageGalleryStaticRenderer } from '@editor/plugins/image-gallery/static'
import { RowsStaticRenderer } from '@editor/plugins/rows/static'
import { SpoilerStaticRenderer } from '@editor/plugins/spoiler/static'
Expand Down Expand Up @@ -161,6 +162,7 @@ export function createRenderers(): InitRenderersArgs {
renderer: DropzoneImageStaticRenderer,
},
{ type: EditorPluginType.Box, renderer: BoxStaticRenderer },
{ type: EditorPluginType.Canvas, renderer: CanvasStaticRenderer },
{ type: EditorPluginType.Course, renderer: CourseSerloStaticRenderer },
{ type: EditorPluginType.SerloTable, renderer: SerloTableStaticRenderer },
{
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"react": "^18.2.0",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-resizable": "^3.0.5"
"react-resizable": "^3.0.5",
"tldraw": "^3.6.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createBlanksExercisePlugin } from '@editor/plugins/blanks-exercise'
import { createBoxPlugin } from '@editor/plugins/box'
import { canvasPlugin } from '@editor/plugins/canvas'
import { createDropzoneImagePlugin } from '@editor/plugins/dropzone-image'
import { createEdusharingAssetPlugin } from '@editor/plugins/edusharing-asset'
import { equationsPlugin } from '@editor/plugins/equations'
Expand Down Expand Up @@ -75,6 +76,10 @@ export function createBasicPlugins(
type: EditorPluginType.Box,
plugin: createBoxPlugin(plugins),
},
{
type: EditorPluginType.Canvas,
plugin: canvasPlugin,
},
{
type: EditorPluginType.SerloTable,
plugin: createSerloTablePlugin(),
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/editor-integration/create-renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AnchorStaticRenderer } from '@editor/plugins/anchor/static'
import { ArticleStaticRenderer } from '@editor/plugins/article/static'
import { BlanksExerciseStaticRenderer } from '@editor/plugins/blanks-exercise/static'
import { BoxStaticRenderer } from '@editor/plugins/box/static'
import { CanvasStaticRenderer } from '@editor/plugins/canvas/static'
import { DropzoneImageStaticRenderer } from '@editor/plugins/dropzone-image/static'
import { EdusharingAssetStaticRenderer } from '@editor/plugins/edusharing-asset/static'
import { EquationsStaticRenderer } from '@editor/plugins/equations/static'
Expand Down Expand Up @@ -58,6 +59,7 @@ export function createRenderers(): InitRenderersArgs {
renderer: DropzoneImageStaticRenderer,
},
{ type: EditorPluginType.Box, renderer: BoxStaticRenderer },
{ type: EditorPluginType.Canvas, renderer: CanvasStaticRenderer },
{ type: EditorPluginType.SerloTable, renderer: SerloTableStaticRenderer },
{ type: EditorPluginType.Equations, renderer: EquationsStaticRenderer },
{
Expand Down
4 changes: 1 addition & 3 deletions packages/editor/src/editor-ui/editor-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ export function EditorModal({
)
}

// The moodle navigation bar has a z-index of 1030... We are using 1040 to make
// sure the modal can be on top and is not cut off
export const defaultModalOverlayStyles = cn(
'fixed bottom-0 left-0 right-0 top-0 z-[1040] bg-white bg-opacity-75'
'fixed bottom-0 left-0 right-0 top-0 z-[101] bg-white bg-opacity-75'
)
4 changes: 4 additions & 0 deletions packages/editor/src/i18n/strings/de/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const editStrings = {
anchorId: 'Sprungmarke (anchor id)',
emptyContentWarning: 'Boxen ohne Inhalt werden nicht angezeigt',
},
canvas: {
title: 'Canvas',
description: 'Erstelle eine Zeichnung oder ein Diagramm.',
},
dropzoneImage: {
title: 'Interaktives Bild (Ablagezonen)',
description:
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/i18n/strings/en/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const editStrings = {
anchorId: 'Anchor ID',
emptyContentWarning: 'Boxes without content will not be displayed',
},
canvas: {
title: 'Canvas',
description: 'Draw complex shapes and diagrams.',
},
dropzoneImage: {
title: 'Interactive Image (Dropzones)',
description:
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/package/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const defaultPlugins = [
EditorPluginType.Multimedia,
EditorPluginType.Spoiler,
EditorPluginType.Box,
EditorPluginType.Canvas,
EditorPluginType.SerloTable,
EditorPluginType.Equations,
EditorPluginType.Geogebra,
Expand Down
38 changes: 38 additions & 0 deletions packages/editor/src/plugins/canvas/editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useRef } from 'react'
import { Tldraw, TLEditorSnapshot } from 'tldraw'

// eslint-disable-next-line import/no-unassigned-import
import 'tldraw/tldraw.css'
import { type CanvasProps } from '.'
import { CanvasToolbar } from './toolbar'

export function CanvasEditor({ state, ...props }: CanvasProps) {
const { focused } = props
const initialSnapshot = useRef<TLEditorSnapshot | undefined>(
state.document.value
? (JSON.parse(state.document.value) as TLEditorSnapshot)
: undefined
)
return (
<div className="serlo-canvas-editor relative h-[600px] w-full rounded border">
{focused && <CanvasToolbar id={props.id} />}
<div className="h-full w-full">
<Tldraw
className="h-full w-full"
snapshot={initialSnapshot.current}
onMount={(editor) => {
if (!state.document.value) {
editor.updateInstanceState({ isReadonly: false })
}

editor.store.listen(() => {
const snapshot = editor.getSnapshot()
state.document.set(JSON.stringify(snapshot))
state.metadata.lastModified.set(Date.now())
})
}}
/>
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions packages/editor/src/plugins/canvas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CanvasEditor } from './editor'
import {
type EditorPlugin,
type EditorPluginProps,
object,
string,
number,
} from '../../plugin'

const canvasState = object({
document: string(''),
metadata: object({
lastModified: number(0),
}),
})

export type CanvasPluginState = typeof canvasState

export const canvasPlugin: EditorPlugin<CanvasPluginState> = {
Component: CanvasEditor,
config: {},
state: canvasState,
// We don't need onText for canvas
onText: undefined,
}

export type CanvasProps = EditorPluginProps<CanvasPluginState>

export interface SerializedTLStore {
// Serialized TLStoreSnapshot
document: string
metadata: {
lastModified: number
}
}
40 changes: 40 additions & 0 deletions packages/editor/src/plugins/canvas/renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { EditorCanvasDocument } from '@editor/types/editor-plugins'
import { Tldraw, TLEditorSnapshot } from 'tldraw'

// eslint-disable-next-line import/no-unassigned-import
import 'tldraw/tldraw.css'

interface CanvasRendererProps {
document: EditorCanvasDocument['state']['document']
}

export function CanvasRenderer({ document }: CanvasRendererProps) {
if (!document) return null

let snapshot: TLEditorSnapshot | undefined
try {
snapshot = JSON.parse(document) as TLEditorSnapshot
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse canvas document:', e)
return null
}

return (
<div className="serlo-canvas-renderer relative h-[600px] w-full rounded border">
<div className="h-full w-full">
<Tldraw
className="h-full w-full"
snapshot={snapshot}
components={{
TopPanel: () => null,
Toolbar: () => null,
}}
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: true })
}}
/>
</div>
</div>
)
}
10 changes: 10 additions & 0 deletions packages/editor/src/plugins/canvas/static.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CanvasRenderer } from '@editor/plugins/canvas/renderer'
import { EditorCanvasDocument } from '@editor/types/editor-plugins'

export function CanvasStaticRenderer({
state: { document },
}: EditorCanvasDocument) {
if (!document) return null

return <CanvasRenderer document={document} />
}
15 changes: 15 additions & 0 deletions packages/editor/src/plugins/canvas/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PluginToolbar } from '@editor/editor-ui/plugin-toolbar'
import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools'
import { EditorPluginType } from '@editor/types/editor-plugin-type'

export const CanvasToolbar = ({ id }: { id: string | undefined }) => {
return (
<PluginToolbar
pluginType={EditorPluginType.Canvas}
pluginSettings={<>{/* Custom toolbar buttons */}</>}
pluginControls={
<PluginDefaultTools pluginId={id || new Date().toString()} />
}
/>
)
}
5 changes: 5 additions & 0 deletions packages/editor/src/plugins/rows/utils/plugin-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const pluginMenuType = {
Highlight: EditorPluginType.Highlight,
Spoiler: EditorPluginType.Spoiler,
Box: EditorPluginType.Box,
Canvas: EditorPluginType.Canvas,
SerloTable: EditorPluginType.SerloTable,
Equations: EditorPluginType.Equations,
Geogebra: EditorPluginType.Geogebra,
Expand Down Expand Up @@ -237,6 +238,8 @@ const iconLookup: Record<PluginMenuType, string> = {
[pluginMenuType.Multimedia]: IconMultimedia,
[pluginMenuType.Video]: IconVideo,
[pluginMenuType.Box]: IconBox,
// TODO add Canvas icon
[pluginMenuType.Canvas]: IconBox,
[pluginMenuType.Equations]: IconEquation,
[pluginMenuType.Geogebra]: IconGeogebra,
[pluginMenuType.Highlight]: IconHighlight,
Expand Down Expand Up @@ -270,6 +273,8 @@ const iconComponentLookup: Record<PluginMenuType, React.ComponentType> = {
[pluginMenuType.Multimedia]: MultimediaIcon,
[pluginMenuType.Video]: VideoIcon,
[pluginMenuType.Box]: BoxIcon,
// TODO add Canvas icon
[pluginMenuType.Canvas]: BoxIcon,
[pluginMenuType.Equations]: EquationsIcon,
[pluginMenuType.Geogebra]: GeogebraIcon,
[pluginMenuType.Highlight]: HighlightIcon,
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/types/editor-plugin-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum EditorPluginType {
Audio = 'audio',
ArticleIntroduction = 'articleIntroduction',
Box = 'box',
Canvas = 'canvas',
Course = 'course',
Equations = 'equations',
Geogebra = 'geogebra',
Expand Down
6 changes: 6 additions & 0 deletions packages/editor/src/types/editor-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ArticlePluginState } from '@editor/plugins/article'
import type { AudioPluginState } from '@editor/plugins/audio'
import type { BlanksExerciseState } from '@editor/plugins/blanks-exercise'
import type { BoxPluginState } from '@editor/plugins/box'
import { CanvasPluginState } from '@editor/plugins/canvas'
import type { CoursePluginState } from '@editor/plugins/course'
import type { DropzoneImagePluginState } from '@editor/plugins/dropzone-image'
import type { EdusharingAssetState } from '@editor/plugins/edusharing-asset'
Expand Down Expand Up @@ -203,6 +204,11 @@ export interface EditorVideoDocument {
state: PrettyStaticState<VideoPluginState>
id?: string
}
export interface EditorCanvasDocument {
plugin: EditorPluginType.Canvas
state: PrettyStaticState<CanvasPluginState>
id?: string
}
export interface EditorAudioDocument {
plugin: EditorPluginType.Audio
state: PrettyStaticState<AudioPluginState>
Expand Down
Loading

0 comments on commit 5699637

Please sign in to comment.