mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-07 16:30:49 +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 { Analytics } from '@hcengineering/analytics'
|
||||||
import { resizeObserver } from '@hcengineering/ui'
|
import { resizeObserver } from '@hcengineering/ui'
|
||||||
import { onDestroy } from 'svelte'
|
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'
|
import DrawingBoardToolbar from './DrawingBoardToolbar.svelte'
|
||||||
|
|
||||||
export let active = false
|
export let active = false
|
||||||
@ -38,7 +45,8 @@
|
|||||||
let oldReadonly: boolean
|
let oldReadonly: boolean
|
||||||
let oldDrawings: DrawingData[]
|
let oldDrawings: DrawingData[]
|
||||||
let modified = false
|
let modified = false
|
||||||
let changingCmdIndex: number | undefined
|
let changingCmdId: string | undefined
|
||||||
|
let cmdEditor: HTMLDivElement | undefined
|
||||||
|
|
||||||
$: updateToolbarPosition(readonly, board, toolbar)
|
$: updateToolbarPosition(readonly, board, toolbar)
|
||||||
$: updateEditableState(drawings, readonly)
|
$: updateEditableState(drawings, readonly)
|
||||||
@ -63,14 +71,15 @@
|
|||||||
commands = []
|
commands = []
|
||||||
} else {
|
} else {
|
||||||
// Edit current content as a new drawing
|
// Edit current content as a new drawing
|
||||||
commands = [...commands]
|
commands = commands.map((cmd) => ({ ...cmd, id: cmd.id ?? makeCommandId() }))
|
||||||
}
|
}
|
||||||
modified = false
|
modified = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
commands = undefined
|
commands = undefined
|
||||||
}
|
}
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
|
cmdEditor = undefined
|
||||||
oldDrawings = drawings
|
oldDrawings = drawings
|
||||||
oldReadonly = readonly
|
oldReadonly = readonly
|
||||||
}
|
}
|
||||||
@ -105,33 +114,40 @@
|
|||||||
function addCommand (cmd: DrawingCmd): void {
|
function addCommand (cmd: DrawingCmd): void {
|
||||||
if (commands !== undefined) {
|
if (commands !== undefined) {
|
||||||
commands = [...commands, cmd]
|
commands = [...commands, cmd]
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
|
cmdEditor = undefined
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCommandProps (index: number): void {
|
function showCommandProps (id: string): void {
|
||||||
changingCmdIndex = index
|
changingCmdId = id
|
||||||
const anyCmd = commands?.[index]
|
for (const cmd of commands ?? []) {
|
||||||
if (anyCmd?.type === 'text') {
|
if (cmd.id === id) {
|
||||||
const cmd = anyCmd as DrawTextCmd
|
if (cmd.type === 'text') {
|
||||||
penColor = cmd.color
|
const textCmd = cmd as DrawTextCmd
|
||||||
fontSize = cmd.fontSize
|
penColor = textCmd.color
|
||||||
|
fontSize = textCmd.fontSize
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeCommand (index: number, cmd: DrawingCmd): void {
|
function changeCommand (cmd: DrawingCmd): void {
|
||||||
if (commands !== undefined) {
|
if (commands !== undefined) {
|
||||||
commands = commands.map((c, i) => (i === index ? cmd : c))
|
commands = commands.map((c) => (c.id === cmd.id ? cmd : c))
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
|
cmdEditor = undefined
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCommand (index: number): void {
|
function deleteCommand (id: string): void {
|
||||||
if (commands !== undefined) {
|
if (commands !== undefined) {
|
||||||
commands = commands.filter((_, i) => i !== index)
|
commands = commands.filter((c) => c.id !== id)
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
|
cmdEditor = undefined
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,19 +175,23 @@
|
|||||||
penWidth,
|
penWidth,
|
||||||
eraserWidth,
|
eraserWidth,
|
||||||
fontSize,
|
fontSize,
|
||||||
changingCmdIndex,
|
changingCmdId,
|
||||||
cmdAdded: addCommand,
|
cmdAdded: addCommand,
|
||||||
cmdChanging: showCommandProps,
|
cmdChanging: showCommandProps,
|
||||||
cmdChanged: changeCommand,
|
cmdChanged: changeCommand,
|
||||||
cmdUnchanged: () => {
|
cmdUnchanged: () => {
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
},
|
},
|
||||||
cmdDeleted: deleteCommand
|
cmdDeleted: deleteCommand,
|
||||||
|
editorCreated: (editor) => {
|
||||||
|
cmdEditor = editor
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if !readonly}
|
{#if !readonly}
|
||||||
<DrawingBoardToolbar
|
<DrawingBoardToolbar
|
||||||
placeInside={toolbarInside}
|
placeInside={toolbarInside}
|
||||||
|
{cmdEditor}
|
||||||
bind:toolbar
|
bind:toolbar
|
||||||
bind:tool
|
bind:tool
|
||||||
bind:penColor
|
bind:penColor
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
export let placeInside = false
|
export let placeInside = false
|
||||||
export let showPanTool = false
|
export let showPanTool = false
|
||||||
export let toolbar: HTMLDivElement | undefined
|
export let toolbar: HTMLDivElement | undefined
|
||||||
|
export let cmdEditor: HTMLDivElement | undefined
|
||||||
|
|
||||||
let colorSelector: HTMLInputElement
|
let colorSelector: HTMLInputElement
|
||||||
let penColors: string[] = defaultColors
|
let penColors: string[] = defaultColors
|
||||||
@ -91,12 +92,14 @@
|
|||||||
penColors = penColors.filter((c: string) => c !== penColor)
|
penColors = penColors.filter((c: string) => c !== penColor)
|
||||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||||
selectColor(penColors[0])
|
selectColor(penColors[0])
|
||||||
|
focusEditor()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'reset-colors': {
|
case 'reset-colors': {
|
||||||
penColors = defaultColors
|
penColors = defaultColors
|
||||||
localStorage.removeItem(storageKey.colors)
|
localStorage.removeItem(storageKey.colors)
|
||||||
selectColor(penColors[0])
|
selectColor(penColors[0])
|
||||||
|
focusEditor()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case undefined: {
|
case undefined: {
|
||||||
@ -115,6 +118,7 @@
|
|||||||
penColors = [...penColors, penColor]
|
penColors = [...penColors, penColor]
|
||||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||||
}
|
}
|
||||||
|
focusEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectColor (color: string): void {
|
function selectColor (color: string): void {
|
||||||
@ -148,6 +152,15 @@
|
|||||||
|
|
||||||
function updateFontSize (): void {
|
function updateFontSize (): void {
|
||||||
localStorage.setItem(storageKey.fontSize, fontSize.toString())
|
localStorage.setItem(storageKey.fontSize, fontSize.toString())
|
||||||
|
focusEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusEditor (): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (cmdEditor !== undefined) {
|
||||||
|
cmdEditor.focus()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -243,6 +256,7 @@
|
|||||||
tool = 'pen'
|
tool = 'pen'
|
||||||
}
|
}
|
||||||
selectColor(color)
|
selectColor(color)
|
||||||
|
focusEditor()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div slot="content" class="colorIcon" style:background={color} />
|
<div slot="content" class="colorIcon" style:background={color} />
|
||||||
|
@ -30,16 +30,18 @@ export interface DrawingProps {
|
|||||||
eraserWidth?: number
|
eraserWidth?: number
|
||||||
fontSize?: number
|
fontSize?: number
|
||||||
defaultCursor?: string
|
defaultCursor?: string
|
||||||
changingCmdIndex?: number
|
changingCmdId?: string
|
||||||
cmdAdded?: (cmd: DrawingCmd) => void
|
cmdAdded?: (cmd: DrawingCmd) => void
|
||||||
cmdChanging?: (index: number) => void
|
cmdChanging?: (id: string) => void
|
||||||
cmdUnchanged?: (index: number) => void
|
cmdUnchanged?: (id: string) => void
|
||||||
cmdChanged?: (index: number, cmd: DrawingCmd) => void
|
cmdChanged?: (cmd: DrawingCmd) => void
|
||||||
cmdDeleted?: (index: number) => void
|
cmdDeleted?: (id: string) => void
|
||||||
|
editorCreated?: (editor: HTMLDivElement) => void
|
||||||
panned?: (offset: Point) => void
|
panned?: (offset: Point) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DrawingCmd {
|
export interface DrawingCmd {
|
||||||
|
id: string
|
||||||
type: 'line' | 'text'
|
type: 'line' | 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +73,10 @@ function avgPoint (p1: Point, p2: Point): Point {
|
|||||||
|
|
||||||
const maxTextLength = 500
|
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">
|
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"/>
|
<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>`
|
</svg>`
|
||||||
@ -294,13 +300,15 @@ export function drawing (
|
|||||||
draw.eraserWidth = props.eraserWidth ?? draw.eraserWidth
|
draw.eraserWidth = props.eraserWidth ?? draw.eraserWidth
|
||||||
draw.fontSize = props.fontSize ?? draw.fontSize
|
draw.fontSize = props.fontSize ?? draw.fontSize
|
||||||
draw.offset = props.offset ?? draw.offset
|
draw.offset = props.offset ?? draw.offset
|
||||||
|
|
||||||
updateCanvasCursor()
|
updateCanvasCursor()
|
||||||
|
updateCanvasTouchAction()
|
||||||
|
|
||||||
interface LiveTextBox {
|
interface LiveTextBox {
|
||||||
pos: Point
|
pos: Point
|
||||||
box: HTMLDivElement
|
box: HTMLDivElement
|
||||||
editor: HTMLDivElement
|
editor: HTMLDivElement
|
||||||
cmdIndex: number
|
cmdId: string
|
||||||
}
|
}
|
||||||
let liveTextBox: LiveTextBox | undefined
|
let liveTextBox: LiveTextBox | undefined
|
||||||
|
|
||||||
@ -328,6 +336,61 @@ export function drawing (
|
|||||||
})
|
})
|
||||||
resizeObserver.observe(canvas)
|
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) => {
|
canvas.onpointerdown = (e) => {
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
return
|
return
|
||||||
@ -337,16 +400,7 @@ export function drawing (
|
|||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
canvas.setPointerCapture(e.pointerId)
|
canvas.setPointerCapture(e.pointerId)
|
||||||
|
drawStart(pointerToNodePoint(e))
|
||||||
const x = e.offsetX
|
|
||||||
const y = e.offsetY
|
|
||||||
|
|
||||||
draw.on = true
|
|
||||||
draw.points = []
|
|
||||||
prevPos = { x, y }
|
|
||||||
if (draw.isDrawingTool()) {
|
|
||||||
draw.addPoint(x, y)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.onpointermove = (e) => {
|
canvas.onpointermove = (e) => {
|
||||||
@ -354,35 +408,7 @@ export function drawing (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
drawContinue(pointerToNodePoint(e))
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.onpointerup = (e) => {
|
canvas.onpointerup = (e) => {
|
||||||
@ -391,24 +417,11 @@ export function drawing (
|
|||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
canvas.releasePointerCapture(e.pointerId)
|
canvas.releasePointerCapture(e.pointerId)
|
||||||
if (draw.on) {
|
drawEnd(pointerToNodePoint(e))
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas.onpointercancel = canvas.onpointerup
|
||||||
|
|
||||||
canvas.onpointerenter = () => {
|
canvas.onpointerenter = () => {
|
||||||
if (!readonly && draw.isDrawingTool()) {
|
if (!readonly && draw.isDrawingTool()) {
|
||||||
canvasCursor.style.visibility = 'visible'
|
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)
|
const pos = draw.mouseToCanvasPoint(mousePos)
|
||||||
for (let i = commands.length - 1; i >= 0; i--) {
|
for (let i = commands.length - 1; i >= 0; i--) {
|
||||||
const anyCmd = commands[i]
|
const anyCmd = commands[i]
|
||||||
if (anyCmd.type === 'text') {
|
if (anyCmd.type === 'text') {
|
||||||
const cmd = anyCmd as DrawTextCmd
|
const cmd = anyCmd as DrawTextCmd
|
||||||
if (draw.isPointInText(pos, cmd)) {
|
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 pos = prevPos
|
||||||
let existingCmd: DrawTextCmd | undefined
|
let existingCmd: DrawTextCmd | undefined
|
||||||
if (cmdIndex >= 0 && commands[cmdIndex]?.type === 'text') {
|
for (const cmd of commands) {
|
||||||
existingCmd = commands[cmdIndex] as DrawTextCmd
|
if (cmd.id === cmdId && cmd.type === 'text') {
|
||||||
pos = draw.canvasToMousePoint(existingCmd.pos)
|
existingCmd = cmd as DrawTextCmd
|
||||||
|
pos = draw.canvasToMousePoint(existingCmd.pos)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const padding = 6
|
const padding = 6
|
||||||
@ -455,6 +528,7 @@ export function drawing (
|
|||||||
box.style.borderRadius = 'var(--small-BorderRadius)'
|
box.style.borderRadius = 'var(--small-BorderRadius)'
|
||||||
box.style.padding = `${padding}px`
|
box.style.padding = `${padding}px`
|
||||||
box.style.background = 'var(--theme-popup-header)'
|
box.style.background = 'var(--theme-popup-header)'
|
||||||
|
box.style.touchAction = 'none'
|
||||||
box.addEventListener('mousedown', (e) => {
|
box.addEventListener('mousedown', (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
})
|
})
|
||||||
@ -513,19 +587,18 @@ export function drawing (
|
|||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (liveTextBox !== undefined) {
|
if (liveTextBox !== undefined) {
|
||||||
const cmdIndex = liveTextBox.cmdIndex
|
const cmdId = liveTextBox.cmdId
|
||||||
if (cmdIndex >= 0) {
|
// reset changingCmdId in clients
|
||||||
// reset changingCmdIndex in clients
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
props.cmdUnchanged?.(cmdId)
|
||||||
props.cmdUnchanged?.(cmdIndex)
|
}, 0)
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
closeLiveTextBox()
|
closeLiveTextBox()
|
||||||
replayCommands()
|
replayCommands()
|
||||||
} else if (e.key === 'Enter' && e.ctrlKey) {
|
} else if (e.key === 'Enter' && e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
storeTextCommand()
|
storeTextCommand()
|
||||||
|
closeLiveTextBox()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
box.appendChild(editor)
|
box.appendChild(editor)
|
||||||
@ -562,44 +635,43 @@ export function drawing (
|
|||||||
return handle
|
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()
|
const dragHandle = makeHandle()
|
||||||
dragHandle.style.left = `-${handleSize / 2}px`
|
dragHandle.style.left = `-${handleSize / 2}px`
|
||||||
dragHandle.style.cursor = 'grab'
|
dragHandle.style.cursor = 'grab'
|
||||||
|
dragHandle.style.touchAction = 'none'
|
||||||
dragHandle.addEventListener('pointerdown', (e) => {
|
dragHandle.addEventListener('pointerdown', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
dragHandle.style.cursor = 'grabbing'
|
dragHandle.style.cursor = 'grabbing'
|
||||||
dragHandle.setPointerCapture(e.pointerId)
|
dragHandle.setPointerCapture(e.pointerId)
|
||||||
const x = e.clientX
|
let prevPos = { x: e.clientX, y: e.clientY }
|
||||||
const y = e.clientY
|
|
||||||
const dragStart = { x, y }
|
|
||||||
const pointerMove = (e: PointerEvent): void => {
|
const pointerMove = (e: PointerEvent): void => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const x = e.clientX
|
const p = { x: e.clientX, y: e.clientY }
|
||||||
const y = e.clientY
|
moveTextBox(p.x - prevPos.x, p.y - prevPos.y)
|
||||||
const dx = x - dragStart.x
|
prevPos = p
|
||||||
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 pointerUp = (e: PointerEvent): void => {
|
const pointerUp = (e: PointerEvent): void => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -610,9 +682,37 @@ export function drawing (
|
|||||||
dragHandle.releasePointerCapture(e.pointerId)
|
dragHandle.releasePointerCapture(e.pointerId)
|
||||||
dragHandle.removeEventListener('pointermove', pointerMove)
|
dragHandle.removeEventListener('pointermove', pointerMove)
|
||||||
dragHandle.removeEventListener('pointerup', pointerUp)
|
dragHandle.removeEventListener('pointerup', pointerUp)
|
||||||
|
dragHandle.removeEventListener('pointercancel', pointerUp)
|
||||||
}
|
}
|
||||||
dragHandle.addEventListener('pointermove', pointerMove)
|
dragHandle.addEventListener('pointermove', pointerMove)
|
||||||
dragHandle.addEventListener('pointerup', pointerUp)
|
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)
|
box.appendChild(dragHandle)
|
||||||
|
|
||||||
@ -622,20 +722,21 @@ export function drawing (
|
|||||||
deleteButton.innerHTML = crossSvg
|
deleteButton.innerHTML = crossSvg
|
||||||
deleteButton.addEventListener('click', () => {
|
deleteButton.addEventListener('click', () => {
|
||||||
node.removeChild(box)
|
node.removeChild(box)
|
||||||
if (liveTextBox?.cmdIndex !== undefined) {
|
if (liveTextBox?.cmdId !== undefined) {
|
||||||
props.cmdDeleted?.(liveTextBox.cmdIndex)
|
props.cmdDeleted?.(liveTextBox.cmdId)
|
||||||
}
|
}
|
||||||
liveTextBox = undefined
|
liveTextBox = undefined
|
||||||
})
|
})
|
||||||
box.appendChild(deleteButton)
|
box.appendChild(deleteButton)
|
||||||
|
|
||||||
node.appendChild(box)
|
node.appendChild(box)
|
||||||
liveTextBox = { box, editor, pos, cmdIndex }
|
liveTextBox = { box, editor, pos, cmdId }
|
||||||
updateLiveTextBox()
|
updateLiveTextBox()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editor.focus()
|
editor.focus()
|
||||||
}, 100)
|
}, 100)
|
||||||
selectAll()
|
selectAll()
|
||||||
|
props.editorCreated?.(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLiveTextBox (): void {
|
function updateLiveTextBox (): void {
|
||||||
@ -658,7 +759,9 @@ export function drawing (
|
|||||||
if (liveTextBox !== undefined) {
|
if (liveTextBox !== undefined) {
|
||||||
const text = (liveTextBox.editor.innerText ?? '').trim()
|
const text = (liveTextBox.editor.innerText ?? '').trim()
|
||||||
if (text !== '') {
|
if (text !== '') {
|
||||||
|
const cmdId = liveTextBox.cmdId
|
||||||
const cmd: DrawTextCmd = {
|
const cmd: DrawTextCmd = {
|
||||||
|
id: cmdId === '' ? makeCommandId() : cmdId,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text,
|
text,
|
||||||
pos: draw.mouseToCanvasPoint(liveTextBox.pos),
|
pos: draw.mouseToCanvasPoint(liveTextBox.pos),
|
||||||
@ -666,10 +769,9 @@ export function drawing (
|
|||||||
fontFace: draw.fontFace,
|
fontFace: draw.fontFace,
|
||||||
color: draw.penColor
|
color: draw.penColor
|
||||||
}
|
}
|
||||||
const cmdIndex = liveTextBox.cmdIndex
|
|
||||||
const notify = (): void => {
|
const notify = (): void => {
|
||||||
if (cmdIndex >= 0) {
|
if (cmdId !== '') {
|
||||||
props.cmdChanged?.(cmdIndex, cmd)
|
props.cmdChanged?.(cmd)
|
||||||
} else {
|
} else {
|
||||||
props.cmdAdded?.(cmd)
|
props.cmdAdded?.(cmd)
|
||||||
}
|
}
|
||||||
@ -680,7 +782,7 @@ export function drawing (
|
|||||||
notify()
|
notify()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
props.cmdUnchanged?.(liveTextBox.cmdIndex)
|
props.cmdUnchanged?.(liveTextBox.cmdId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -689,6 +791,7 @@ export function drawing (
|
|||||||
if (draw.points.length > 0) {
|
if (draw.points.length > 0) {
|
||||||
const erasing = draw.tool === 'erase'
|
const erasing = draw.tool === 'erase'
|
||||||
const cmd: DrawLineCmd = {
|
const cmd: DrawLineCmd = {
|
||||||
|
id: makeCommandId(),
|
||||||
type: 'line',
|
type: 'line',
|
||||||
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
|
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
|
||||||
erasing,
|
erasing,
|
||||||
@ -726,13 +829,17 @@ export function drawing (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCanvasTouchAction (): void {
|
||||||
|
canvas.style.touchAction = readonly ? 'unset' : 'none'
|
||||||
|
}
|
||||||
|
|
||||||
function replayCommands (): void {
|
function replayCommands (): void {
|
||||||
draw.ctx.reset()
|
draw.ctx.reset()
|
||||||
for (let i = 0; i < commands.length; i++) {
|
for (const cmd of commands) {
|
||||||
if (liveTextBox?.cmdIndex === i) {
|
if (cmd.id !== undefined && liveTextBox?.cmdId === cmd.id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
draw.drawCommand(commands[i])
|
draw.drawCommand(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,9 +881,10 @@ export function drawing (
|
|||||||
}
|
}
|
||||||
if (props.readonly !== readonly) {
|
if (props.readonly !== readonly) {
|
||||||
readonly = props.readonly ?? false
|
readonly = props.readonly ?? false
|
||||||
|
updateCanvasTouchAction()
|
||||||
updateCursor = true
|
updateCursor = true
|
||||||
}
|
}
|
||||||
if (props.changingCmdIndex === undefined) {
|
if (props.changingCmdId === undefined) {
|
||||||
if (liveTextBox !== undefined) {
|
if (liveTextBox !== undefined) {
|
||||||
storeTextCommand(true)
|
storeTextCommand(true)
|
||||||
closeLiveTextBox()
|
closeLiveTextBox()
|
||||||
@ -784,9 +892,9 @@ export function drawing (
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (liveTextBox === undefined) {
|
if (liveTextBox === undefined) {
|
||||||
makeLiveTextBox(props.changingCmdIndex)
|
makeLiveTextBox(props.changingCmdId)
|
||||||
replay = true
|
replay = true
|
||||||
} else if (liveTextBox.cmdIndex !== props.changingCmdIndex) {
|
} else if (liveTextBox.cmdId !== props.changingCmdId) {
|
||||||
storeTextCommand(true)
|
storeTextCommand(true)
|
||||||
closeLiveTextBox()
|
closeLiveTextBox()
|
||||||
replay = true
|
replay = true
|
||||||
|
@ -13,7 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 { Loading } from '@hcengineering/ui'
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { Array as YArray, Map as YMap } from 'yjs'
|
import { Array as YArray, Map as YMap } from 'yjs'
|
||||||
@ -34,11 +41,14 @@
|
|||||||
let fontSize: number
|
let fontSize: number
|
||||||
let commands: DrawingCmd[] = []
|
let commands: DrawingCmd[] = []
|
||||||
let offset: { x: number, y: number } = { x: 0, y: 0 }
|
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 toolbar: HTMLDivElement
|
||||||
let oldSelected = false
|
let oldSelected = false
|
||||||
|
let oldReadonly = false
|
||||||
|
|
||||||
$: onSelectedChanged(selected)
|
$: onSelectedChanged(selected)
|
||||||
|
$: onReadonlyChanged(readonly)
|
||||||
|
|
||||||
function listenSavedCommands (): void {
|
function listenSavedCommands (): void {
|
||||||
commands = savedCmds.toArray()
|
commands = savedCmds.toArray()
|
||||||
@ -50,25 +60,79 @@
|
|||||||
// offset = savedProps.get('offset')
|
// offset = savedProps.get('offset')
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCommandProps (index: number): void {
|
function showCommandProps (id: string): void {
|
||||||
changingCmdIndex = index
|
changingCmdId = id
|
||||||
const anyCmd = commands[index]
|
for (const cmd of commands) {
|
||||||
if (anyCmd?.type === 'text') {
|
if (cmd.id === id) {
|
||||||
const cmd = anyCmd as DrawTextCmd
|
if (cmd.type === 'text') {
|
||||||
penColor = cmd.color
|
const textCmd = cmd as DrawTextCmd
|
||||||
fontSize = cmd.fontSize
|
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 {
|
function onSelectedChanged (selected: boolean): void {
|
||||||
if (oldSelected !== selected) {
|
if (oldSelected !== selected) {
|
||||||
if (oldSelected && !selected && changingCmdIndex !== undefined) {
|
if (oldSelected && !selected && changingCmdId !== undefined) {
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
|
cmdEditor = undefined
|
||||||
}
|
}
|
||||||
oldSelected = selected
|
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(() => {
|
onMount(() => {
|
||||||
commands = savedCmds.toArray()
|
commands = savedCmds.toArray()
|
||||||
// offset = savedProps.get('offset')
|
// offset = savedProps.get('offset')
|
||||||
@ -109,27 +173,23 @@
|
|||||||
penWidth,
|
penWidth,
|
||||||
eraserWidth,
|
eraserWidth,
|
||||||
fontSize,
|
fontSize,
|
||||||
changingCmdIndex,
|
changingCmdId,
|
||||||
cmdAdded: (cmd) => {
|
cmdAdded: (cmd) => {
|
||||||
savedCmds.push([cmd])
|
savedCmds.push([cmd])
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
},
|
},
|
||||||
cmdChanging: showCommandProps,
|
cmdChanging: showCommandProps,
|
||||||
cmdChanged: (index, cmd) => {
|
cmdChanged: changeCommand,
|
||||||
savedCmds.delete(index)
|
|
||||||
savedCmds.insert(index, [cmd])
|
|
||||||
changingCmdIndex = undefined
|
|
||||||
},
|
|
||||||
cmdUnchanged: () => {
|
cmdUnchanged: () => {
|
||||||
changingCmdIndex = undefined
|
changingCmdId = undefined
|
||||||
},
|
|
||||||
cmdDeleted: (index) => {
|
|
||||||
savedCmds.delete(index)
|
|
||||||
changingCmdIndex = undefined
|
|
||||||
},
|
},
|
||||||
|
cmdDeleted: deleteCommand,
|
||||||
panned: (newOffset) => {
|
panned: (newOffset) => {
|
||||||
offset = newOffset
|
offset = newOffset
|
||||||
// savedProps.set('offset', offset)
|
// savedProps.set('offset', offset)
|
||||||
|
},
|
||||||
|
editorCreated: (editor) => {
|
||||||
|
cmdEditor = editor
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -142,6 +202,7 @@
|
|||||||
<DrawingBoardToolbar
|
<DrawingBoardToolbar
|
||||||
placeInside={true}
|
placeInside={true}
|
||||||
showPanTool={true}
|
showPanTool={true}
|
||||||
|
{cmdEditor}
|
||||||
bind:toolbar
|
bind:toolbar
|
||||||
bind:tool
|
bind:tool
|
||||||
bind:penColor
|
bind:penColor
|
||||||
|
@ -35,23 +35,43 @@
|
|||||||
let resizer: HTMLElement
|
let resizer: HTMLElement
|
||||||
let startY: number
|
let startY: number
|
||||||
let resizedHeight: number | undefined
|
let resizedHeight: number | undefined
|
||||||
|
let resizerTouchId: number | undefined
|
||||||
let loading = true
|
let loading = true
|
||||||
let loadingTimer: any
|
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 {
|
function onResizerPointerDown (e: PointerEvent): void {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const height = node.attrs.height ?? defaultHeight
|
|
||||||
startY = e.clientY - height
|
|
||||||
resizedHeight = height
|
|
||||||
resizer.setPointerCapture(e.pointerId)
|
resizer.setPointerCapture(e.pointerId)
|
||||||
resizer.addEventListener('pointermove', onResizerPointerMove)
|
resizer.addEventListener('pointermove', onResizerPointerMove)
|
||||||
resizer.addEventListener('pointerup', onResizerPointerUp)
|
resizer.addEventListener('pointerup', onResizerPointerUp)
|
||||||
|
resizer.addEventListener('pointercancel', onResizerPointerUp)
|
||||||
|
resizeStart(e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResizerPointerMove (e: PointerEvent): void {
|
function onResizerPointerMove (e: PointerEvent): void {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
resizedHeight = Math.max(minHeight, e.clientY - startY)
|
resizeContinue(e.clientY)
|
||||||
resizedHeight = Math.min(maxHeight, resizedHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResizerPointerUp (e: PointerEvent): void {
|
function onResizerPointerUp (e: PointerEvent): void {
|
||||||
@ -59,11 +79,35 @@
|
|||||||
resizer.releasePointerCapture(e.pointerId)
|
resizer.releasePointerCapture(e.pointerId)
|
||||||
resizer.removeEventListener('pointermove', onResizerPointerMove)
|
resizer.removeEventListener('pointermove', onResizerPointerMove)
|
||||||
resizer.removeEventListener('pointerup', onResizerPointerUp)
|
resizer.removeEventListener('pointerup', onResizerPointerUp)
|
||||||
if (typeof getPos === 'function') {
|
resizer.removeEventListener('pointercancel', onResizerPointerUp)
|
||||||
const tr = editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height: resizedHeight })
|
resizeFinish()
|
||||||
editor.view.dispatch(tr)
|
}
|
||||||
|
|
||||||
|
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(() => {
|
onMount(() => {
|
||||||
@ -110,7 +154,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if selected}
|
{#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">
|
<svg viewBox="0 0 60 4" height="4" width="60" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<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"
|
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;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
Loading…
Reference in New Issue
Block a user