@lazlon-platform/html-editor 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/hooks/actions.ts +4 -3
- package/lib/hooks/batch.ts +3 -2
- package/lib/hooks/node.ts +14 -6
- package/lib/hooks/pointer/moveable.ts +75 -54
- package/lib/hooks/pointer/pointer.ts +22 -11
- package/lib/hooks/pointer/resize.ts +176 -210
- package/lib/hooks/pointer/rotation.ts +41 -55
- package/lib/hooks/pointer/selectionFrame.ts +8 -11
- package/lib/hooks/pointer/selector.ts +48 -40
- package/lib/hooks/pointer/snap.ts +20 -20
- package/lib/hooks/textMarks.ts +67 -50
- package/lib/model/geometry/math.ts +484 -0
- package/lib/model/geometry/svg.ts +55 -0
- package/lib/model/node/shape/arrow.ts +1 -1
- package/lib/model/node/shape/polygon.ts +23 -1
- package/lib/model/node/shape/star.ts +29 -1
- package/lib/model/node/text.ts +21 -12
- package/lib/model/node.ts +3 -7
- package/lib/model/page.ts +3 -1
- package/lib/ui/node/NodeView.tsx +1 -13
- package/lib/ui/node/TextContent.tsx +13 -16
- package/lib/ui/selection.ts +9 -26
- package/package.json +34 -34
- package/lib/model/geometry.ts +0 -247
package/lib/hooks/actions.ts
CHANGED
|
@@ -5,9 +5,10 @@ import type { Node, NodeProps } from "../model/node"
|
|
|
5
5
|
import { GroupNode } from "../model/node/group"
|
|
6
6
|
import type { Page } from "../model/page"
|
|
7
7
|
import { flattenNodes } from "../model/traversal"
|
|
8
|
-
import {
|
|
8
|
+
import { selectionBox } from "../ui/selection"
|
|
9
9
|
import { useBatchSet } from "./batch"
|
|
10
10
|
import { useEditor } from "./editor"
|
|
11
|
+
import { boxBounds } from "../model/geometry/math"
|
|
11
12
|
|
|
12
13
|
export function clone(
|
|
13
14
|
editor: Editor,
|
|
@@ -95,7 +96,7 @@ export function useGroupAction() {
|
|
|
95
96
|
|
|
96
97
|
function group() {
|
|
97
98
|
const { selection, selectionPage: page } = editor
|
|
98
|
-
const { width, height, x, y } =
|
|
99
|
+
const { width, height, x, y } = boxBounds(selectionBox(selection))
|
|
99
100
|
if (selection.size === 0 || !page) return
|
|
100
101
|
|
|
101
102
|
const group = new GroupNode(editor, page, {
|
|
@@ -296,7 +297,7 @@ export function useDistributeAction() {
|
|
|
296
297
|
const editor = useEditor()
|
|
297
298
|
|
|
298
299
|
function distribute(pos: "x" | "y", size: "width" | "height") {
|
|
299
|
-
const rect =
|
|
300
|
+
const rect = boxBounds(selectionBox(editor.selection))
|
|
300
301
|
const array = editor.selection.values().toArray()
|
|
301
302
|
|
|
302
303
|
const undo: HistoryAction[] = array.map((node) => [
|
package/lib/hooks/batch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEqual
|
|
1
|
+
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useRef } from "react"
|
|
3
3
|
import { useComputed } from "react-bolt"
|
|
4
4
|
import { type HistoryAction } from "../model/history"
|
|
@@ -90,7 +90,8 @@ export function useBatchSet() {
|
|
|
90
90
|
push = true
|
|
91
91
|
const nextValues = typeof set === "function" ? set(node) : set
|
|
92
92
|
const keys = Object.keys(nextValues) as WritableKeys<N>[]
|
|
93
|
-
|
|
93
|
+
const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
|
|
94
|
+
prev.push(["set-node-props", [node.id, prevValues]])
|
|
94
95
|
next.push(["set-node-props", [node.id, nextValues]])
|
|
95
96
|
Object.assign(node, nextValues)
|
|
96
97
|
}
|
package/lib/hooks/node.ts
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
import type { Node } from "../model"
|
|
2
|
-
import {
|
|
2
|
+
import { box, boxBounds, deg } from "../model/geometry/math"
|
|
3
3
|
import { useNodeField, useNodeFieldBatch } from "./batch"
|
|
4
4
|
|
|
5
5
|
export function useVisualPositionBatch(nodes: Node[]) {
|
|
6
|
-
const rotation = useNodeField(nodes, "rotation", 0)
|
|
6
|
+
const rotation = useNodeField(nodes, "rotation", deg(0))
|
|
7
7
|
const width = useNodeField(nodes, "width", NaN)
|
|
8
8
|
const height = useNodeField(nodes, "height", NaN)
|
|
9
9
|
const x = useNodeFieldBatch(nodes, "x", NaN)
|
|
10
10
|
const y = useNodeFieldBatch(nodes, "y", NaN)
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const bounds = boxBounds(
|
|
13
|
+
box({
|
|
14
|
+
x: x.value,
|
|
15
|
+
y: y.value,
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
rotation,
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
13
21
|
|
|
14
22
|
function withBoundingBox(axis: "x" | "y", fn: (value: number) => void) {
|
|
15
23
|
return function (value: number): void {
|
|
16
|
-
const delta = value -
|
|
24
|
+
const delta = value - bounds[axis]
|
|
17
25
|
return fn(axis === "x" ? x.value + delta : y.value + delta)
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
return {
|
|
22
30
|
x: {
|
|
23
|
-
value:
|
|
31
|
+
value: bounds.x,
|
|
24
32
|
onChange: withBoundingBox("x", x.onChange),
|
|
25
33
|
onChangeEnd: withBoundingBox("x", x.onChangeEnd),
|
|
26
34
|
},
|
|
27
35
|
y: {
|
|
28
|
-
value:
|
|
36
|
+
value: bounds.y,
|
|
29
37
|
onChange: withBoundingBox("y", y.onChange),
|
|
30
38
|
onChangeEnd: withBoundingBox("y", y.onChangeEnd),
|
|
31
39
|
},
|
|
@@ -1,73 +1,94 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
box,
|
|
4
|
+
boxBounds,
|
|
5
|
+
boxContainsPoint,
|
|
6
|
+
type Point,
|
|
7
|
+
point,
|
|
8
|
+
pointSubtract,
|
|
9
|
+
rect,
|
|
10
|
+
} from "../../model/geometry/math"
|
|
3
11
|
import type { HistoryEntry } from "../../model/history"
|
|
4
12
|
import type { Node } from "../../model/node"
|
|
5
|
-
import {
|
|
13
|
+
import { selectionBox } from "../../ui/selection"
|
|
6
14
|
import { useEditor, usePage } from "../editor"
|
|
7
|
-
import { usePointer } from "./pointer"
|
|
15
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
8
16
|
import { useSnap } from "./snap"
|
|
9
17
|
|
|
10
|
-
type StartPoints = {
|
|
11
|
-
node: Node
|
|
12
|
-
start: Point
|
|
13
|
-
offset: Point
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const moveInit = {
|
|
17
|
-
start: { x: 0, y: 0, width: 0, height: 0 },
|
|
18
|
-
grab: { x: 0, y: 0 },
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
export function useMoveable() {
|
|
22
19
|
const editor = useEditor()
|
|
23
20
|
const page = usePage()
|
|
24
|
-
const startpoints = useRef(Array<StartPoints>())
|
|
25
|
-
const move = useRef(moveInit)
|
|
26
21
|
const snap = useSnap()
|
|
27
22
|
|
|
23
|
+
const state = useRef({
|
|
24
|
+
initialSelectionRect: rect(),
|
|
25
|
+
cursorOffset: point(),
|
|
26
|
+
nodes: Array<{
|
|
27
|
+
node: Node
|
|
28
|
+
startingPoint: Point
|
|
29
|
+
selectionOffset: Point
|
|
30
|
+
}>(),
|
|
31
|
+
})
|
|
32
|
+
|
|
28
33
|
return usePointer({
|
|
29
34
|
onDown(event) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.values()
|
|
35
|
-
|
|
36
|
-
node
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
move.current = {
|
|
43
|
-
start: rect,
|
|
44
|
-
grab: {
|
|
45
|
-
x: Math.round(event.clientX / editor.zoom - rect.x),
|
|
46
|
-
y: Math.round(event.clientY / editor.zoom - rect.y),
|
|
47
|
-
},
|
|
35
|
+
const cursor = cursorPosition(event, page)
|
|
36
|
+
|
|
37
|
+
// clicked outside of selection, try grabbing nodes under the cursor
|
|
38
|
+
if (!boxContainsPoint(selectionBox(editor.selection), cursor)) {
|
|
39
|
+
const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
|
|
40
|
+
for (const node of stackOrderedNodes) {
|
|
41
|
+
if (boxContainsPoint(box(node), cursor)) {
|
|
42
|
+
editor.selection = new Set([node])
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
if (editor.selection.size === 0) return false
|
|
49
|
+
|
|
50
|
+
const initialSelectionBox = selectionBox(editor.selection)
|
|
51
|
+
const initialSelectionRect = boxBounds(initialSelectionBox)
|
|
52
|
+
|
|
53
|
+
for (const node of editor.selection) {
|
|
51
54
|
if (node.blockMove(event)) return false
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
if (boxContainsPoint(initialSelectionBox, cursor)) {
|
|
58
|
+
state.current = {
|
|
59
|
+
initialSelectionRect,
|
|
60
|
+
cursorOffset: pointSubtract(cursor, initialSelectionRect),
|
|
61
|
+
nodes: editor.selection
|
|
62
|
+
.values()
|
|
63
|
+
.map((node) => ({
|
|
64
|
+
node,
|
|
65
|
+
startingPoint: point(node),
|
|
66
|
+
selectionOffset: pointSubtract(point(node), initialSelectionRect),
|
|
67
|
+
}))
|
|
68
|
+
.toArray(),
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
55
73
|
},
|
|
56
74
|
|
|
57
|
-
onMove(
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const { x, y } = snap(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
onMove(event) {
|
|
76
|
+
const cursor = cursorPosition(event, page)
|
|
77
|
+
const target = pointSubtract(cursor, state.current.cursorOffset)
|
|
78
|
+
|
|
79
|
+
const { x, y } = snap(
|
|
80
|
+
!event.shiftKey,
|
|
81
|
+
rect({
|
|
82
|
+
x: target.x,
|
|
83
|
+
y: target.y,
|
|
84
|
+
width: state.current.initialSelectionRect.width,
|
|
85
|
+
height: state.current.initialSelectionRect.height,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for (const { node, selectionOffset } of state.current.nodes) {
|
|
90
|
+
node.x = x + selectionOffset.x
|
|
91
|
+
node.y = y + selectionOffset.y
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
editor.action = { action: "move", payload: { x, y } }
|
|
@@ -82,12 +103,12 @@ export function useMoveable() {
|
|
|
82
103
|
editor.action = {}
|
|
83
104
|
page.snapLines = []
|
|
84
105
|
|
|
85
|
-
const entries =
|
|
86
|
-
|
|
106
|
+
const entries: HistoryEntry[] = state.current.nodes.map(
|
|
107
|
+
({ node, startingPoint: start }) => ({
|
|
87
108
|
redo: ["set-node-props", [node.id, { x: node.x, y: node.y }]],
|
|
88
109
|
undo: ["set-node-props", [node.id, { x: start.x, y: start.y }]],
|
|
89
|
-
}
|
|
90
|
-
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
91
112
|
|
|
92
113
|
editor.history.push({
|
|
93
114
|
redo: ["batch", entries.map((e) => e.redo)],
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import { Page, type Node } from "@lazlon/html-editor/model"
|
|
1
2
|
import { useCallback, useRef } from "react"
|
|
2
|
-
|
|
3
|
-
type MoveEvent = {
|
|
4
|
-
start: React.PointerEvent
|
|
5
|
-
event: globalThis.PointerEvent
|
|
6
|
-
}
|
|
3
|
+
import { floatNorm, type Point } from "../../model/geometry/math"
|
|
7
4
|
|
|
8
5
|
export type UsePointerProps = {
|
|
9
6
|
onDown?(event: React.PointerEvent): boolean | void
|
|
10
|
-
onMove?(event:
|
|
11
|
-
onEnd?(event:
|
|
12
|
-
onCancel?(event:
|
|
7
|
+
onMove?(event: globalThis.PointerEvent): void
|
|
8
|
+
onEnd?(event: globalThis.PointerEvent): void
|
|
9
|
+
onCancel?(event: globalThis.PointerEvent): void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function cursorPosition(
|
|
13
|
+
event: { clientX: number; clientY: number },
|
|
14
|
+
relativeTo: Page | Node,
|
|
15
|
+
): Point {
|
|
16
|
+
const page = relativeTo instanceof Page ? relativeTo : relativeTo.page
|
|
17
|
+
const { zoom } = page.editor
|
|
18
|
+
const { x, y } = page.ref!.getBoundingClientRect()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
x: floatNorm(event.clientX / zoom - x),
|
|
22
|
+
y: floatNorm(event.clientY / zoom - y),
|
|
23
|
+
}
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export function usePointer(props: UsePointerProps) {
|
|
@@ -23,7 +34,7 @@ export function usePointer(props: UsePointerProps) {
|
|
|
23
34
|
|
|
24
35
|
function onPointerMove(event: globalThis.PointerEvent) {
|
|
25
36
|
isMovingRef.current = true
|
|
26
|
-
onMove?.(
|
|
37
|
+
onMove?.(event)
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
function onPointerUp(event: globalThis.PointerEvent) {
|
|
@@ -32,9 +43,9 @@ export function usePointer(props: UsePointerProps) {
|
|
|
32
43
|
removeEventListener("pointercancel", onPointerUp)
|
|
33
44
|
|
|
34
45
|
if (isMovingRef.current) {
|
|
35
|
-
onEnd?.(
|
|
46
|
+
onEnd?.(event)
|
|
36
47
|
} else {
|
|
37
|
-
onCancel?.(
|
|
48
|
+
onCancel?.(event)
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
isMovingRef.current = false
|