Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb2850d4fb | |||
| 87597b7701 | |||
| ae64b7764d | |||
| ed58fa228e | |||
| 8ceed77081 | |||
| d2ba54ec3a | |||
| 3416e2bdf1 |
@@ -52,6 +52,7 @@
|
|||||||
"react-intersection-observer": "9.16.0",
|
"react-intersection-observer": "9.16.0",
|
||||||
"react-select": "5.10.1",
|
"react-select": "5.10.1",
|
||||||
"styled-components": "6.1.18",
|
"styled-components": "6.1.18",
|
||||||
|
"tldraw": "3.13.1",
|
||||||
"use-debounce": "10.0.4",
|
"use-debounce": "10.0.4",
|
||||||
"y-protocols": "1.0.6",
|
"y-protocols": "1.0.6",
|
||||||
"yjs": "*",
|
"yjs": "*",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { randomColor } from '../utils';
|
|||||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||||
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||||
import { CalloutBlock, DividerBlock } from './custom-blocks';
|
import { CalloutBlock, DividerBlock } from './custom-blocks';
|
||||||
|
import { DrawBlock } from './custom-blocks/DrawBlock';
|
||||||
|
|
||||||
export const blockNoteSchema = withPageBreak(
|
export const blockNoteSchema = withPageBreak(
|
||||||
BlockNoteSchema.create({
|
BlockNoteSchema.create({
|
||||||
@@ -35,6 +36,7 @@ export const blockNoteSchema = withPageBreak(
|
|||||||
...defaultBlockSpecs,
|
...defaultBlockSpecs,
|
||||||
callout: CalloutBlock,
|
callout: CalloutBlock,
|
||||||
divider: DividerBlock,
|
divider: DividerBlock,
|
||||||
|
draw: DrawBlock,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
+2
@@ -15,6 +15,7 @@ import {
|
|||||||
getCalloutReactSlashMenuItems,
|
getCalloutReactSlashMenuItems,
|
||||||
getDividerReactSlashMenuItems,
|
getDividerReactSlashMenuItems,
|
||||||
} from './custom-blocks';
|
} from './custom-blocks';
|
||||||
|
import { getDrawReactSlashMenuItems } from './custom-blocks/DrawBlock';
|
||||||
|
|
||||||
export const BlockNoteSuggestionMenu = () => {
|
export const BlockNoteSuggestionMenu = () => {
|
||||||
const editor = useBlockNoteEditor<DocsBlockSchema>();
|
const editor = useBlockNoteEditor<DocsBlockSchema>();
|
||||||
@@ -30,6 +31,7 @@ export const BlockNoteSuggestionMenu = () => {
|
|||||||
getPageBreakReactSlashMenuItems(editor),
|
getPageBreakReactSlashMenuItems(editor),
|
||||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||||
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
|
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
|
||||||
|
getDrawReactSlashMenuItems(editor, t, basicBlocksName),
|
||||||
),
|
),
|
||||||
query,
|
query,
|
||||||
),
|
),
|
||||||
|
|||||||
+341
@@ -0,0 +1,341 @@
|
|||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
import {
|
||||||
|
ReactRendererProps,
|
||||||
|
defaultProps,
|
||||||
|
insertOrUpdateBlock,
|
||||||
|
} from '@blocknote/core';
|
||||||
|
import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react';
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Editor,
|
||||||
|
TLEventMapHandler,
|
||||||
|
TLStore,
|
||||||
|
Tldraw,
|
||||||
|
getSnapshot,
|
||||||
|
loadSnapshot,
|
||||||
|
} from 'tldraw';
|
||||||
|
import 'tldraw/tldraw.css';
|
||||||
|
|
||||||
|
import { Box, Icon } from '@/components';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../../types';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { clear } from 'console';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ----------------------------------------------------------------------------------
|
||||||
|
* Collaborative **Draw** block – backed by a Y‑js document synced through
|
||||||
|
* `@hocuspocus/provider` (see `useProviderStore`).
|
||||||
|
* ----------------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Each Draw block owns its own Y‑Doc, identified by `roomId` (persisted in `propSchema`).
|
||||||
|
* The block serialises a base‑64‑encoded Y‑js update (`drawingData`) so newcomers see
|
||||||
|
* the latest snapshot _immediately_, without waiting for the websocket connection.
|
||||||
|
*/
|
||||||
|
export const DrawBlock = createReactBlockSpec(
|
||||||
|
{
|
||||||
|
type: 'draw',
|
||||||
|
propSchema: {
|
||||||
|
textAlignment: defaultProps.textAlignment,
|
||||||
|
backgroundColor: defaultProps.backgroundColor,
|
||||||
|
roomId: { default: `drawing-${Date.now()}` },
|
||||||
|
drawingData: { default: '' },
|
||||||
|
changeHistory: { default: '' },
|
||||||
|
lastChange: { default: '' },
|
||||||
|
increment: { default: 0 }, // Increment to force re-rendering
|
||||||
|
},
|
||||||
|
content: 'inline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: ({ block, editor: editorBN }) => {
|
||||||
|
const [editor, setEditor] = useState<Editor>();
|
||||||
|
|
||||||
|
const setAppToState = useCallback((editor: Editor) => {
|
||||||
|
setEditor(editor);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const [storeEvents, setStoreEvents] = useState<string[]>([]);
|
||||||
|
console.log('Loading saved drawing data');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (block.props.drawingData && editor) {
|
||||||
|
console.log('DDData');
|
||||||
|
try {
|
||||||
|
const drawingData = JSON.parse(block.props.drawingData);
|
||||||
|
//const drawingData = block.props.drawingData;
|
||||||
|
|
||||||
|
// Update: Using the non-deprecated method to load the snapshot
|
||||||
|
// Instead of editor.store.loadSnapshot(drawingData)
|
||||||
|
loadSnapshot(editor.store, drawingData);
|
||||||
|
|
||||||
|
console.log('Successfully loaded drawing data');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load drawing data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [block.props.drawingData, editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved drawing data if available
|
||||||
|
|
||||||
|
function logChangeEvent(eventInfo: string, changeData: any = null) {
|
||||||
|
console.log(eventInfo);
|
||||||
|
|
||||||
|
// Get current properties
|
||||||
|
// const currentProps = { ...block.props };
|
||||||
|
|
||||||
|
// // Get current changeHistory or initialize empty array
|
||||||
|
// const currentHistory = Array.isArray(currentProps.changeHistory)
|
||||||
|
// ? currentProps.changeHistory
|
||||||
|
// : [];
|
||||||
|
|
||||||
|
// // Create a change record with timestamp
|
||||||
|
// const changeRecord = {
|
||||||
|
// timestamp: new Date().toISOString(),
|
||||||
|
// event: eventInfo,
|
||||||
|
// data: changeData,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Create new props object with all updated values
|
||||||
|
// const updatedProps = {
|
||||||
|
// ...currentProps,
|
||||||
|
// changeHistory: [...currentHistory, changeRecord],
|
||||||
|
// lastChange: changeRecord,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Add drawingData if available
|
||||||
|
// if (changeData?.drawingData) {
|
||||||
|
// updatedProps.drawingData = changeData.drawingData;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Update the block with the new props
|
||||||
|
// console.log('Updating block with props:', updatedProps);
|
||||||
|
|
||||||
|
// //editorBN.updateBlock(block, { props: updatedProps });
|
||||||
|
|
||||||
|
// if (timeoutId.current) {
|
||||||
|
// clearTimeout(timeoutId.current);
|
||||||
|
// }
|
||||||
|
// timeoutId.current = setTimeout(() => {
|
||||||
|
// editorBN.updateBlock(block, {
|
||||||
|
// props: updatedProps,
|
||||||
|
// });
|
||||||
|
// }, 300);
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutId.current) {
|
||||||
|
clearTimeout(timeoutId.current);
|
||||||
|
}
|
||||||
|
timeoutId.current = setTimeout(() => {
|
||||||
|
const snapshot = getSnapshot(editor.store);
|
||||||
|
//const snapshot = JSON.stringify(editor.store.serialize());
|
||||||
|
|
||||||
|
console.log('Captured drawing snapshot:', snapshot);
|
||||||
|
|
||||||
|
// Only update drawingData property to avoid multiple updates
|
||||||
|
const currentProps = { ...block.props };
|
||||||
|
|
||||||
|
editorBN.updateBlock(block, {
|
||||||
|
props: {
|
||||||
|
drawingData: JSON.stringify(snapshot),
|
||||||
|
increment: currentProps.increment + 1, // Increment to force re-rendering
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
//setStoreEvents((events) => [...events, eventInfo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//[1]
|
||||||
|
const handleChangeEvent: TLEventMapHandler<'change'> = (change) => {
|
||||||
|
// Added
|
||||||
|
for (const record of Object.values(change.changes.added)) {
|
||||||
|
if (record.typeName === 'shape') {
|
||||||
|
logChangeEvent(`created shape (${record.type})`, {
|
||||||
|
action: 'created',
|
||||||
|
shapeType: record.type,
|
||||||
|
shape: record,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated
|
||||||
|
for (const [from, to] of Object.values(change.changes.updated)) {
|
||||||
|
if (
|
||||||
|
from.typeName === 'instance' &&
|
||||||
|
to.typeName === 'instance' &&
|
||||||
|
from.currentPageId !== to.currentPageId
|
||||||
|
) {
|
||||||
|
logChangeEvent(
|
||||||
|
`changed page (${from.currentPageId}, ${to.currentPageId})`,
|
||||||
|
{
|
||||||
|
action: 'changedPage',
|
||||||
|
fromPageId: from.currentPageId,
|
||||||
|
toPageId: to.currentPageId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
from.id.startsWith('shape') &&
|
||||||
|
to.id.startsWith('shape')
|
||||||
|
) {
|
||||||
|
let diff = _.reduce(
|
||||||
|
from,
|
||||||
|
(result: any[], value, key: string) =>
|
||||||
|
_.isEqual(value, to[key])
|
||||||
|
? result
|
||||||
|
: result.concat([key, to[key]]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const diffObj = {};
|
||||||
|
|
||||||
|
if (diff?.[0] === 'props') {
|
||||||
|
diff = _.reduce(
|
||||||
|
from.props,
|
||||||
|
(result: any[], value, key) =>
|
||||||
|
_.isEqual(value, to.props[key])
|
||||||
|
? result
|
||||||
|
: result.concat([key, to.props[key]]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert diff array to object for better storage
|
||||||
|
for (let i = 0; i < diff.length; i += 2) {
|
||||||
|
diffObj[diff[i]] = diff[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logChangeEvent(`updated shape (${JSON.stringify(diff)})`, {
|
||||||
|
action: 'updated',
|
||||||
|
shapeId: from.id,
|
||||||
|
changes: diffObj,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed
|
||||||
|
for (const record of Object.values(change.changes.removed)) {
|
||||||
|
if (record.typeName === 'shape') {
|
||||||
|
logChangeEvent(`deleted shape (${record.type})`, {
|
||||||
|
action: 'deleted',
|
||||||
|
shapeType: record.type,
|
||||||
|
shape: record,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the entire drawing state periodically when changes occur
|
||||||
|
// if (
|
||||||
|
// Object.keys(change.changes.added).length > 0 ||
|
||||||
|
// Object.keys(change.changes.updated).length > 0 ||
|
||||||
|
// Object.keys(change.changes.removed).length > 0
|
||||||
|
// ) {
|
||||||
|
// // Capture the current drawing state if available
|
||||||
|
// if (editor.store) {
|
||||||
|
// try {
|
||||||
|
// // Update: Using the non-deprecated method to get the snapshot
|
||||||
|
// // Instead of
|
||||||
|
// //
|
||||||
|
// const snapshot = getSnapshot(editor.store);
|
||||||
|
// //const snapshot = JSON.stringify(editor.store.serialize());
|
||||||
|
|
||||||
|
// console.log('Captured drawing snapshot:', snapshot);
|
||||||
|
|
||||||
|
// // Only update drawingData property to avoid multiple updates
|
||||||
|
// const currentProps = { ...block.props };
|
||||||
|
|
||||||
|
// if (timeoutId.current) {
|
||||||
|
// clearTimeout(timeoutId.current);
|
||||||
|
// }
|
||||||
|
// timeoutId.current = setTimeout(() => {
|
||||||
|
// editorBN.updateBlock(block, {
|
||||||
|
// props: {
|
||||||
|
// ...currentProps,
|
||||||
|
// drawingData: snapshot,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }, 300);
|
||||||
|
|
||||||
|
// console.log('Drawing snapshot updated');
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Failed to capture drawing snapshot:', error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
const cleanupFunction = editor.store.listen(handleChangeEvent, {
|
||||||
|
source: 'user',
|
||||||
|
scope: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanupFunction();
|
||||||
|
};
|
||||||
|
}, [block, editor, editorBN]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box style={{ width: '100%', height: 300 }}>
|
||||||
|
{/*
|
||||||
|
* We deliberately pass the TL‑store directly. TLDraw will observe the
|
||||||
|
* changes – including those coming over the wire – and re‑render.
|
||||||
|
*/}
|
||||||
|
<Tldraw onMount={setAppToState} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slash‑menu helper → inserts a new collaborative draw block with a unique `roomId`.
|
||||||
|
*/
|
||||||
|
export const getDrawReactSlashMenuItems = (
|
||||||
|
editor: DocsBlockNoteEditor,
|
||||||
|
t: TFunction<'translation', undefined>,
|
||||||
|
group: string,
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
title: t('Draw'),
|
||||||
|
onItemClick: () => {
|
||||||
|
insertOrUpdateBlock(editor, {
|
||||||
|
type: 'draw',
|
||||||
|
props: {
|
||||||
|
roomId: `drawing-${Date.now()}`,
|
||||||
|
drawingData: null,
|
||||||
|
changeHistory: [],
|
||||||
|
lastChange: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
aliases: ['draw'],
|
||||||
|
group,
|
||||||
|
icon: <Icon iconName="draw" $size="18px" />,
|
||||||
|
subtext: t('Add a collaborative canvas'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatting‑toolbar item so users can transform an existing block into a Draw block.
|
||||||
|
*/
|
||||||
|
export const getDrawFormattingToolbarItems = (
|
||||||
|
t: TFunction<'translation', undefined>,
|
||||||
|
): BlockTypeSelectItem => ({
|
||||||
|
name: t('Draw'),
|
||||||
|
type: 'draw',
|
||||||
|
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
||||||
|
isSelected: (block) => block.type === 'draw',
|
||||||
|
});
|
||||||
+940
-27
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user