Save drawing boards separately from document (#7257)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
Chunosov 2024-12-04 02:56:59 +07:00 committed by GitHub
parent 6d31873779
commit f69d5ede0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 435 additions and 278 deletions

View File

@ -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}
<div class="divider buttons-divider" />
{#if tool === 'pen'}
<input type="range" min={2} max={20} step={2} bind:value={penWidth} on:change={updatePenWidth} />
<input
class="widthSelector"
type="range"
min={2}
max={18}
step={4}
bind:value={penWidth}
on:change={updatePenWidth}
/>
{:else}
<input type="range" min={10} max={100} step={10} bind:value={eraserWidth} on:change={updateEraserWidth} />
<input
class="widthSelector"
type="range"
min={20}
max={110}
step={30}
bind:value={eraserWidth}
on:change={updateEraserWidth}
/>
{/if}
<div class="divider buttons-divider" />
{#each penColors as color}
@ -247,4 +263,8 @@
width: 0;
opacity: 0;
}
.widthSelector {
width: 80px;
}
</style>

View File

@ -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

View File

@ -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')

View File

@ -16,7 +16,15 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { type Doc, generateId, makeDocCollabId } from '@hcengineering/core'
import {
type Blob,
Class,
type CollaborativeDoc,
type Doc,
type Ref,
generateId,
makeDocCollabId
} from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import {
getAttribute,
@ -24,7 +32,8 @@
getFileUrl,
getImageSize,
imageSizeToRatio,
KeyedAttribute
KeyedAttribute,
DrawingCmd
} from '@hcengineering/presentation'
import { markupToJSON } from '@hcengineering/text'
import {
@ -45,7 +54,7 @@
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import Placeholder from '@tiptap/extension-placeholder'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'
import { Doc as YDoc } from 'yjs'
import { Completion } from '../Completion'
import { deleteAttachment } from '../command/deleteAttachment'
@ -66,7 +75,7 @@
import TextEditorToolbar from './TextEditorToolbar.svelte'
import { noSelectionRender, renderCursor } from './editor/collaboration'
import { defaultEditorAttributes } from './editor/editorProps'
import { DrawingBoardExtension } from './extension/drawingBoard'
import { DrawingBoardExtension, SavedBoard } from './extension/drawingBoard'
import { EmojiExtension } from './extension/emoji'
import { FileUploadExtension } from './extension/fileUploadExt'
import { ImageUploadExtension } from './extension/imageUploadExt'
@ -364,7 +373,7 @@
editor.commands.setHorizontalRule()
break
case 'drawing-board':
makeNewDrawingBoard(pos)
editor.commands.insertContentAt(pos, { type: 'drawingBoard', attrs: { id: generateId() } })
break
}
}
@ -374,14 +383,39 @@
remoteProvider.awareness?.setLocalStateField('lastUpdate', Date.now())
}
function makeNewDrawingBoard (pos: number): void {
const id = generateId()
ydoc.getArray('drawing-board-registry').push([id])
const drawing = ydoc.getMap(`drawing-board-${id}`)
drawing.set('commands', new YArray())
drawing.set('props', new YMap())
editor.commands.insertContentAt(pos, { type: 'drawingBoard', attrs: { id } })
editor.commands.showDrawingBoardPopup()
interface SavedBoardRaw {
ydoc: YDoc
localProvider: Provider
remoteProvider: Provider
localSynced: boolean
remoteSynced: boolean
}
const savedBoards: Record<string, SavedBoardRaw> = {}
function getSavedBoard (id: string): SavedBoard {
let board = savedBoards[id]
if (board === undefined) {
const ydoc = new YDoc({ guid: id })
// We don't have a real class for boards,
// but collaborator only needs a string id
// which is produced from such an id-object
const collabId: CollaborativeDoc = {
objectClass: 'DrawingBoard' as Ref<Class<Doc>>,
objectId: id as Ref<Doc>,
objectAttr: 'content'
}
const localProvider = createLocalProvider(ydoc, collabId)
const remoteProvider = createRemoteProvider(ydoc, collabId, id as Ref<Blob>)
savedBoards[id] = { ydoc, localProvider, remoteProvider, localSynced: false, remoteSynced: false }
void localProvider.loaded.then(() => (savedBoards[id].localSynced = true))
void remoteProvider.loaded.then(() => (savedBoards[id].remoteSynced = true))
board = savedBoards[id]
}
return {
props: board.ydoc.getMap('props'),
commands: board.ydoc.getArray<DrawingCmd>('commands'),
loading: !board.localSynced || !board.remoteSynced
}
}
onMount(async () => {
@ -432,7 +466,7 @@
}
}),
EmojiExtension,
DrawingBoardExtension.configure({ ydoc }),
DrawingBoardExtension.configure({ getSavedBoard }),
...extensions
],
parseOptions: {

View File

@ -0,0 +1,139 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DrawingBoardToolbar, DrawingCmd, DrawingTool, drawing } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui'
import { onMount, onDestroy } from 'svelte'
import { Array as YArray, Map as YMap } from 'yjs'
export let savedCmds: YArray<DrawingCmd>
export let savedProps: YMap<any>
export let grabFocus = false
export let resizeable = false
export let height: number | undefined = undefined
export let readonly = false
export let selected = false
export let loading = false
let tool: DrawingTool
let penColor: string
let penWidth: number
let eraserWidth: number
let commandCount: number
let commands: DrawingCmd[] = []
let offset: { x: number, y: number }
let toolbar: HTMLDivElement
function listenSavedCommands (): void {
if (savedCmds.length === 0) {
commands = []
} else {
for (let i = commands.length; i < savedCmds.length; i++) {
commands.push(savedCmds.get(i))
}
}
commandCount = savedCmds.length
}
function listenSavedProps (): void {
offset = savedProps.get('offset')
}
onMount(() => {
commands = savedCmds.toArray()
offset = savedProps.get('offset')
savedCmds.observe(listenSavedCommands)
savedProps.observe(listenSavedProps)
})
onDestroy(() => {
savedCmds.unobserve(listenSavedCommands)
savedProps.unobserve(listenSavedProps)
})
</script>
{#if savedCmds !== undefined && savedProps !== undefined}
{#if loading}
<div
class="board"
class:selected
style:flex-grow={resizeable ? undefined : '1'}
style:height={resizeable ? `${height}px` : undefined}
>
<Loading />
<slot />
</div>
{:else}
<div
class="board"
class:selected
style:flex-grow={resizeable ? undefined : '1'}
style:height={resizeable ? `${height}px` : undefined}
use:drawing={{
readonly,
autoSize: true,
commandCount,
commands,
offset,
tool,
penColor,
penWidth,
eraserWidth,
cmdAdded: (cmd) => {
savedCmds.push([cmd])
},
panned: (offset) => {
savedProps.set('offset', offset)
}
}}
>
{#if grabFocus}
<!-- grab focus from the editor -->
<!-- svelte-ignore a11y-autofocus -->
<input style:opacity="0" autoFocus />
{/if}
{#if !readonly}
<DrawingBoardToolbar
placeInside={true}
showPanTool={true}
bind:toolbar
bind:tool
bind:penColor
bind:penWidth
bind:eraserWidth
on:clear={() => {
savedCmds.delete(0, savedCmds.length)
savedProps.set('offset', { x: 0, y: 0 })
}}
/>
{/if}
<slot />
</div>
{/if}
{/if}
<style lang="scss">
.board {
position: relative;
width: 100%;
background-color: var(--theme-navpanel-color);
border: 1px solid var(--theme-navpanel-border);
border-radius: var(--small-BorderRadius);
&.selected {
border: 1px solid var(--theme-editbox-focus-border);
}
}
</style>

View File

@ -0,0 +1,176 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Button, IconScribble } from '@hcengineering/ui'
import { Node } from '@tiptap/pm/model'
import { Editor } from '@tiptap/core'
import { onDestroy, onMount } from 'svelte'
import { showBoardPopup, SavedBoard } from './extension/drawingBoard'
import NodeViewWrapper from './node-view/NodeViewWrapper.svelte'
import DrawingBoardEditor from './DrawingBoardEditor.svelte'
export let getSavedBoard: (id: string) => SavedBoard
export let node: Node
export let editor: Editor
export let selected: boolean
export let getPos: any
const defaultHeight = 500
const maxHeight = 1000
const minHeight = 100
let savedBoard: SavedBoard
let resizer: HTMLElement
let startY: number
let resizedHeight: number | undefined
let loading = true
let loadingTimer: any
function onResizerPointerDown (e: PointerEvent): void {
e.preventDefault()
const height = node.attrs.height ?? defaultHeight
startY = e.clientY - height
resizedHeight = height
resizer.setPointerCapture(e.pointerId)
resizer.addEventListener('pointermove', onResizerPointerMove)
resizer.addEventListener('pointerup', onResizerPointerUp)
}
function onResizerPointerMove (e: PointerEvent): void {
e.preventDefault()
resizedHeight = Math.max(minHeight, e.clientY - startY)
resizedHeight = Math.min(maxHeight, resizedHeight)
}
function onResizerPointerUp (e: PointerEvent): void {
e.preventDefault()
resizer.releasePointerCapture(e.pointerId)
resizer.removeEventListener('pointermove', onResizerPointerMove)
resizer.removeEventListener('pointerup', onResizerPointerUp)
if (typeof getPos === 'function') {
const tr = editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height: resizedHeight })
editor.view.dispatch(tr)
}
resizedHeight = undefined
}
onMount(() => {
let delay = 100
const getBoard = (): void => {
loadingTimer = undefined
savedBoard = getSavedBoard(node.attrs.id)
loading = savedBoard.loading
if (loading) {
loadingTimer = setTimeout(getBoard, delay)
delay *= 1.5
}
}
getBoard()
})
onDestroy(() => {
if (loadingTimer !== undefined) {
clearTimeout(loadingTimer)
}
})
</script>
{#if savedBoard?.commands !== undefined && savedBoard?.props !== undefined}
<NodeViewWrapper data-drag-handle="" data-type="drawingBoard" data-id={node.attrs.id}>
<DrawingBoardEditor
savedCmds={savedBoard.commands}
savedProps={savedBoard.props}
resizeable={true}
readonly={!selected}
{loading}
{selected}
height={resizedHeight ?? node.attrs.height ?? defaultHeight}
>
<div class="openButtonContainer">
<Button
kind={selected ? 'primary' : 'ghost'}
icon={IconScribble}
disabled={loading}
on:click={() => {
showBoardPopup(savedBoard, editor)
}}
/>
</div>
{#if selected}
<div class="handle resizer" bind:this={resizer} on:pointerdown={onResizerPointerDown}>
<svg viewBox="0 0 60 4" height="4" width="60" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="m60 2a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z"
/>
</svg>
</div>
<div class="handle drag">
<svg viewBox="0 0 4 28" height="28" width="4" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="m2 28c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"
/>
</svg>
</div>
{/if}
</DrawingBoardEditor>
</NodeViewWrapper>
{/if}
<style lang="scss">
.openButtonContainer {
z-index: 1;
position: absolute;
top: 0.3rem;
right: 0.3rem;
}
.handle {
z-index: 1;
position: absolute;
color: var(--global-on-accent-TextColor);
background-color: var(--global-accent-IconColor);
border: 1px solid var(--theme-editbox-focus-border);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
&:hover {
opacity: 1;
}
}
.resizer {
bottom: 0;
left: calc(50% - 4rem);
width: 8rem;
height: 0.6rem;
cursor: row-resize;
border-top-left-radius: var(--small-BorderRadius);
border-top-right-radius: var(--small-BorderRadius);
border-bottom: none;
}
.drag {
left: -0.6rem;
top: calc(50% - 2rem);
width: 0.6rem;
height: 4rem;
cursor: move;
border-top-left-radius: var(--small-BorderRadius);
border-bottom-left-radius: var(--small-BorderRadius);
border-right: none;
}
</style>

View File

@ -13,109 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import { DrawingBoardToolbar, DrawingCmd, DrawingTool, drawing } from '@hcengineering/presentation'
import { DrawingCmd } from '@hcengineering/presentation'
import textEditor from '@hcengineering/text-editor'
import { Dialog, FocusHandler, createFocusManager } from '@hcengineering/ui'
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { Array as YArray, Map as YMap } from 'yjs'
import DrawingBoardEditor from './DrawingBoardEditor.svelte'
export let savedCmds: YArray<DrawingCmd>
export let savedProps: YMap<any>
export let fullSize = false
const manager = createFocusManager()
const dispatch = createEventDispatcher()
let tool: DrawingTool
let penColor: string
let penWidth: number
let eraserWidth: number
let commandCount: number
let commands: DrawingCmd[] = []
let offset: { x: number, y: number }
let toolbar: HTMLDivElement
function listenSavedCommands (): void {
if (savedCmds.length === 0) {
commands = []
} else {
for (let i = commands.length; i < savedCmds.length; i++) {
commands.push(savedCmds.get(i))
}
}
commandCount = savedCmds.length
let dialog: Dialog
$: if (dialog !== undefined) {
dialog.maximize()
}
function listenSavedProps (): void {
offset = savedProps.get('offset')
}
onMount(() => {
if (fullSize) {
dispatch('fullsize')
}
commands = savedCmds.toArray()
offset = savedProps.get('offset')
savedCmds.observe(listenSavedCommands)
savedProps.observe(listenSavedProps)
})
onDestroy(() => {
savedCmds.unobserve(listenSavedCommands)
savedProps.unobserve(listenSavedProps)
})
</script>
{#if savedCmds !== undefined && savedProps !== undefined}
<FocusHandler {manager} />
<Dialog
isFullSize
label={textEditor.string.DrawingBoard}
padding="0"
bind:this={dialog}
on:fullsize
on:close={() => {
dispatch('close')
}}
>
<div
style:position="relative"
style:flex-grow="1"
use:drawing={{
readonly: false,
autoSize: true,
commandCount,
commands,
offset,
tool,
penColor,
penWidth,
eraserWidth,
cmdAdded: (cmd) => {
savedCmds.push([cmd])
},
panned: (offset) => {
savedProps.set('offset', offset)
}
}}
>
<!-- grab focus from the editor -->
<!-- svelte-ignore a11y-autofocus -->
<input style:opacity="0" autoFocus />
<DrawingBoardToolbar
placeInside={true}
showPanTool={true}
bind:toolbar
bind:tool
bind:penColor
bind:penWidth
bind:eraserWidth
on:clear={() => {
savedCmds.delete(0, savedCmds.length)
savedProps.set('offset', { x: 0, y: 0 })
}}
/>
</div>
<DrawingBoardEditor {savedCmds} {savedProps} grabFocus />
</Dialog>
{/if}

View File

@ -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 = `<svg width="20" height="20" viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="m1.07 4.9-1.07-.54c2.22-4.44 8.42-5.88 7.08-2.5-.94 2.35-5.16 5.95-5.44 8.11-.2 1.51 2.63 2.18 5.69 1.33.47-4.95 4.09-7.4 6.01-6.27 2.17 1.28.85 5.17-4.83 7.15.38 2.86 3.93 1.06 5.53.29l.54 1.07c-6.13 3.07-7.1.29-7.24-1.01-3.72.93-7.21-.14-6.88-2.72.34-2.66 4.66-6.26 5.51-8.4.37-.87-3.43.54-4.9 3.49zm7.53 5.98c4.03-1.59 5.28-4.14 4.13-4.81-.64-.38-3.36.35-4.13 4.81z" />
</svg>`
const chevronSvg = `<svg height="4" viewBox="0 0 60 4" width="60" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="m60 2a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z" />
</svg>`
interface SavedBoard {
export interface SavedBoard {
commands: YArray<DrawingCmd>
props: YMap<any>
loading: boolean
}
function getSavedBoard (ydoc: YDoc | undefined, id: string): SavedBoard {
const board = ydoc?.getMap(`drawing-board-${id}`)
const commands = board?.get('commands') as YArray<DrawingCmd>
const props = board?.get('props') as YMap<any>
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<DrawingBoardOptions>({
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<DrawingBoardOptions>({
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<DrawingBoardOptions>({
},
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)
}
}
}
})
}
})