From f69d5ede0e841b8410592bc0af6ad91fafa89ee9 Mon Sep 17 00:00:00 2001 From: Chunosov Date: Wed, 4 Dec 2024 02:56:59 +0700 Subject: [PATCH] Save drawing boards separately from document (#7257) Signed-off-by: Nikolay Chunosov --- .../src/components/DrawingBoardToolbar.svelte | 30 ++- packages/presentation/src/drawing.ts | 4 +- packages/ui/src/components/Dialog.svelte | 4 + .../components/CollaborativeTextEditor.svelte | 62 ++++-- .../src/components/DrawingBoardEditor.svelte | 139 ++++++++++++ .../components/DrawingBoardNodeView.svelte | 176 +++++++++++++++ .../src/components/DrawingBoardPopup.svelte | 89 +------- .../src/components/extension/drawingBoard.ts | 209 +++--------------- 8 files changed, 435 insertions(+), 278 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte create mode 100644 plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte diff --git a/packages/presentation/src/components/DrawingBoardToolbar.svelte b/packages/presentation/src/components/DrawingBoardToolbar.svelte index 1d5f793615..856b96405e 100644 --- a/packages/presentation/src/components/DrawingBoardToolbar.svelte +++ b/packages/presentation/src/components/DrawingBoardToolbar.svelte @@ -80,7 +80,7 @@ case 'add-color': { if (colorSelector !== undefined) { colorSelector.value = penColor - colorSelector.click() + colorSelector.showPicker() } break } @@ -130,8 +130,8 @@ if (!penColors.includes(penColor)) { penColor = penColors[0] ?? defaultColor } - penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '4') - eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '30') + penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '6') + eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '50') }) function updatePenWidth (): void { @@ -181,9 +181,25 @@ {/if}
{#if tool === 'pen'} - + {:else} - + {/if}
{#each penColors as color} @@ -247,4 +263,8 @@ width: 0; opacity: 0; } + + .widthSelector { + width: 80px; + } diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index cc9a97b402..fd459fda00 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -295,7 +295,6 @@ export function drawing ( } }) prevPos = { x, y } - props.panned?.(draw.offset) }) } } @@ -310,6 +309,8 @@ export function drawing ( if (draw.isDrawingTool()) { draw.drawLive(e.offsetX, e.offsetY, true) storeCommand() + } else if (draw.tool === 'pan') { + props.panned?.(draw.offset) } draw.on = false } @@ -347,6 +348,7 @@ export function drawing ( canvas.style.cursor = props.defaultCursor ?? 'default' } else if (draw.isDrawingTool()) { canvas.style.cursor = 'none' + canvasCursor.style.visibility = 'visible' const erasing = draw.tool === 'erase' const w = draw.cursorWidth() canvasCursor.style.background = erasing ? 'none' : draw.penColor diff --git a/packages/ui/src/components/Dialog.svelte b/packages/ui/src/components/Dialog.svelte index ef8f85087d..9679461d9f 100644 --- a/packages/ui/src/components/Dialog.svelte +++ b/packages/ui/src/components/Dialog.svelte @@ -35,6 +35,10 @@ const dispatch = createEventDispatcher() + export function maximize (): void { + toggleFullSize = true + } + let fullSize: boolean = false let toggleFullSize: boolean = fullSize $: needFullSize = checkAdaptiveMatching($deviceInfo.size, 'md') diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index 938e5d7714..711a7c4b34 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -16,7 +16,15 @@ --> + +{#if savedCmds !== undefined && savedProps !== undefined} + {#if loading} +
+ + +
+ {:else} +
{ + savedCmds.push([cmd]) + }, + panned: (offset) => { + savedProps.set('offset', offset) + } + }} + > + {#if grabFocus} + + + + {/if} + {#if !readonly} + { + savedCmds.delete(0, savedCmds.length) + savedProps.set('offset', { x: 0, y: 0 }) + }} + /> + {/if} + +
+ {/if} +{/if} + + diff --git a/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte b/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte new file mode 100644 index 0000000000..6c8d20b80e --- /dev/null +++ b/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte @@ -0,0 +1,176 @@ + + + +{#if savedBoard?.commands !== undefined && savedBoard?.props !== undefined} + + +
+
+ {#if selected} +
+ + + +
+
+ + + +
+ {/if} +
+
+{/if} + + diff --git a/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte b/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte index 75348ad5e2..2d894be680 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte @@ -13,109 +13,36 @@ // limitations under the License. --> {#if savedCmds !== undefined && savedProps !== undefined} { dispatch('close') }} > -
{ - savedCmds.push([cmd]) - }, - panned: (offset) => { - savedProps.set('offset', offset) - } - }} - > - - - - - { - savedCmds.delete(0, savedCmds.length) - savedProps.set('offset', { x: 0, y: 0 }) - }} - /> -
+
{/if} diff --git a/plugins/text-editor-resources/src/components/extension/drawingBoard.ts b/plugins/text-editor-resources/src/components/extension/drawingBoard.ts index c1476599cb..e01d31a569 100644 --- a/plugins/text-editor-resources/src/components/extension/drawingBoard.ts +++ b/plugins/text-editor-resources/src/components/extension/drawingBoard.ts @@ -13,41 +13,26 @@ // limitations under the License. // -import { type DrawingCmd, type DrawingProps, drawing } from '@hcengineering/presentation' +import { type DrawingCmd } from '@hcengineering/presentation' import { showPopup } from '@hcengineering/ui' import { type Editor, mergeAttributes, Node } from '@tiptap/core' import { NodeSelection } from '@tiptap/pm/state' -import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs' +import type { Array as YArray, Map as YMap } from 'yjs' +import DrawingBoardNodeView from '../DrawingBoardNodeView.svelte' import DrawingBoardPopup from '../DrawingBoardPopup.svelte' - -const defaultHeight = 500 -const maxHeight = 1000 -const minHeight = 100 +import { SvelteNodeViewRenderer } from '../node-view' export interface DrawingBoardOptions { - ydoc?: YDoc + getSavedBoard: (id: string) => SavedBoard } -const scribbleSvg = ` - -` -const chevronSvg = ` - -` - -interface SavedBoard { +export interface SavedBoard { commands: YArray props: YMap + loading: boolean } -function getSavedBoard (ydoc: YDoc | undefined, id: string): SavedBoard { - const board = ydoc?.getMap(`drawing-board-${id}`) - const commands = board?.get('commands') as YArray - const props = board?.get('props') as YMap - return { commands, props } -} - -function showBoardPopup (board: SavedBoard, editor: Editor): void { +export function showBoardPopup (board: SavedBoard, editor: Editor): void { if (board.commands !== undefined && board.props !== undefined) { showPopup( DrawingBoardPopup, @@ -76,23 +61,35 @@ export const DrawingBoardExtension = Node.create({ group: 'block', + draggable: true, + renderHTML ({ HTMLAttributes }) { return ['div', mergeAttributes({ 'data-type': this.name }, HTMLAttributes)] }, + parseHTML () { + return [ + { + tag: 'div[data-type="drawingBoard"]' + } + ] + }, + addOptions () { return { - ydoc: undefined + getSavedBoard: (id) => ({}) as any as SavedBoard } }, addAttributes () { return { id: { - renderHTML: (attrs) => ({ 'data-id': attrs.id }) + renderHTML: (attrs) => ({ 'data-id': attrs.id }), + parseHTML: (element) => element.getAttribute('data-id') }, height: { - renderHTML: (attrs) => ({ 'data-height': attrs.height }) + renderHTML: (attrs) => ({ 'data-height': attrs.height }), + parseHTML: (element) => parseInt(element.getAttribute('data-height') ?? '500') } } }, @@ -105,8 +102,10 @@ export const DrawingBoardExtension = Node.create({ if (state.selection instanceof NodeSelection) { const node = state.selection.node if (node?.type.name === this.name) { - const board = getSavedBoard(this.options.ydoc, node.attrs.id) - showBoardPopup(board, this.editor) + const board = this.options.getSavedBoard(node.attrs.id) + if (!board.loading) { + showBoardPopup(board, this.editor) + } return true } } @@ -122,155 +121,11 @@ export const DrawingBoardExtension = Node.create({ }, addNodeView () { - return ({ node, getPos }) => { - const board = getSavedBoard(this.options.ydoc, node.attrs.id) - if (board.commands === undefined || board.props === undefined) { - return {} + return SvelteNodeViewRenderer(DrawingBoardNodeView, { + contentAs: 'div', + componentProps: { + getSavedBoard: this.options.getSavedBoard } - - const normalBorder = '1px solid var(--theme-navpanel-border)' - const selectedBorder = '1px solid var(--theme-editbox-focus-border)' - - const dom = document.createElement('div') - dom.id = node.attrs.id - dom.contentEditable = 'false' - dom.style.width = '100%' - dom.style.position = 'relative' - dom.style.height = `${node.attrs.height ?? defaultHeight}px` - dom.style.border = normalBorder - dom.style.borderRadius = 'var(--small-BorderRadius)' - dom.style.backgroundColor = 'var(--theme-navpanel-color)' - dom.ondblclick = () => { - showBoardPopup(board, this.editor) - } - - const drawingProps: DrawingProps = { - readonly: true, - autoSize: true, - commands: board.commands.toArray(), - commandCount: board.commands.length, - offset: board.props.get('offset') - } - - const { canvas, update: updateDrawing } = drawing(dom, drawingProps) - if (canvas === undefined || updateDrawing === undefined) { - return {} - } - dom.appendChild(canvas) - - const listenSavedCommands = (): void => { - let update = false - if (board.commands.length === 0) { - update = true - drawingProps.commands = [] - } else if (board.commands.length > drawingProps.commands.length) { - update = true - for (let i = drawingProps.commands.length; i < board.commands.length; i++) { - drawingProps.commands.push(board.commands.get(i)) - } - } - if (update) { - drawingProps.commandCount = board.commands.length - updateDrawing(drawingProps) - } - } - const listenSavedProps = (): void => { - drawingProps.offset = board.props.get('offset') - updateDrawing(drawingProps) - } - board.commands.observe(listenSavedCommands) - board.props.observe(listenSavedProps) - - const button = document.createElement('div') - button.style.visibility = 'hidden' - button.style.cursor = 'pointer' - button.style.position = 'absolute' - button.style.top = '0.3rem' - button.style.right = '0.3rem' - button.style.width = '2.2rem' - button.style.height = '2.2rem' - button.style.color = 'var(--primary-button-color)' - button.style.backgroundColor = 'var(--primary-button-default)' - button.style.borderRadius = 'var(--extra-small-BorderRadius)' - button.style.border = normalBorder - button.style.display = 'flex' - button.style.alignItems = 'center' - button.style.justifyContent = 'center' - button.innerHTML = scribbleSvg - button.onclick = () => { - showBoardPopup(board, this.editor) - } - dom.appendChild(button) - - const resizer = document.createElement('div') - resizer.style.position = 'absolute' - resizer.style.bottom = '0' - resizer.style.left = 'calc(50% - 4rem)' - resizer.style.width = '8rem' - resizer.style.height = '0.6rem' - resizer.style.cursor = 'row-resize' - resizer.style.color = 'var(--global-on-accent-TextColor)' - resizer.style.backgroundColor = 'var(--global-accent-IconColor)' - resizer.style.border = selectedBorder - resizer.style.display = 'flex' - resizer.style.alignItems = 'center' - resizer.style.justifyContent = 'center' - resizer.style.visibility = 'hidden' - resizer.style.borderTopLeftRadius = 'var(--small-BorderRadius)' - resizer.style.borderTopRightRadius = 'var(--small-BorderRadius)' - resizer.style.borderBottom = 'none' - resizer.style.opacity = '0.5' - resizer.innerHTML = chevronSvg - resizer.onpointerenter = () => { - resizer.style.opacity = '1' - } - resizer.onpointerleave = () => { - resizer.style.opacity = '0.5' - } - resizer.onpointerdown = (e: PointerEvent): void => { - e.preventDefault() - resizer.setPointerCapture(e.pointerId) - const { offsetHeight } = dom - const startY = e.clientY - offsetHeight - let height = node.attrs.height ?? defaultHeight - const onPointerMove = (e: PointerEvent): void => { - e.preventDefault() - height = Math.max(minHeight, e.clientY - startY) - height = Math.min(maxHeight, height) - dom.style.height = `${height}px` - } - const onPointerUp = (e: PointerEvent): void => { - e.preventDefault() - resizer.releasePointerCapture(e.pointerId) - if (typeof getPos === 'function') { - const tr = this.editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height }) - this.editor.view.dispatch(tr) - } - dom.removeEventListener('pointermove', onPointerMove) - dom.removeEventListener('pointerup', onPointerUp) - } - dom.addEventListener('pointermove', onPointerMove) - dom.addEventListener('pointerup', onPointerUp) - } - dom.appendChild(resizer) - - return { - dom, - selectNode: () => { - dom.style.border = selectedBorder - button.style.visibility = 'visible' - resizer.style.visibility = 'visible' - }, - deselectNode: () => { - dom.style.border = normalBorder - button.style.visibility = 'hidden' - resizer.style.visibility = 'hidden' - }, - destroy: () => { - board.commands.unobserve(listenSavedCommands) - board.props.unobserve(listenSavedProps) - } - } - } + }) } })