mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 03:40:48 +00:00
Fix drawing bugs and process touch events (#7412)
Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
parent
a4a458b06c
commit
f1dbf21146
@ -16,7 +16,14 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { resizeObserver } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { drawing, type DrawingCmd, type DrawingData, type DrawingTool, type DrawTextCmd } from '../drawing'
|
||||
import {
|
||||
drawing,
|
||||
makeCommandId,
|
||||
type DrawingCmd,
|
||||
type DrawingData,
|
||||
type DrawingTool,
|
||||
type DrawTextCmd
|
||||
} from '../drawing'
|
||||
import DrawingBoardToolbar from './DrawingBoardToolbar.svelte'
|
||||
|
||||
export let active = false
|
||||
@ -38,7 +45,8 @@
|
||||
let oldReadonly: boolean
|
||||
let oldDrawings: DrawingData[]
|
||||
let modified = false
|
||||
let changingCmdIndex: number | undefined
|
||||
let changingCmdId: string | undefined
|
||||
let cmdEditor: HTMLDivElement | undefined
|
||||
|
||||
$: updateToolbarPosition(readonly, board, toolbar)
|
||||
$: updateEditableState(drawings, readonly)
|
||||
@ -63,14 +71,15 @@
|
||||
commands = []
|
||||
} else {
|
||||
// Edit current content as a new drawing
|
||||
commands = [...commands]
|
||||
commands = commands.map((cmd) => ({ ...cmd, id: cmd.id ?? makeCommandId() }))
|
||||
}
|
||||
modified = false
|
||||
}
|
||||
} else {
|
||||
commands = undefined
|
||||
}
|
||||
changingCmdIndex = undefined
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
oldDrawings = drawings
|
||||
oldReadonly = readonly
|
||||
}
|
||||
@ -105,33 +114,40 @@
|
||||
function addCommand (cmd: DrawingCmd): void {
|
||||
if (commands !== undefined) {
|
||||
commands = [...commands, cmd]
|
||||
changingCmdIndex = undefined
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
function showCommandProps (index: number): void {
|
||||
changingCmdIndex = index
|
||||
const anyCmd = commands?.[index]
|
||||
if (anyCmd?.type === 'text') {
|
||||
const cmd = anyCmd as DrawTextCmd
|
||||
penColor = cmd.color
|
||||
fontSize = cmd.fontSize
|
||||
function showCommandProps (id: string): void {
|
||||
changingCmdId = id
|
||||
for (const cmd of commands ?? []) {
|
||||
if (cmd.id === id) {
|
||||
if (cmd.type === 'text') {
|
||||
const textCmd = cmd as DrawTextCmd
|
||||
penColor = textCmd.color
|
||||
fontSize = textCmd.fontSize
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeCommand (index: number, cmd: DrawingCmd): void {
|
||||
function changeCommand (cmd: DrawingCmd): void {
|
||||
if (commands !== undefined) {
|
||||
commands = commands.map((c, i) => (i === index ? cmd : c))
|
||||
changingCmdIndex = undefined
|
||||
commands = commands.map((c) => (c.id === cmd.id ? cmd : c))
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCommand (index: number): void {
|
||||
function deleteCommand (id: string): void {
|
||||
if (commands !== undefined) {
|
||||
commands = commands.filter((_, i) => i !== index)
|
||||
changingCmdIndex = undefined
|
||||
commands = commands.filter((c) => c.id !== id)
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
@ -159,19 +175,23 @@
|
||||
penWidth,
|
||||
eraserWidth,
|
||||
fontSize,
|
||||
changingCmdIndex,
|
||||
changingCmdId,
|
||||
cmdAdded: addCommand,
|
||||
cmdChanging: showCommandProps,
|
||||
cmdChanged: changeCommand,
|
||||
cmdUnchanged: () => {
|
||||
changingCmdIndex = undefined
|
||||
changingCmdId = undefined
|
||||
},
|
||||
cmdDeleted: deleteCommand
|
||||
cmdDeleted: deleteCommand,
|
||||
editorCreated: (editor) => {
|
||||
cmdEditor = editor
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if !readonly}
|
||||
<DrawingBoardToolbar
|
||||
placeInside={toolbarInside}
|
||||
{cmdEditor}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
bind:penColor
|
||||
|
@ -53,6 +53,7 @@
|
||||
export let placeInside = false
|
||||
export let showPanTool = false
|
||||
export let toolbar: HTMLDivElement | undefined
|
||||
export let cmdEditor: HTMLDivElement | undefined
|
||||
|
||||
let colorSelector: HTMLInputElement
|
||||
let penColors: string[] = defaultColors
|
||||
@ -91,12 +92,14 @@
|
||||
penColors = penColors.filter((c: string) => c !== penColor)
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||
selectColor(penColors[0])
|
||||
focusEditor()
|
||||
break
|
||||
}
|
||||
case 'reset-colors': {
|
||||
penColors = defaultColors
|
||||
localStorage.removeItem(storageKey.colors)
|
||||
selectColor(penColors[0])
|
||||
focusEditor()
|
||||
break
|
||||
}
|
||||
case undefined: {
|
||||
@ -115,6 +118,7 @@
|
||||
penColors = [...penColors, penColor]
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||
}
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
function selectColor (color: string): void {
|
||||
@ -148,6 +152,15 @@
|
||||
|
||||
function updateFontSize (): void {
|
||||
localStorage.setItem(storageKey.fontSize, fontSize.toString())
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
function focusEditor (): void {
|
||||
setTimeout(() => {
|
||||
if (cmdEditor !== undefined) {
|
||||
cmdEditor.focus()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -243,6 +256,7 @@
|
||||
tool = 'pen'
|
||||
}
|
||||
selectColor(color)
|
||||
focusEditor()
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="colorIcon" style:background={color} />
|
||||
|
@ -30,16 +30,18 @@ export interface DrawingProps {
|
||||
eraserWidth?: number
|
||||
fontSize?: number
|
||||
defaultCursor?: string
|
||||
changingCmdIndex?: number
|
||||
changingCmdId?: string
|
||||
cmdAdded?: (cmd: DrawingCmd) => void
|
||||
cmdChanging?: (index: number) => void
|
||||
cmdUnchanged?: (index: number) => void
|
||||
cmdChanged?: (index: number, cmd: DrawingCmd) => void
|
||||
cmdDeleted?: (index: number) => void
|
||||
cmdChanging?: (id: string) => void
|
||||
cmdUnchanged?: (id: string) => void
|
||||
cmdChanged?: (cmd: DrawingCmd) => void
|
||||
cmdDeleted?: (id: string) => void
|
||||
editorCreated?: (editor: HTMLDivElement) => void
|
||||
panned?: (offset: Point) => void
|
||||
}
|
||||
|
||||
export interface DrawingCmd {
|
||||
id: string
|
||||
type: 'line' | 'text'
|
||||
}
|
||||
|
||||
@ -71,6 +73,10 @@ function avgPoint (p1: Point, p2: Point): Point {
|
||||
|
||||
const maxTextLength = 500
|
||||
|
||||
export const makeCommandId = (): string => {
|
||||
return crypto.randomUUID().toString()
|
||||
}
|
||||
|
||||
const crossSvg = `<svg height="8" width="8" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1.29 2.71 5.3 5.29-5.3 5.29c-.92.92.49 2.34 1.41 1.41l5.3-5.29 5.29 5.3c.92.92 2.34-.49 1.41-1.41l-5.29-5.3 5.3-5.29c.92-.93-.49-2.34-1.42-1.42l-5.29 5.3-5.29-5.3c-.93-.92-2.34.49-1.42 1.42z"/>
|
||||
</svg>`
|
||||
@ -294,13 +300,15 @@ export function drawing (
|
||||
draw.eraserWidth = props.eraserWidth ?? draw.eraserWidth
|
||||
draw.fontSize = props.fontSize ?? draw.fontSize
|
||||
draw.offset = props.offset ?? draw.offset
|
||||
|
||||
updateCanvasCursor()
|
||||
updateCanvasTouchAction()
|
||||
|
||||
interface LiveTextBox {
|
||||
pos: Point
|
||||
box: HTMLDivElement
|
||||
editor: HTMLDivElement
|
||||
cmdIndex: number
|
||||
cmdId: string
|
||||
}
|
||||
let liveTextBox: LiveTextBox | undefined
|
||||
|
||||
@ -328,6 +336,61 @@ export function drawing (
|
||||
})
|
||||
resizeObserver.observe(canvas)
|
||||
|
||||
let touchId: number | undefined
|
||||
|
||||
function findTouch (touches: TouchList, id: number | undefined = touchId): Touch | undefined {
|
||||
for (let i = 0; i < touches.length; i++) {
|
||||
const touch = touches[i]
|
||||
if (touch.identifier === id) {
|
||||
return touch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function touchToNodePoint (touch: Touch, node: HTMLElement): Point {
|
||||
const rect = node.getBoundingClientRect()
|
||||
return {
|
||||
x: touch.clientX - rect.left,
|
||||
y: touch.clientY - rect.top
|
||||
}
|
||||
}
|
||||
|
||||
function pointerToNodePoint (e: PointerEvent): Point {
|
||||
return { x: e.offsetX, y: e.offsetY }
|
||||
}
|
||||
|
||||
canvas.ontouchstart = (e) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const touch = e.changedTouches[0]
|
||||
touchId = touch.identifier
|
||||
drawStart(touchToNodePoint(touch, canvas))
|
||||
}
|
||||
|
||||
canvas.ontouchmove = (e) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const touch = findTouch(e.changedTouches)
|
||||
if (touch !== undefined) {
|
||||
drawContinue(touchToNodePoint(touch, canvas))
|
||||
}
|
||||
}
|
||||
|
||||
canvas.ontouchend = (e) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const touch = findTouch(e.changedTouches)
|
||||
if (touch !== undefined) {
|
||||
drawEnd(touchToNodePoint(touch, canvas))
|
||||
}
|
||||
touchId = undefined
|
||||
}
|
||||
|
||||
canvas.ontouchcancel = canvas.ontouchend
|
||||
|
||||
canvas.onpointerdown = (e) => {
|
||||
if (readonly) {
|
||||
return
|
||||
@ -337,16 +400,7 @@ export function drawing (
|
||||
}
|
||||
e.preventDefault()
|
||||
canvas.setPointerCapture(e.pointerId)
|
||||
|
||||
const x = e.offsetX
|
||||
const y = e.offsetY
|
||||
|
||||
draw.on = true
|
||||
draw.points = []
|
||||
prevPos = { x, y }
|
||||
if (draw.isDrawingTool()) {
|
||||
draw.addPoint(x, y)
|
||||
}
|
||||
drawStart(pointerToNodePoint(e))
|
||||
}
|
||||
|
||||
canvas.onpointermove = (e) => {
|
||||
@ -354,35 +408,7 @@ export function drawing (
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
const x = e.offsetX
|
||||
const y = e.offsetY
|
||||
|
||||
if (draw.isDrawingTool()) {
|
||||
const w = draw.cursorWidth()
|
||||
canvasCursor.style.left = `${x - w / 2}px`
|
||||
canvasCursor.style.top = `${y - w / 2}px`
|
||||
if (draw.on) {
|
||||
if (Math.hypot(prevPos.x - x, prevPos.y - y) < draw.minLineLength) {
|
||||
return
|
||||
}
|
||||
draw.drawLive(x, y)
|
||||
prevPos = { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
if (draw.on && draw.tool === 'pan') {
|
||||
requestAnimationFrame(() => {
|
||||
draw.offset.x += x - prevPos.x
|
||||
draw.offset.y += y - prevPos.y
|
||||
replayCommands()
|
||||
prevPos = { x, y }
|
||||
})
|
||||
}
|
||||
|
||||
if (draw.on && draw.tool === 'text') {
|
||||
prevPos = { x, y }
|
||||
}
|
||||
drawContinue(pointerToNodePoint(e))
|
||||
}
|
||||
|
||||
canvas.onpointerup = (e) => {
|
||||
@ -391,24 +417,11 @@ export function drawing (
|
||||
}
|
||||
e.preventDefault()
|
||||
canvas.releasePointerCapture(e.pointerId)
|
||||
if (draw.on) {
|
||||
if (draw.isDrawingTool()) {
|
||||
draw.drawLive(e.offsetX, e.offsetY, true)
|
||||
storeLineCommand()
|
||||
} else if (draw.tool === 'pan') {
|
||||
props.panned?.(draw.offset)
|
||||
} else if (draw.tool === 'text') {
|
||||
if (liveTextBox !== undefined) {
|
||||
storeTextCommand()
|
||||
} else {
|
||||
const cmdIndex = findTextCommand(prevPos)
|
||||
props.cmdChanging?.(cmdIndex)
|
||||
}
|
||||
}
|
||||
draw.on = false
|
||||
}
|
||||
drawEnd(pointerToNodePoint(e))
|
||||
}
|
||||
|
||||
canvas.onpointercancel = canvas.onpointerup
|
||||
|
||||
canvas.onpointerenter = () => {
|
||||
if (!readonly && draw.isDrawingTool()) {
|
||||
canvasCursor.style.visibility = 'visible'
|
||||
@ -421,26 +434,86 @@ export function drawing (
|
||||
}
|
||||
}
|
||||
|
||||
function findTextCommand (mousePos: Point): number {
|
||||
function drawStart (p: Point): void {
|
||||
draw.on = true
|
||||
draw.points = []
|
||||
prevPos = p
|
||||
if (draw.isDrawingTool()) {
|
||||
draw.addPoint(p.x, p.y)
|
||||
}
|
||||
}
|
||||
|
||||
function drawContinue (p: Point): void {
|
||||
if (draw.isDrawingTool()) {
|
||||
const w = draw.cursorWidth()
|
||||
canvasCursor.style.left = `${p.x - w / 2}px`
|
||||
canvasCursor.style.top = `${p.y - w / 2}px`
|
||||
if (draw.on) {
|
||||
if (Math.hypot(prevPos.x - p.x, prevPos.y - p.y) < draw.minLineLength) {
|
||||
return
|
||||
}
|
||||
draw.drawLive(p.x, p.y)
|
||||
prevPos = p
|
||||
}
|
||||
}
|
||||
|
||||
if (draw.on && draw.tool === 'pan') {
|
||||
requestAnimationFrame(() => {
|
||||
draw.offset.x += p.x - prevPos.x
|
||||
draw.offset.y += p.y - prevPos.y
|
||||
replayCommands()
|
||||
prevPos = p
|
||||
})
|
||||
}
|
||||
|
||||
if (draw.on && draw.tool === 'text') {
|
||||
prevPos = p
|
||||
}
|
||||
}
|
||||
|
||||
function drawEnd (p: Point): void {
|
||||
if (draw.on) {
|
||||
if (draw.isDrawingTool()) {
|
||||
draw.drawLive(p.x, p.y, true)
|
||||
storeLineCommand()
|
||||
} else if (draw.tool === 'pan') {
|
||||
props.panned?.(draw.offset)
|
||||
} else if (draw.tool === 'text') {
|
||||
if (liveTextBox !== undefined) {
|
||||
storeTextCommand()
|
||||
closeLiveTextBox()
|
||||
} else {
|
||||
const cmd = findTextCommand(prevPos)
|
||||
props.cmdChanging?.(cmd?.id ?? '')
|
||||
}
|
||||
}
|
||||
draw.on = false
|
||||
}
|
||||
}
|
||||
|
||||
function findTextCommand (mousePos: Point): DrawTextCmd | undefined {
|
||||
const pos = draw.mouseToCanvasPoint(mousePos)
|
||||
for (let i = commands.length - 1; i >= 0; i--) {
|
||||
const anyCmd = commands[i]
|
||||
if (anyCmd.type === 'text') {
|
||||
const cmd = anyCmd as DrawTextCmd
|
||||
if (draw.isPointInText(pos, cmd)) {
|
||||
return i
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
return undefined
|
||||
}
|
||||
|
||||
function makeLiveTextBox (cmdIndex: number): void {
|
||||
function makeLiveTextBox (cmdId: string): void {
|
||||
let pos = prevPos
|
||||
let existingCmd: DrawTextCmd | undefined
|
||||
if (cmdIndex >= 0 && commands[cmdIndex]?.type === 'text') {
|
||||
existingCmd = commands[cmdIndex] as DrawTextCmd
|
||||
pos = draw.canvasToMousePoint(existingCmd.pos)
|
||||
for (const cmd of commands) {
|
||||
if (cmd.id === cmdId && cmd.type === 'text') {
|
||||
existingCmd = cmd as DrawTextCmd
|
||||
pos = draw.canvasToMousePoint(existingCmd.pos)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const padding = 6
|
||||
@ -455,6 +528,7 @@ export function drawing (
|
||||
box.style.borderRadius = 'var(--small-BorderRadius)'
|
||||
box.style.padding = `${padding}px`
|
||||
box.style.background = 'var(--theme-popup-header)'
|
||||
box.style.touchAction = 'none'
|
||||
box.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
@ -513,19 +587,18 @@ export function drawing (
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
if (liveTextBox !== undefined) {
|
||||
const cmdIndex = liveTextBox.cmdIndex
|
||||
if (cmdIndex >= 0) {
|
||||
// reset changingCmdIndex in clients
|
||||
setTimeout(() => {
|
||||
props.cmdUnchanged?.(cmdIndex)
|
||||
}, 0)
|
||||
}
|
||||
const cmdId = liveTextBox.cmdId
|
||||
// reset changingCmdId in clients
|
||||
setTimeout(() => {
|
||||
props.cmdUnchanged?.(cmdId)
|
||||
}, 0)
|
||||
}
|
||||
closeLiveTextBox()
|
||||
replayCommands()
|
||||
} else if (e.key === 'Enter' && e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
storeTextCommand()
|
||||
closeLiveTextBox()
|
||||
}
|
||||
})
|
||||
box.appendChild(editor)
|
||||
@ -562,44 +635,43 @@ export function drawing (
|
||||
return handle
|
||||
}
|
||||
|
||||
const moveTextBox = (dx: number, dy: number): void => {
|
||||
let newX = box.offsetLeft + dx
|
||||
let newY = box.offsetTop + dy
|
||||
// For screenshots the canvas always has the same size as the underlying image
|
||||
// and we should not be able to drag the text box outside of the screenshot
|
||||
if (props.autoSize !== true) {
|
||||
newX = Math.max(0, newX)
|
||||
newY = Math.max(0, newY)
|
||||
if (newX + box.offsetWidth > node.clientWidth) {
|
||||
newX = node.clientWidth - box.offsetWidth
|
||||
}
|
||||
if (newY + box.offsetHeight > node.clientHeight) {
|
||||
newY = node.clientHeight - box.offsetHeight
|
||||
}
|
||||
}
|
||||
box.style.left = `${newX}px`
|
||||
box.style.top = `${newY}px`
|
||||
if (liveTextBox !== undefined) {
|
||||
liveTextBox.pos.x = newX + padding
|
||||
liveTextBox.pos.y = newY + padding
|
||||
}
|
||||
}
|
||||
|
||||
const dragHandle = makeHandle()
|
||||
dragHandle.style.left = `-${handleSize / 2}px`
|
||||
dragHandle.style.cursor = 'grab'
|
||||
dragHandle.style.touchAction = 'none'
|
||||
dragHandle.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault()
|
||||
dragHandle.style.cursor = 'grabbing'
|
||||
dragHandle.setPointerCapture(e.pointerId)
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
const dragStart = { x, y }
|
||||
let prevPos = { x: e.clientX, y: e.clientY }
|
||||
const pointerMove = (e: PointerEvent): void => {
|
||||
e.preventDefault()
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
const dx = x - dragStart.x
|
||||
const dy = y - dragStart.y
|
||||
dragStart.x = x
|
||||
dragStart.y = y
|
||||
let newX = box.offsetLeft + dx
|
||||
let newY = box.offsetTop + dy
|
||||
// For screenshots the canvas always has the same size as the underlying image
|
||||
// and we should not be able to drag the text box outside of the screenshot
|
||||
if (props.autoSize !== true) {
|
||||
newX = Math.max(0, newX)
|
||||
newY = Math.max(0, newY)
|
||||
if (newX + box.offsetWidth > node.clientWidth) {
|
||||
newX = node.clientWidth - box.offsetWidth
|
||||
}
|
||||
if (newY + box.offsetHeight > node.clientHeight) {
|
||||
newY = node.clientHeight - box.offsetHeight
|
||||
}
|
||||
}
|
||||
box.style.left = `${newX}px`
|
||||
box.style.top = `${newY}px`
|
||||
if (liveTextBox !== undefined) {
|
||||
liveTextBox.pos.x = newX + padding
|
||||
liveTextBox.pos.y = newY + padding
|
||||
}
|
||||
const p = { x: e.clientX, y: e.clientY }
|
||||
moveTextBox(p.x - prevPos.x, p.y - prevPos.y)
|
||||
prevPos = p
|
||||
}
|
||||
const pointerUp = (e: PointerEvent): void => {
|
||||
setTimeout(() => {
|
||||
@ -610,9 +682,37 @@ export function drawing (
|
||||
dragHandle.releasePointerCapture(e.pointerId)
|
||||
dragHandle.removeEventListener('pointermove', pointerMove)
|
||||
dragHandle.removeEventListener('pointerup', pointerUp)
|
||||
dragHandle.removeEventListener('pointercancel', pointerUp)
|
||||
}
|
||||
dragHandle.addEventListener('pointermove', pointerMove)
|
||||
dragHandle.addEventListener('pointerup', pointerUp)
|
||||
dragHandle.addEventListener('pointercancel', pointerUp)
|
||||
})
|
||||
dragHandle.addEventListener('touchstart', (e) => {
|
||||
dragHandle.style.cursor = 'grabbing'
|
||||
const touch = e.changedTouches[0]
|
||||
const touchId = touch.identifier
|
||||
let prevPos = touchToNodePoint(touch, dragHandle)
|
||||
const touchMove = (e: TouchEvent): void => {
|
||||
const touch = findTouch(e.changedTouches, touchId)
|
||||
if (touch !== undefined) {
|
||||
const p = touchToNodePoint(touch, dragHandle)
|
||||
moveTextBox(p.x - prevPos.x, p.y - prevPos.y)
|
||||
prevPos = p
|
||||
}
|
||||
}
|
||||
const touchEnd = (e: TouchEvent): void => {
|
||||
setTimeout(() => {
|
||||
editor.focus()
|
||||
}, 100)
|
||||
dragHandle.style.cursor = 'grab'
|
||||
dragHandle.removeEventListener('touchmove', touchMove)
|
||||
dragHandle.removeEventListener('touchend', touchEnd)
|
||||
dragHandle.removeEventListener('touchcancel', touchEnd)
|
||||
}
|
||||
dragHandle.addEventListener('touchmove', touchMove)
|
||||
dragHandle.addEventListener('touchend', touchEnd)
|
||||
dragHandle.addEventListener('touchcancel', touchEnd)
|
||||
})
|
||||
box.appendChild(dragHandle)
|
||||
|
||||
@ -622,20 +722,21 @@ export function drawing (
|
||||
deleteButton.innerHTML = crossSvg
|
||||
deleteButton.addEventListener('click', () => {
|
||||
node.removeChild(box)
|
||||
if (liveTextBox?.cmdIndex !== undefined) {
|
||||
props.cmdDeleted?.(liveTextBox.cmdIndex)
|
||||
if (liveTextBox?.cmdId !== undefined) {
|
||||
props.cmdDeleted?.(liveTextBox.cmdId)
|
||||
}
|
||||
liveTextBox = undefined
|
||||
})
|
||||
box.appendChild(deleteButton)
|
||||
|
||||
node.appendChild(box)
|
||||
liveTextBox = { box, editor, pos, cmdIndex }
|
||||
liveTextBox = { box, editor, pos, cmdId }
|
||||
updateLiveTextBox()
|
||||
setTimeout(() => {
|
||||
editor.focus()
|
||||
}, 100)
|
||||
selectAll()
|
||||
props.editorCreated?.(editor)
|
||||
}
|
||||
|
||||
function updateLiveTextBox (): void {
|
||||
@ -658,7 +759,9 @@ export function drawing (
|
||||
if (liveTextBox !== undefined) {
|
||||
const text = (liveTextBox.editor.innerText ?? '').trim()
|
||||
if (text !== '') {
|
||||
const cmdId = liveTextBox.cmdId
|
||||
const cmd: DrawTextCmd = {
|
||||
id: cmdId === '' ? makeCommandId() : cmdId,
|
||||
type: 'text',
|
||||
text,
|
||||
pos: draw.mouseToCanvasPoint(liveTextBox.pos),
|
||||
@ -666,10 +769,9 @@ export function drawing (
|
||||
fontFace: draw.fontFace,
|
||||
color: draw.penColor
|
||||
}
|
||||
const cmdIndex = liveTextBox.cmdIndex
|
||||
const notify = (): void => {
|
||||
if (cmdIndex >= 0) {
|
||||
props.cmdChanged?.(cmdIndex, cmd)
|
||||
if (cmdId !== '') {
|
||||
props.cmdChanged?.(cmd)
|
||||
} else {
|
||||
props.cmdAdded?.(cmd)
|
||||
}
|
||||
@ -680,7 +782,7 @@ export function drawing (
|
||||
notify()
|
||||
}
|
||||
} else {
|
||||
props.cmdUnchanged?.(liveTextBox.cmdIndex)
|
||||
props.cmdUnchanged?.(liveTextBox.cmdId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -689,6 +791,7 @@ export function drawing (
|
||||
if (draw.points.length > 0) {
|
||||
const erasing = draw.tool === 'erase'
|
||||
const cmd: DrawLineCmd = {
|
||||
id: makeCommandId(),
|
||||
type: 'line',
|
||||
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
|
||||
erasing,
|
||||
@ -726,13 +829,17 @@ export function drawing (
|
||||
}
|
||||
}
|
||||
|
||||
function updateCanvasTouchAction (): void {
|
||||
canvas.style.touchAction = readonly ? 'unset' : 'none'
|
||||
}
|
||||
|
||||
function replayCommands (): void {
|
||||
draw.ctx.reset()
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
if (liveTextBox?.cmdIndex === i) {
|
||||
for (const cmd of commands) {
|
||||
if (cmd.id !== undefined && liveTextBox?.cmdId === cmd.id) {
|
||||
continue
|
||||
}
|
||||
draw.drawCommand(commands[i])
|
||||
draw.drawCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
@ -774,9 +881,10 @@ export function drawing (
|
||||
}
|
||||
if (props.readonly !== readonly) {
|
||||
readonly = props.readonly ?? false
|
||||
updateCanvasTouchAction()
|
||||
updateCursor = true
|
||||
}
|
||||
if (props.changingCmdIndex === undefined) {
|
||||
if (props.changingCmdId === undefined) {
|
||||
if (liveTextBox !== undefined) {
|
||||
storeTextCommand(true)
|
||||
closeLiveTextBox()
|
||||
@ -784,9 +892,9 @@ export function drawing (
|
||||
}
|
||||
} else {
|
||||
if (liveTextBox === undefined) {
|
||||
makeLiveTextBox(props.changingCmdIndex)
|
||||
makeLiveTextBox(props.changingCmdId)
|
||||
replay = true
|
||||
} else if (liveTextBox.cmdIndex !== props.changingCmdIndex) {
|
||||
} else if (liveTextBox.cmdId !== props.changingCmdId) {
|
||||
storeTextCommand(true)
|
||||
closeLiveTextBox()
|
||||
replay = true
|
||||
|
@ -13,7 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DrawingBoardToolbar, DrawingCmd, DrawingTool, DrawTextCmd, drawing } from '@hcengineering/presentation'
|
||||
import {
|
||||
DrawingBoardToolbar,
|
||||
DrawingCmd,
|
||||
DrawingTool,
|
||||
DrawTextCmd,
|
||||
drawing,
|
||||
makeCommandId
|
||||
} from '@hcengineering/presentation'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Array as YArray, Map as YMap } from 'yjs'
|
||||
@ -34,11 +41,14 @@
|
||||
let fontSize: number
|
||||
let commands: DrawingCmd[] = []
|
||||
let offset: { x: number, y: number } = { x: 0, y: 0 }
|
||||
let changingCmdIndex: number | undefined
|
||||
let changingCmdId: string | undefined
|
||||
let cmdEditor: HTMLDivElement | undefined
|
||||
let toolbar: HTMLDivElement
|
||||
let oldSelected = false
|
||||
let oldReadonly = false
|
||||
|
||||
$: onSelectedChanged(selected)
|
||||
$: onReadonlyChanged(readonly)
|
||||
|
||||
function listenSavedCommands (): void {
|
||||
commands = savedCmds.toArray()
|
||||
@ -50,25 +60,79 @@
|
||||
// offset = savedProps.get('offset')
|
||||
}
|
||||
|
||||
function showCommandProps (index: number): void {
|
||||
changingCmdIndex = index
|
||||
const anyCmd = commands[index]
|
||||
if (anyCmd?.type === 'text') {
|
||||
const cmd = anyCmd as DrawTextCmd
|
||||
penColor = cmd.color
|
||||
fontSize = cmd.fontSize
|
||||
function showCommandProps (id: string): void {
|
||||
changingCmdId = id
|
||||
for (const cmd of commands) {
|
||||
if (cmd.id === id) {
|
||||
if (cmd.type === 'text') {
|
||||
const textCmd = cmd as DrawTextCmd
|
||||
penColor = textCmd.color
|
||||
fontSize = textCmd.fontSize
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeCommand (cmd: DrawingCmd): void {
|
||||
let index = -1
|
||||
for (let i = 0; i < savedCmds.length; i++) {
|
||||
if (savedCmds.get(i).id === cmd.id) {
|
||||
savedCmds.delete(i)
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
savedCmds.insert(index, [cmd])
|
||||
} else {
|
||||
savedCmds.push([cmd])
|
||||
}
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
}
|
||||
|
||||
function deleteCommand (id: string): void {
|
||||
for (let i = 0; i < savedCmds.length; i++) {
|
||||
if (savedCmds.get(i).id === id) {
|
||||
savedCmds.delete(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
}
|
||||
|
||||
function onSelectedChanged (selected: boolean): void {
|
||||
if (oldSelected !== selected) {
|
||||
if (oldSelected && !selected && changingCmdIndex !== undefined) {
|
||||
changingCmdIndex = undefined
|
||||
if (oldSelected && !selected && changingCmdId !== undefined) {
|
||||
changingCmdId = undefined
|
||||
cmdEditor = undefined
|
||||
}
|
||||
oldSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
function onReadonlyChanged (readonly: boolean): void {
|
||||
if (oldReadonly !== readonly) {
|
||||
if (!readonly) {
|
||||
let allHaveIds = true
|
||||
for (let i = 0; i < savedCmds.length; i++) {
|
||||
if (savedCmds.get(i).id === undefined) {
|
||||
allHaveIds = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!allHaveIds) {
|
||||
const cmds = savedCmds.toArray()
|
||||
savedCmds.delete(0, savedCmds.length)
|
||||
savedCmds.push(cmds.map((cmd) => ({ ...cmd, id: cmd.id ?? makeCommandId() })))
|
||||
}
|
||||
}
|
||||
oldReadonly = readonly
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
commands = savedCmds.toArray()
|
||||
// offset = savedProps.get('offset')
|
||||
@ -109,27 +173,23 @@
|
||||
penWidth,
|
||||
eraserWidth,
|
||||
fontSize,
|
||||
changingCmdIndex,
|
||||
changingCmdId,
|
||||
cmdAdded: (cmd) => {
|
||||
savedCmds.push([cmd])
|
||||
changingCmdIndex = undefined
|
||||
changingCmdId = undefined
|
||||
},
|
||||
cmdChanging: showCommandProps,
|
||||
cmdChanged: (index, cmd) => {
|
||||
savedCmds.delete(index)
|
||||
savedCmds.insert(index, [cmd])
|
||||
changingCmdIndex = undefined
|
||||
},
|
||||
cmdChanged: changeCommand,
|
||||
cmdUnchanged: () => {
|
||||
changingCmdIndex = undefined
|
||||
},
|
||||
cmdDeleted: (index) => {
|
||||
savedCmds.delete(index)
|
||||
changingCmdIndex = undefined
|
||||
changingCmdId = undefined
|
||||
},
|
||||
cmdDeleted: deleteCommand,
|
||||
panned: (newOffset) => {
|
||||
offset = newOffset
|
||||
// savedProps.set('offset', offset)
|
||||
},
|
||||
editorCreated: (editor) => {
|
||||
cmdEditor = editor
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -142,6 +202,7 @@
|
||||
<DrawingBoardToolbar
|
||||
placeInside={true}
|
||||
showPanTool={true}
|
||||
{cmdEditor}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
bind:penColor
|
||||
|
@ -35,23 +35,43 @@
|
||||
let resizer: HTMLElement
|
||||
let startY: number
|
||||
let resizedHeight: number | undefined
|
||||
let resizerTouchId: number | undefined
|
||||
let loading = true
|
||||
let loadingTimer: any
|
||||
|
||||
function resizeStart (y: number): void {
|
||||
const height = node.attrs.height ?? defaultHeight
|
||||
startY = y - height
|
||||
resizedHeight = height
|
||||
}
|
||||
|
||||
function resizeContinue (y: number): void {
|
||||
resizedHeight = Math.max(minHeight, y - startY)
|
||||
resizedHeight = Math.min(maxHeight, resizedHeight)
|
||||
}
|
||||
|
||||
function resizeFinish (): void {
|
||||
if (resizedHeight !== undefined) {
|
||||
if (typeof getPos === 'function') {
|
||||
const tr = editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height: resizedHeight })
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
resizedHeight = undefined
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
resizer.addEventListener('pointercancel', onResizerPointerUp)
|
||||
resizeStart(e.clientY)
|
||||
}
|
||||
|
||||
function onResizerPointerMove (e: PointerEvent): void {
|
||||
e.preventDefault()
|
||||
resizedHeight = Math.max(minHeight, e.clientY - startY)
|
||||
resizedHeight = Math.min(maxHeight, resizedHeight)
|
||||
resizeContinue(e.clientY)
|
||||
}
|
||||
|
||||
function onResizerPointerUp (e: PointerEvent): void {
|
||||
@ -59,11 +79,35 @@
|
||||
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)
|
||||
resizer.removeEventListener('pointercancel', onResizerPointerUp)
|
||||
resizeFinish()
|
||||
}
|
||||
|
||||
function onResizerTouchStart (e: TouchEvent): void {
|
||||
const touch = e.changedTouches[0]
|
||||
resizerTouchId = touch.identifier
|
||||
resizer.addEventListener('touchmove', onResizerTouchMove)
|
||||
resizer.addEventListener('touchend', onResizerTouchEnd)
|
||||
resizer.addEventListener('touchcancel', onResizerTouchEnd)
|
||||
resizeStart(touch.clientY)
|
||||
}
|
||||
|
||||
function onResizerTouchMove (e: TouchEvent): void {
|
||||
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||
const touch = e.changedTouches[i]
|
||||
if (touch.identifier === resizerTouchId) {
|
||||
resizeContinue(touch.clientY)
|
||||
return
|
||||
}
|
||||
}
|
||||
resizedHeight = undefined
|
||||
}
|
||||
|
||||
function onResizerTouchEnd (): void {
|
||||
resizer.removeEventListener('touchmove', onResizerTouchMove)
|
||||
resizer.removeEventListener('touchend', onResizerTouchEnd)
|
||||
resizer.removeEventListener('touchcancel', onResizerTouchEnd)
|
||||
resizerTouchId = undefined
|
||||
resizeFinish()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@ -110,7 +154,12 @@
|
||||
/>
|
||||
</div>
|
||||
{#if selected}
|
||||
<div class="handle resizer" bind:this={resizer} on:pointerdown={onResizerPointerDown}>
|
||||
<div
|
||||
class="handle resizer"
|
||||
bind:this={resizer}
|
||||
on:pointerdown={onResizerPointerDown}
|
||||
on:touchstart={onResizerTouchStart}
|
||||
>
|
||||
<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"
|
||||
@ -147,6 +196,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
touch-action: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
Loading…
Reference in New Issue
Block a user