@lazlon-platform/html-editor 0.4.0 → 0.6.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 +54 -26
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/node.ts +14 -6
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +92 -57
- package/lib/hooks/pointer/pointer.ts +21 -11
- package/lib/hooks/pointer/resize.ts +176 -210
- package/lib/hooks/pointer/rotation.ts +89 -68
- package/lib/hooks/pointer/selectionFrame.ts +8 -11
- package/lib/hooks/pointer/selector.ts +62 -40
- package/lib/hooks/pointer/snap.ts +23 -23
- package/lib/hooks/textMarks.ts +1 -3
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +13 -9
- package/lib/model/geometry/math.ts +623 -0
- package/lib/model/geometry/svg.ts +55 -0
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +7 -10
- package/lib/model/node/{editable → editableNode}/index.ts +13 -29
- package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
- package/lib/model/node/{group.ts → groupNode.ts} +9 -13
- package/lib/model/node/{image.ts → imageNode.ts} +5 -11
- package/lib/model/node/lineNode.ts +59 -0
- package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
- package/lib/model/node/shapeNode/shape.ts +96 -0
- package/lib/model/node/{text.ts → textNode.ts} +19 -21
- package/lib/model/node.ts +11 -29
- package/lib/model/page.ts +4 -3
- package/lib/model/traversal.ts +1 -1
- package/lib/ui/extractor.ts +3 -3
- package/lib/ui/index.ts +2 -4
- package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +32 -0
- package/lib/ui/node/NodeView.tsx +1 -13
- package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
- package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
- package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
- package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
- package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
- package/lib/ui/node/ShapeContent/index.tsx +43 -0
- package/lib/ui/node/TextContent.tsx +1 -1
- package/lib/ui/selection.ts +9 -26
- package/package.json +34 -34
- package/lib/model/geometry.ts +0 -247
- package/lib/model/node/shape/arrow.ts +0 -50
- package/lib/model/node/shape/ellipse.ts +0 -26
- package/lib/model/node/shape/polygon.ts +0 -108
- package/lib/model/node/shape/star.ts +0 -63
- package/lib/ui/node/ArrowContent.tsx +0 -60
- package/lib/ui/node/EllipseContent.tsx +0 -49
- package/lib/ui/node/PolygonContent.tsx +0 -81
- package/lib/ui/node/StarContent.tsx +0 -60
- /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
- /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
package/lib/hooks/actions.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { useComputed } from "react-bolt"
|
|
2
|
+
import {
|
|
3
|
+
flattenNodes,
|
|
4
|
+
GroupNode,
|
|
5
|
+
type HistoryAction,
|
|
6
|
+
type Node,
|
|
7
|
+
type NodeProps,
|
|
8
|
+
type Page,
|
|
9
|
+
} from "../model"
|
|
2
10
|
import type { Editor, NodeConstructor } from "../model/editor"
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
import {
|
|
12
|
+
boxBounds,
|
|
13
|
+
deg,
|
|
14
|
+
floatNorm,
|
|
15
|
+
rectCenter,
|
|
16
|
+
rotatePoint,
|
|
17
|
+
} from "../model/geometry/math"
|
|
18
|
+
import { selectionBox } from "../ui/selection"
|
|
9
19
|
import { useBatchSet } from "./batch"
|
|
10
20
|
import { useEditor } from "./editor"
|
|
11
21
|
|
|
@@ -14,7 +24,7 @@ export function clone(
|
|
|
14
24
|
node: Node,
|
|
15
25
|
props?: ((props: NodeProps) => Partial<NodeProps>) | Partial<NodeProps>,
|
|
16
26
|
) {
|
|
17
|
-
const copy = node.
|
|
27
|
+
const copy = node.props
|
|
18
28
|
const newProps = typeof props === "function" ? props(copy) : props
|
|
19
29
|
const newNode = editor.deserializeNode(node.page, {
|
|
20
30
|
props: { ...copy, ...newProps },
|
|
@@ -34,14 +44,14 @@ export function useAddNodeAction(page?: Page) {
|
|
|
34
44
|
|
|
35
45
|
return function addNode<N extends NodeConstructor>(
|
|
36
46
|
NodeClass: N,
|
|
37
|
-
props?: Partial<
|
|
47
|
+
props?: Partial<N["prototype"]["props"]>,
|
|
38
48
|
) {
|
|
39
49
|
const [firstPage] = editor.pages.values()
|
|
40
50
|
const targetPage = page ?? firstPage
|
|
41
51
|
if (!targetPage) return
|
|
42
52
|
const { width, height, nodes } = targetPage
|
|
43
53
|
|
|
44
|
-
const node = new NodeClass(
|
|
54
|
+
const node = new NodeClass(targetPage, {
|
|
45
55
|
id: editor.id(),
|
|
46
56
|
x: Math.round(width / 2) - 50,
|
|
47
57
|
y: Math.round(height / 2) - 50,
|
|
@@ -52,7 +62,7 @@ export function useAddNodeAction(page?: Page) {
|
|
|
52
62
|
|
|
53
63
|
targetPage.nodes = new Map([...nodes, [node.id, node]])
|
|
54
64
|
editor.history.push({
|
|
55
|
-
redo: ["add-node", [targetPage.id, [
|
|
65
|
+
redo: ["add-node", [targetPage.id, [editor.serializeNode(node)]]],
|
|
56
66
|
undo: ["delete-node", [targetPage.id, [node.id]]],
|
|
57
67
|
})
|
|
58
68
|
|
|
@@ -82,7 +92,7 @@ export function useTrashAction() {
|
|
|
82
92
|
|
|
83
93
|
editor.history.push({
|
|
84
94
|
redo: ["delete-node", [page.id, array.map((node) => node.id)]],
|
|
85
|
-
undo: ["add-node", [page.id, array.map((n) =>
|
|
95
|
+
undo: ["add-node", [page.id, array.map((n) => editor.serializeNode(n))]],
|
|
86
96
|
})
|
|
87
97
|
|
|
88
98
|
editor.selection = new Set()
|
|
@@ -95,25 +105,28 @@ export function useGroupAction() {
|
|
|
95
105
|
|
|
96
106
|
function group() {
|
|
97
107
|
const { selection, selectionPage: page } = editor
|
|
98
|
-
const { width, height, x, y } =
|
|
108
|
+
const { width, height, x, y } = boxBounds(selectionBox(selection))
|
|
99
109
|
if (selection.size === 0 || !page) return
|
|
100
110
|
|
|
101
|
-
const group = new GroupNode(
|
|
111
|
+
const group = new GroupNode(page, {
|
|
102
112
|
id: editor.id(),
|
|
103
113
|
width,
|
|
104
114
|
height,
|
|
105
115
|
x,
|
|
106
116
|
y,
|
|
107
|
-
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
group.nodes = new Set(
|
|
120
|
+
selection
|
|
108
121
|
.values()
|
|
109
122
|
.map((node) =>
|
|
110
123
|
clone(editor, node, (n) => ({
|
|
111
124
|
x: n.x - x,
|
|
112
125
|
y: n.y - y,
|
|
113
|
-
}))
|
|
126
|
+
})),
|
|
114
127
|
)
|
|
115
128
|
.toArray(),
|
|
116
|
-
|
|
129
|
+
)
|
|
117
130
|
|
|
118
131
|
editor.selection = new Set()
|
|
119
132
|
page.nodes = new Map([
|
|
@@ -138,7 +151,7 @@ export function useGroupAction() {
|
|
|
138
151
|
.toArray(),
|
|
139
152
|
],
|
|
140
153
|
],
|
|
141
|
-
["add-node", [page.id, [
|
|
154
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
142
155
|
],
|
|
143
156
|
],
|
|
144
157
|
undo: [
|
|
@@ -151,7 +164,7 @@ export function useGroupAction() {
|
|
|
151
164
|
page.id,
|
|
152
165
|
selection
|
|
153
166
|
.values()
|
|
154
|
-
.map((n) =>
|
|
167
|
+
.map((n) => editor.serializeNode(n))
|
|
155
168
|
.toArray(),
|
|
156
169
|
],
|
|
157
170
|
],
|
|
@@ -162,13 +175,28 @@ export function useGroupAction() {
|
|
|
162
175
|
|
|
163
176
|
function ungroup(group: GroupNode) {
|
|
164
177
|
const page = group.page
|
|
178
|
+
const groupCenter = rectCenter(group)
|
|
165
179
|
const nodes = group.nodes
|
|
166
180
|
.values()
|
|
167
181
|
.map((node) =>
|
|
168
|
-
clone(editor, node, (n) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
clone(editor, node, (n) => {
|
|
183
|
+
const center = rotatePoint(
|
|
184
|
+
rectCenter({
|
|
185
|
+
x: n.x + group.x,
|
|
186
|
+
y: n.y + group.y,
|
|
187
|
+
width: n.width,
|
|
188
|
+
height: n.height,
|
|
189
|
+
}),
|
|
190
|
+
groupCenter,
|
|
191
|
+
group.rotation,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
x: floatNorm(center.x - n.width / 2),
|
|
196
|
+
y: floatNorm(center.y - n.height / 2),
|
|
197
|
+
rotation: deg((n.rotation ?? 0) + group.rotation),
|
|
198
|
+
}
|
|
199
|
+
}),
|
|
172
200
|
)
|
|
173
201
|
.toArray()
|
|
174
202
|
|
|
@@ -183,14 +211,14 @@ export function useGroupAction() {
|
|
|
183
211
|
"batch",
|
|
184
212
|
[
|
|
185
213
|
["delete-node", [page.id, [group.id]]],
|
|
186
|
-
["add-node", [page.id, nodes.map((n) =>
|
|
214
|
+
["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
|
|
187
215
|
],
|
|
188
216
|
],
|
|
189
217
|
undo: [
|
|
190
218
|
"batch",
|
|
191
219
|
[
|
|
192
220
|
["delete-node", [page.id, nodes.map((node) => node.id)]],
|
|
193
|
-
["add-node", [page.id, [
|
|
221
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
194
222
|
],
|
|
195
223
|
],
|
|
196
224
|
})
|
|
@@ -225,7 +253,7 @@ export function useDuplicateAction() {
|
|
|
225
253
|
|
|
226
254
|
editor.selection = new Set(nodes)
|
|
227
255
|
editor.history.push({
|
|
228
|
-
redo: ["add-node", [page.id, nodes.map((n) =>
|
|
256
|
+
redo: ["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
|
|
229
257
|
undo: ["delete-node", [page.id, nodes.map((node) => node.id)]],
|
|
230
258
|
})
|
|
231
259
|
}
|
|
@@ -296,7 +324,7 @@ export function useDistributeAction() {
|
|
|
296
324
|
const editor = useEditor()
|
|
297
325
|
|
|
298
326
|
function distribute(pos: "x" | "y", size: "width" | "height") {
|
|
299
|
-
const rect =
|
|
327
|
+
const rect = boxBounds(selectionBox(editor.selection))
|
|
300
328
|
const array = editor.selection.values().toArray()
|
|
301
329
|
|
|
302
330
|
const undo: HistoryAction[] = array.map((node) => [
|
package/lib/hooks/batch.ts
CHANGED
|
@@ -5,27 +5,37 @@ import { type HistoryAction } from "../model/history"
|
|
|
5
5
|
import { Node } from "../model/node"
|
|
6
6
|
import { useEditor } from "./editor"
|
|
7
7
|
|
|
8
|
+
export function reduce<T>(values: T[], fallback: T) {
|
|
9
|
+
const [first, ...rest] = values
|
|
10
|
+
return rest.reduce(
|
|
11
|
+
(acc, value) => (isEqual(value, acc) ? acc : fallback),
|
|
12
|
+
values.length > 0 ? first : fallback,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export function useNodeField<N extends Node, K extends keyof N>(
|
|
9
17
|
nodes: Iterable<N>,
|
|
10
18
|
key: K,
|
|
11
19
|
fallback: N[K],
|
|
12
20
|
) {
|
|
13
|
-
const
|
|
21
|
+
const values = Array.from(nodes).map((node) => node[key])
|
|
14
22
|
return useComputed<N[K]>({
|
|
15
23
|
equals: isEqual,
|
|
16
|
-
fn: () =>
|
|
17
|
-
rest.reduce(
|
|
18
|
-
(acc, node) => (isEqual(node[key], acc) ? acc : fallback),
|
|
19
|
-
first ? first[key] : fallback,
|
|
20
|
-
),
|
|
24
|
+
fn: () => reduce(values, fallback),
|
|
21
25
|
})
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
export interface NodeFieldBatch<N extends Node, T> {
|
|
29
|
+
value: T
|
|
30
|
+
onChange(set: T | ((node: N) => T)): void
|
|
31
|
+
onChangeEnd(set: T | ((node: N) => T)): void
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export function useNodeFieldBatch<N extends Node, K extends keyof N>(
|
|
25
35
|
nodes: N[],
|
|
26
36
|
key: K,
|
|
27
37
|
fallback: N[K],
|
|
28
|
-
) {
|
|
38
|
+
): NodeFieldBatch<N, N[K]> {
|
|
29
39
|
const { history } = useEditor()
|
|
30
40
|
const initial = useRef<Map<N, N[K]> | null>(null)
|
|
31
41
|
|
package/lib/hooks/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
export { useBatchSet, useNodeField, useNodeFieldBatch } from "./batch"
|
|
14
14
|
export { EditorContext, PageContext, useEditor, usePage } from "./editor"
|
|
15
15
|
export { useMoveable } from "./pointer/moveable"
|
|
16
|
+
export { useMovePoint } from "./pointer/movePoint"
|
|
16
17
|
export { usePointer } from "./pointer/pointer"
|
|
17
18
|
export { useResize } from "./pointer/resize"
|
|
18
19
|
export { useRotation } from "./pointer/rotation"
|
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
|
},
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import type { LineNode } from "../../model"
|
|
3
|
+
import {
|
|
4
|
+
point,
|
|
5
|
+
pointAdd,
|
|
6
|
+
pointSubtract,
|
|
7
|
+
rect,
|
|
8
|
+
type Point,
|
|
9
|
+
} from "../../model/geometry/math"
|
|
10
|
+
import { useEditor } from "../editor"
|
|
11
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
12
|
+
import { useSnap } from "./snap"
|
|
13
|
+
|
|
14
|
+
export function useMovePoint(props: { lineNode: LineNode; pointIndex: number }) {
|
|
15
|
+
const editor = useEditor()
|
|
16
|
+
const { lineNode, pointIndex } = props
|
|
17
|
+
const page = lineNode.page
|
|
18
|
+
const snap = useSnap(page)
|
|
19
|
+
|
|
20
|
+
const state = useRef({
|
|
21
|
+
cursorOffset: point(),
|
|
22
|
+
initialAnchor: point(),
|
|
23
|
+
initialPoints: [] as Point[],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return usePointer({
|
|
27
|
+
onDown(event) {
|
|
28
|
+
const cursor = cursorPosition(event, page)
|
|
29
|
+
const linePoint = lineNode.points[pointIndex]
|
|
30
|
+
const pointPosition = pointAdd(lineNode, linePoint)
|
|
31
|
+
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
event.stopPropagation()
|
|
34
|
+
|
|
35
|
+
state.current = {
|
|
36
|
+
cursorOffset: pointSubtract(cursor, pointPosition),
|
|
37
|
+
initialAnchor: point(lineNode),
|
|
38
|
+
initialPoints: Array.from(lineNode.points),
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
onMove(event) {
|
|
42
|
+
const { cursorOffset, initialPoints, initialAnchor } = state.current
|
|
43
|
+
const cursor = pointSubtract(cursorPosition(event, page), cursorOffset)
|
|
44
|
+
const snapped = snap(
|
|
45
|
+
!event.shiftKey,
|
|
46
|
+
rect({ x: cursor.x, y: cursor.y, width: 0, height: 0 }),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const points = initialPoints.map((p, i) =>
|
|
50
|
+
i === pointIndex ? snapped : pointAdd(initialAnchor, p),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const bounds = point(rect(...points))
|
|
54
|
+
|
|
55
|
+
lineNode.x = bounds.x
|
|
56
|
+
lineNode.y = bounds.y
|
|
57
|
+
lineNode.points = points.map((p) => pointSubtract(p, bounds))
|
|
58
|
+
|
|
59
|
+
editor.action = { action: "move", payload: point(snapped) }
|
|
60
|
+
},
|
|
61
|
+
onCancel() {
|
|
62
|
+
editor.action = {}
|
|
63
|
+
page.snapLines = []
|
|
64
|
+
},
|
|
65
|
+
onEnd() {
|
|
66
|
+
editor.action = {}
|
|
67
|
+
page.snapLines = []
|
|
68
|
+
|
|
69
|
+
lineNode.page.editor.history.push<LineNode>({
|
|
70
|
+
redo: ["set-node-props", [lineNode.id, { points: lineNode.points }]],
|
|
71
|
+
undo: ["set-node-props", [lineNode.id, { points: state.current.initialPoints }]],
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -1,73 +1,108 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
accessibleLine,
|
|
4
|
+
box,
|
|
5
|
+
boxBounds,
|
|
6
|
+
boxContainsPoint,
|
|
7
|
+
lineContainsPoint,
|
|
8
|
+
type Point,
|
|
9
|
+
point,
|
|
10
|
+
pointSubtract,
|
|
11
|
+
rect,
|
|
12
|
+
} from "../../model/geometry/math"
|
|
3
13
|
import type { HistoryEntry } from "../../model/history"
|
|
4
14
|
import type { Node } from "../../model/node"
|
|
5
|
-
import {
|
|
15
|
+
import { selectionBox } from "../../ui/selection"
|
|
6
16
|
import { useEditor, usePage } from "../editor"
|
|
7
|
-
import { usePointer } from "./pointer"
|
|
17
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
8
18
|
import { useSnap } from "./snap"
|
|
9
|
-
|
|
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
|
-
}
|
|
19
|
+
import { LineNode } from "@lazlon/html-editor/model"
|
|
20
20
|
|
|
21
21
|
export function useMoveable() {
|
|
22
22
|
const editor = useEditor()
|
|
23
23
|
const page = usePage()
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
24
|
+
const snap = useSnap(page)
|
|
25
|
+
|
|
26
|
+
const state = useRef({
|
|
27
|
+
initialSelectionRect: rect(),
|
|
28
|
+
cursorOffset: point(),
|
|
29
|
+
nodes: Array<{
|
|
30
|
+
node: Node
|
|
31
|
+
startingPoint: Point
|
|
32
|
+
selectionOffset: Point
|
|
33
|
+
}>(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function isCursorInSelection(cursor: Point) {
|
|
37
|
+
const [firstNode, ...rest] = editor.selection
|
|
38
|
+
if (rest.length === 0 && firstNode && firstNode instanceof LineNode) {
|
|
39
|
+
return lineContainsPoint(accessibleLine(firstNode), cursor)
|
|
40
|
+
}
|
|
41
|
+
return boxContainsPoint(selectionBox(editor.selection), cursor)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isNodeHit(node: Node, cursor: Point) {
|
|
45
|
+
if (node instanceof LineNode) {
|
|
46
|
+
return lineContainsPoint(accessibleLine(node), cursor)
|
|
47
|
+
}
|
|
48
|
+
return boxContainsPoint(box(node), cursor)
|
|
49
|
+
}
|
|
27
50
|
|
|
28
51
|
return usePointer({
|
|
29
52
|
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
|
-
},
|
|
53
|
+
const cursor = cursorPosition(event, page)
|
|
54
|
+
|
|
55
|
+
// clicked outside of selection, try grabbing nodes under the cursor
|
|
56
|
+
if (!isCursorInSelection(cursor)) {
|
|
57
|
+
const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
|
|
58
|
+
for (const node of stackOrderedNodes) {
|
|
59
|
+
if (isNodeHit(node, cursor)) {
|
|
60
|
+
editor.selection = new Set([node])
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
if (editor.selection.size === 0) return false
|
|
67
|
+
|
|
68
|
+
const initialSelectionBox = selectionBox(editor.selection)
|
|
69
|
+
const initialSelectionRect = boxBounds(initialSelectionBox)
|
|
70
|
+
|
|
71
|
+
if (isCursorInSelection(cursor)) {
|
|
72
|
+
state.current = {
|
|
73
|
+
initialSelectionRect,
|
|
74
|
+
cursorOffset: pointSubtract(cursor, initialSelectionRect),
|
|
75
|
+
nodes: editor.selection
|
|
76
|
+
.values()
|
|
77
|
+
.map((node) => ({
|
|
78
|
+
node,
|
|
79
|
+
startingPoint: point(node),
|
|
80
|
+
selectionOffset: pointSubtract(point(node), initialSelectionRect),
|
|
81
|
+
}))
|
|
82
|
+
.toArray(),
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
return false
|
|
52
86
|
}
|
|
53
|
-
|
|
54
|
-
return isPointerInSelectionRect(selection, event)
|
|
55
87
|
},
|
|
56
88
|
|
|
57
|
-
onMove(
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const { x, y } = snap(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
onMove(event) {
|
|
90
|
+
const cursor = cursorPosition(event, page)
|
|
91
|
+
const target = pointSubtract(cursor, state.current.cursorOffset)
|
|
92
|
+
|
|
93
|
+
const { x, y } = snap(
|
|
94
|
+
!event.shiftKey,
|
|
95
|
+
rect({
|
|
96
|
+
x: target.x,
|
|
97
|
+
y: target.y,
|
|
98
|
+
width: state.current.initialSelectionRect.width,
|
|
99
|
+
height: state.current.initialSelectionRect.height,
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for (const { node, selectionOffset } of state.current.nodes) {
|
|
104
|
+
node.x = x + selectionOffset.x
|
|
105
|
+
node.y = y + selectionOffset.y
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
editor.action = { action: "move", payload: { x, y } }
|
|
@@ -82,12 +117,12 @@ export function useMoveable() {
|
|
|
82
117
|
editor.action = {}
|
|
83
118
|
page.snapLines = []
|
|
84
119
|
|
|
85
|
-
const entries =
|
|
86
|
-
|
|
120
|
+
const entries: HistoryEntry[] = state.current.nodes.map(
|
|
121
|
+
({ node, startingPoint: start }) => ({
|
|
87
122
|
redo: ["set-node-props", [node.id, { x: node.x, y: node.y }]],
|
|
88
123
|
undo: ["set-node-props", [node.id, { x: start.x, y: start.y }]],
|
|
89
|
-
}
|
|
90
|
-
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
91
126
|
|
|
92
127
|
editor.history.push({
|
|
93
128
|
redo: ["batch", entries.map((e) => e.redo)],
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
import { Page } 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
|
+
page: Page,
|
|
15
|
+
): Point {
|
|
16
|
+
const { zoom } = page.editor
|
|
17
|
+
const { x, y } = page.ref!.getBoundingClientRect()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
x: floatNorm(event.clientX / zoom - x),
|
|
21
|
+
y: floatNorm(event.clientY / zoom - y),
|
|
22
|
+
}
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export function usePointer(props: UsePointerProps) {
|
|
@@ -23,7 +33,7 @@ export function usePointer(props: UsePointerProps) {
|
|
|
23
33
|
|
|
24
34
|
function onPointerMove(event: globalThis.PointerEvent) {
|
|
25
35
|
isMovingRef.current = true
|
|
26
|
-
onMove?.(
|
|
36
|
+
onMove?.(event)
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
function onPointerUp(event: globalThis.PointerEvent) {
|
|
@@ -32,9 +42,9 @@ export function usePointer(props: UsePointerProps) {
|
|
|
32
42
|
removeEventListener("pointercancel", onPointerUp)
|
|
33
43
|
|
|
34
44
|
if (isMovingRef.current) {
|
|
35
|
-
onEnd?.(
|
|
45
|
+
onEnd?.(event)
|
|
36
46
|
} else {
|
|
37
|
-
onCancel?.(
|
|
47
|
+
onCancel?.(event)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
isMovingRef.current = false
|