@lazlon-platform/html-editor 0.5.0 → 0.7.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 +136 -87
- package/lib/hooks/batch.ts +24 -10
- package/lib/hooks/index.ts +7 -6
- package/lib/hooks/page.ts +2 -4
- package/lib/hooks/pointer/useMovePoint.ts +100 -0
- package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +47 -39
- package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
- package/lib/hooks/pointer/useResize/index.ts +31 -0
- package/lib/hooks/pointer/useResize/multi.ts +161 -0
- package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
- package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
- package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
- package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
- package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
- package/lib/hooks/pointer/useRotation.ts +102 -0
- package/lib/hooks/pointer/{selector.ts → useSelector.ts} +18 -3
- package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
- package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
- package/lib/hooks/textMarks.ts +30 -21
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +31 -13
- package/lib/model/geometry/math.ts +128 -1
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +15 -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} +6 -12
- package/lib/model/node/lineNode.ts +80 -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} +9 -24
- package/lib/model/node.ts +27 -32
- package/lib/model/page.ts +4 -4
- 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} +10 -7
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +30 -0
- 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 +6 -5
- package/package.json +1 -1
- package/lib/hooks/pointer/resize.ts +0 -247
- package/lib/hooks/pointer/rotation.ts +0 -103
- 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 -130
- package/lib/model/node/shape/star.ts +0 -91
- 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,21 +1,36 @@
|
|
|
1
1
|
import { useComputed } from "react-bolt"
|
|
2
|
+
import {
|
|
3
|
+
flattenNodes,
|
|
4
|
+
GroupNode,
|
|
5
|
+
type HistoryAction,
|
|
6
|
+
type Node,
|
|
7
|
+
type Page,
|
|
8
|
+
} from "../model"
|
|
2
9
|
import type { Editor, NodeConstructor } from "../model/editor"
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
import {
|
|
11
|
+
boxBounds,
|
|
12
|
+
deg,
|
|
13
|
+
floatNorm,
|
|
14
|
+
rectCenter,
|
|
15
|
+
rotatePoint,
|
|
16
|
+
} from "../model/geometry/math"
|
|
8
17
|
import { selectionBox } from "../ui/selection"
|
|
9
18
|
import { useBatchSet } from "./batch"
|
|
10
19
|
import { useEditor } from "./editor"
|
|
11
|
-
import { boxBounds } from "../model/geometry/math"
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
function getSelection(editor: Editor) {
|
|
22
|
+
return editor.selection
|
|
23
|
+
.values()
|
|
24
|
+
.toArray()
|
|
25
|
+
.filter((n) => !n.locked)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clone<N extends Node>(
|
|
14
29
|
editor: Editor,
|
|
15
|
-
node:
|
|
16
|
-
props?: ((props:
|
|
30
|
+
node: N,
|
|
31
|
+
props?: ((props: N["props"]) => Partial<N["props"]>) | Partial<N["props"]>,
|
|
17
32
|
) {
|
|
18
|
-
const copy = node.
|
|
33
|
+
const copy = node.props
|
|
19
34
|
const newProps = typeof props === "function" ? props(copy) : props
|
|
20
35
|
const newNode = editor.deserializeNode(node.page, {
|
|
21
36
|
props: { ...copy, ...newProps },
|
|
@@ -35,14 +50,15 @@ export function useAddNodeAction(page?: Page) {
|
|
|
35
50
|
|
|
36
51
|
return function addNode<N extends NodeConstructor>(
|
|
37
52
|
NodeClass: N,
|
|
38
|
-
props?: Partial<
|
|
53
|
+
props?: Partial<N["prototype"]["props"]>,
|
|
39
54
|
) {
|
|
40
55
|
const [firstPage] = editor.pages.values()
|
|
41
56
|
const targetPage = page ?? firstPage
|
|
42
|
-
if (!targetPage)
|
|
57
|
+
if (!targetPage) throw Error("missing Page")
|
|
58
|
+
|
|
43
59
|
const { width, height, nodes } = targetPage
|
|
44
60
|
|
|
45
|
-
const node = new NodeClass(
|
|
61
|
+
const node = new NodeClass(targetPage, {
|
|
46
62
|
id: editor.id(),
|
|
47
63
|
x: Math.round(width / 2) - 50,
|
|
48
64
|
y: Math.round(height / 2) - 50,
|
|
@@ -53,7 +69,7 @@ export function useAddNodeAction(page?: Page) {
|
|
|
53
69
|
|
|
54
70
|
targetPage.nodes = new Map([...nodes, [node.id, node]])
|
|
55
71
|
editor.history.push({
|
|
56
|
-
redo: ["add-node", [targetPage.id, [
|
|
72
|
+
redo: ["add-node", [targetPage.id, [editor.serializeNode(node)]]],
|
|
57
73
|
undo: ["delete-node", [targetPage.id, [node.id]]],
|
|
58
74
|
})
|
|
59
75
|
|
|
@@ -67,7 +83,7 @@ export function useMoveAction() {
|
|
|
67
83
|
|
|
68
84
|
return function move(props: { x?: number; y?: number }) {
|
|
69
85
|
const { x, y } = props
|
|
70
|
-
const selection = editor
|
|
86
|
+
const selection = getSelection(editor)
|
|
71
87
|
if (x) batchSet(selection, (n) => ({ x: n.x + x }))
|
|
72
88
|
if (y) batchSet(selection, (n) => ({ y: n.y + y }))
|
|
73
89
|
}
|
|
@@ -77,17 +93,18 @@ export function useTrashAction() {
|
|
|
77
93
|
const editor = useEditor()
|
|
78
94
|
|
|
79
95
|
return function trash() {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
if (selection.
|
|
96
|
+
const page = editor.selectionPage
|
|
97
|
+
const selection = getSelection(editor)
|
|
98
|
+
if (selection.length === 0 || !page) return
|
|
83
99
|
|
|
84
100
|
editor.history.push({
|
|
85
|
-
redo: ["delete-node", [page.id,
|
|
86
|
-
undo: ["add-node", [page.id,
|
|
101
|
+
redo: ["delete-node", [page.id, selection.map((node) => node.id)]],
|
|
102
|
+
undo: ["add-node", [page.id, selection.map((n) => editor.serializeNode(n))]],
|
|
87
103
|
})
|
|
88
104
|
|
|
105
|
+
const current = editor.selection
|
|
89
106
|
editor.selection = new Set()
|
|
90
|
-
page.nodes = new Map(page.nodes.entries().filter(([, node]) => !
|
|
107
|
+
page.nodes = new Map(page.nodes.entries().filter(([, node]) => !current.has(node)))
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
110
|
|
|
@@ -95,32 +112,38 @@ export function useGroupAction() {
|
|
|
95
112
|
const editor = useEditor()
|
|
96
113
|
|
|
97
114
|
function group() {
|
|
98
|
-
const
|
|
115
|
+
const page = editor.selectionPage
|
|
116
|
+
const selection = getSelection(editor)
|
|
117
|
+
if (selection.length === 0 || !page) return
|
|
118
|
+
|
|
99
119
|
const { width, height, x, y } = boxBounds(selectionBox(selection))
|
|
100
|
-
if (selection.size === 0 || !page) return
|
|
101
120
|
|
|
102
|
-
const group = new GroupNode(
|
|
121
|
+
const group = new GroupNode(page, {
|
|
103
122
|
id: editor.id(),
|
|
104
123
|
width,
|
|
105
124
|
height,
|
|
106
125
|
x,
|
|
107
126
|
y,
|
|
108
|
-
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
group.nodes = new Set(
|
|
130
|
+
selection
|
|
109
131
|
.values()
|
|
110
132
|
.map((node) =>
|
|
111
133
|
clone(editor, node, (n) => ({
|
|
112
134
|
x: n.x - x,
|
|
113
135
|
y: n.y - y,
|
|
114
|
-
}))
|
|
136
|
+
})),
|
|
115
137
|
)
|
|
116
138
|
.toArray(),
|
|
117
|
-
|
|
139
|
+
)
|
|
118
140
|
|
|
141
|
+
const current = editor.selection
|
|
119
142
|
editor.selection = new Set()
|
|
120
143
|
page.nodes = new Map([
|
|
121
144
|
...page.nodes
|
|
122
145
|
.values()
|
|
123
|
-
.filter((n) => !
|
|
146
|
+
.filter((n) => !current.has(n))
|
|
124
147
|
.map((n) => [n.id, n] as const),
|
|
125
148
|
[group.id, group],
|
|
126
149
|
])
|
|
@@ -139,7 +162,7 @@ export function useGroupAction() {
|
|
|
139
162
|
.toArray(),
|
|
140
163
|
],
|
|
141
164
|
],
|
|
142
|
-
["add-node", [page.id, [
|
|
165
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
143
166
|
],
|
|
144
167
|
],
|
|
145
168
|
undo: [
|
|
@@ -152,7 +175,7 @@ export function useGroupAction() {
|
|
|
152
175
|
page.id,
|
|
153
176
|
selection
|
|
154
177
|
.values()
|
|
155
|
-
.map((n) =>
|
|
178
|
+
.map((n) => editor.serializeNode(n))
|
|
156
179
|
.toArray(),
|
|
157
180
|
],
|
|
158
181
|
],
|
|
@@ -162,14 +185,31 @@ export function useGroupAction() {
|
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
function ungroup(group: GroupNode) {
|
|
188
|
+
if (group.locked) return
|
|
189
|
+
|
|
165
190
|
const page = group.page
|
|
191
|
+
const groupCenter = rectCenter(group)
|
|
166
192
|
const nodes = group.nodes
|
|
167
193
|
.values()
|
|
168
194
|
.map((node) =>
|
|
169
|
-
clone(editor, node, (n) =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
clone(editor, node, (n) => {
|
|
196
|
+
const center = rotatePoint(
|
|
197
|
+
rectCenter({
|
|
198
|
+
x: n.x + group.x,
|
|
199
|
+
y: n.y + group.y,
|
|
200
|
+
width: n.width,
|
|
201
|
+
height: n.height,
|
|
202
|
+
}),
|
|
203
|
+
groupCenter,
|
|
204
|
+
group.rotation,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
x: floatNorm(center.x - n.width / 2),
|
|
209
|
+
y: floatNorm(center.y - n.height / 2),
|
|
210
|
+
rotation: deg((n.rotation ?? 0) + group.rotation),
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
173
213
|
)
|
|
174
214
|
.toArray()
|
|
175
215
|
|
|
@@ -184,14 +224,14 @@ export function useGroupAction() {
|
|
|
184
224
|
"batch",
|
|
185
225
|
[
|
|
186
226
|
["delete-node", [page.id, [group.id]]],
|
|
187
|
-
["add-node", [page.id, nodes.map((n) =>
|
|
227
|
+
["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
|
|
188
228
|
],
|
|
189
229
|
],
|
|
190
230
|
undo: [
|
|
191
231
|
"batch",
|
|
192
232
|
[
|
|
193
233
|
["delete-node", [page.id, nodes.map((node) => node.id)]],
|
|
194
|
-
["add-node", [page.id, [
|
|
234
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
195
235
|
],
|
|
196
236
|
],
|
|
197
237
|
})
|
|
@@ -204,30 +244,24 @@ export function useDuplicateAction() {
|
|
|
204
244
|
const editor = useEditor()
|
|
205
245
|
|
|
206
246
|
return function duplicate(props?: { page?: Page; nodes?: Set<Node>; offset?: number }) {
|
|
207
|
-
const selection = props?.nodes ?? editor.selection
|
|
208
247
|
const offset = props?.offset ?? 10
|
|
209
248
|
const page = props?.page ?? editor.selectionPage
|
|
210
|
-
|
|
249
|
+
const nodes = Array.from(props?.nodes ?? getSelection(editor))
|
|
250
|
+
if (nodes.length === 0 || !page) return
|
|
211
251
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
})),
|
|
219
|
-
)
|
|
220
|
-
.toArray()
|
|
252
|
+
const dup = nodes.map((node) =>
|
|
253
|
+
clone(editor, node, (n) => ({
|
|
254
|
+
x: n.x + offset,
|
|
255
|
+
y: n.y + offset,
|
|
256
|
+
})),
|
|
257
|
+
)
|
|
221
258
|
|
|
222
|
-
page.nodes = new Map([
|
|
223
|
-
...page.nodes,
|
|
224
|
-
...nodes.map((node) => [node.id, node] as const),
|
|
225
|
-
])
|
|
259
|
+
page.nodes = new Map([...page.nodes, ...dup.map((node) => [node.id, node] as const)])
|
|
226
260
|
|
|
227
|
-
editor.selection = new Set(
|
|
261
|
+
editor.selection = new Set(dup)
|
|
228
262
|
editor.history.push({
|
|
229
|
-
redo: ["add-node", [page.id,
|
|
230
|
-
undo: ["delete-node", [page.id,
|
|
263
|
+
redo: ["add-node", [page.id, dup.map((n) => editor.serializeNode(n))]],
|
|
264
|
+
undo: ["delete-node", [page.id, dup.map((node) => node.id)]],
|
|
231
265
|
})
|
|
232
266
|
}
|
|
233
267
|
}
|
|
@@ -237,26 +271,15 @@ export function useToggleLockAction() {
|
|
|
237
271
|
|
|
238
272
|
return function toggleLock() {
|
|
239
273
|
const selection = editor.selection.values().toArray()
|
|
240
|
-
const
|
|
241
|
-
for (const node of selection) {
|
|
242
|
-
node.locked = !isLocked
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
274
|
+
const nextLocked = selection.some((n) => !n.locked)
|
|
246
275
|
|
|
247
|
-
export function useAlignAction() {
|
|
248
|
-
type AlignProps = { x: number } | { y: number }
|
|
249
|
-
|
|
250
|
-
const editor = useEditor()
|
|
251
|
-
|
|
252
|
-
function align(fn: (node: Node) => { prev: AlignProps; next: AlignProps }) {
|
|
253
276
|
const undo: HistoryAction[] = []
|
|
254
277
|
const redo: HistoryAction[] = []
|
|
255
278
|
|
|
256
|
-
for (const node of
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
279
|
+
for (const node of selection) {
|
|
280
|
+
undo.push(["set-node-props", [node.id, { locked: node.locked }]])
|
|
281
|
+
redo.push(["set-node-props", [node.id, { locked: nextLocked }]])
|
|
282
|
+
node.locked = nextLocked
|
|
260
283
|
}
|
|
261
284
|
|
|
262
285
|
editor.history.push({
|
|
@@ -264,6 +287,24 @@ export function useAlignAction() {
|
|
|
264
287
|
redo: ["batch", redo],
|
|
265
288
|
})
|
|
266
289
|
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function useAlignAction() {
|
|
293
|
+
type AlignProps = { x: number } | { y: number }
|
|
294
|
+
|
|
295
|
+
const editor = useEditor()
|
|
296
|
+
|
|
297
|
+
function align(set: (node: Node) => { prev: AlignProps; next: AlignProps }) {
|
|
298
|
+
editor.pushHistory(
|
|
299
|
+
getSelection(editor).map((node) => {
|
|
300
|
+
const { prev, next } = set(node)
|
|
301
|
+
return {
|
|
302
|
+
undo: ["set-node-props", [node.id, prev]],
|
|
303
|
+
redo: ["set-node-props", [node.id, next]],
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
306
|
+
)
|
|
307
|
+
}
|
|
267
308
|
|
|
268
309
|
function halign(getX: (node: Node) => number) {
|
|
269
310
|
align((node) => {
|
|
@@ -297,20 +338,22 @@ export function useDistributeAction() {
|
|
|
297
338
|
const editor = useEditor()
|
|
298
339
|
|
|
299
340
|
function distribute(pos: "x" | "y", size: "width" | "height") {
|
|
341
|
+
const selection = getSelection(editor)
|
|
342
|
+
if (selection.length === 0) return
|
|
343
|
+
|
|
300
344
|
const rect = boxBounds(selectionBox(editor.selection))
|
|
301
|
-
const array = editor.selection.values().toArray()
|
|
302
345
|
|
|
303
|
-
const undo: HistoryAction[] =
|
|
346
|
+
const undo: HistoryAction[] = selection.map((node) => [
|
|
304
347
|
"set-node-props",
|
|
305
348
|
[node.id, { [pos]: node[pos] }],
|
|
306
349
|
])
|
|
307
350
|
|
|
308
|
-
const total =
|
|
309
|
-
const gap = (rect[size] - total) / (
|
|
351
|
+
const total = selection.reduce((sum, it) => sum + it[size], 0)
|
|
352
|
+
const gap = (rect[size] - total) / (selection.length - 1)
|
|
310
353
|
|
|
311
354
|
let cursor = rect[pos]
|
|
312
355
|
|
|
313
|
-
const redo: HistoryAction[] =
|
|
356
|
+
const redo: HistoryAction[] = selection
|
|
314
357
|
.toSorted((a, b) => a[pos] - b[pos])
|
|
315
358
|
.map((node) => {
|
|
316
359
|
node[pos] = cursor
|
|
@@ -375,15 +418,17 @@ export function useStackOrderAction() {
|
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
const canBringBackward = useComputed(() => {
|
|
378
|
-
const
|
|
379
|
-
|
|
421
|
+
const page = editor.selectionPage
|
|
422
|
+
const selection = getSelection(editor)
|
|
423
|
+
if (selection.length === 0 || !page) return false
|
|
380
424
|
const nodes = page.nodes.values().toArray()
|
|
381
425
|
return selection.values().some((n) => n !== nodes.at(0))
|
|
382
426
|
})
|
|
383
427
|
|
|
384
428
|
const canBringForward = useComputed(() => {
|
|
385
|
-
const
|
|
386
|
-
|
|
429
|
+
const page = editor.selectionPage
|
|
430
|
+
const selection = getSelection(editor)
|
|
431
|
+
if (selection.length === 0 || !page) return false
|
|
387
432
|
const nodes = page.nodes.values().toArray()
|
|
388
433
|
return selection.values().some((n) => n !== nodes.at(-1))
|
|
389
434
|
})
|
|
@@ -392,17 +437,20 @@ export function useStackOrderAction() {
|
|
|
392
437
|
canBringBackward,
|
|
393
438
|
canBringForward,
|
|
394
439
|
bringBackward() {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
440
|
+
const page = editor.selectionPage
|
|
441
|
+
const selection = getSelection(editor)
|
|
442
|
+
if (selection.length === 0 || !page) return
|
|
443
|
+
order(page, moveBackward(page.nodes.values().toArray(), new Set(selection)))
|
|
398
444
|
},
|
|
399
445
|
bringForward() {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
446
|
+
const page = editor.selectionPage
|
|
447
|
+
const selection = getSelection(editor)
|
|
448
|
+
if (selection.length === 0 || !page) return
|
|
449
|
+
order(page, moveForward(page.nodes.values().toArray(), new Set(selection)))
|
|
403
450
|
},
|
|
404
451
|
bringToBack() {
|
|
405
|
-
const
|
|
452
|
+
const page = editor.selectionPage
|
|
453
|
+
const selection = new Set(getSelection(editor))
|
|
406
454
|
if (selection.size === 0 || !page) return
|
|
407
455
|
order(page, [
|
|
408
456
|
...selection,
|
|
@@ -410,7 +458,8 @@ export function useStackOrderAction() {
|
|
|
410
458
|
])
|
|
411
459
|
},
|
|
412
460
|
bringToFront() {
|
|
413
|
-
const
|
|
461
|
+
const page = editor.selectionPage
|
|
462
|
+
const selection = new Set(getSelection(editor))
|
|
414
463
|
if (selection.size === 0 || !page) return
|
|
415
464
|
order(page, [
|
|
416
465
|
...page.nodes.values().filter((node) => !selection.has(node)),
|
package/lib/hooks/batch.ts
CHANGED
|
@@ -5,27 +5,39 @@ 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 [first, ...rest] = nodes
|
|
14
21
|
return useComputed<N[K]>({
|
|
15
22
|
equals: isEqual,
|
|
16
|
-
fn: () =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
),
|
|
23
|
+
fn: () => {
|
|
24
|
+
const values = Array.from(nodes).map((node) => node[key])
|
|
25
|
+
return reduce(values, fallback)
|
|
26
|
+
},
|
|
21
27
|
})
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
export interface NodeFieldBatch<N extends Node, T> {
|
|
31
|
+
value: T
|
|
32
|
+
onChange(set: T | ((node: N) => T)): void
|
|
33
|
+
onChangeEnd(set: T | ((node: N) => T)): void
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export function useNodeFieldBatch<N extends Node, K extends keyof N>(
|
|
25
37
|
nodes: N[],
|
|
26
38
|
key: K,
|
|
27
39
|
fallback: N[K],
|
|
28
|
-
) {
|
|
40
|
+
): NodeFieldBatch<N, N[K]> {
|
|
29
41
|
const { history } = useEditor()
|
|
30
42
|
const initial = useRef<Map<N, N[K]> | null>(null)
|
|
31
43
|
|
|
@@ -82,12 +94,10 @@ export function useBatchSet() {
|
|
|
82
94
|
nodes: Iterable<N>,
|
|
83
95
|
set: Props<N> | ((n: N) => Props<N>),
|
|
84
96
|
) {
|
|
85
|
-
let push = false
|
|
86
97
|
const prev: HistoryAction[] = []
|
|
87
98
|
const next: HistoryAction[] = []
|
|
88
99
|
|
|
89
100
|
for (const node of nodes) {
|
|
90
|
-
push = true
|
|
91
101
|
const nextValues = typeof set === "function" ? set(node) : set
|
|
92
102
|
const keys = Object.keys(nextValues) as WritableKeys<N>[]
|
|
93
103
|
const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
|
|
@@ -96,8 +106,12 @@ export function useBatchSet() {
|
|
|
96
106
|
Object.assign(node, nextValues)
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
if (
|
|
109
|
+
if (next.length > 1) {
|
|
100
110
|
history.push({ undo: ["batch", prev], redo: ["batch", next] })
|
|
111
|
+
} else if (next.length > 0) {
|
|
112
|
+
const [undo] = prev
|
|
113
|
+
const [redo] = next
|
|
114
|
+
history.push({ undo, redo })
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
}
|
package/lib/hooks/index.ts
CHANGED
|
@@ -12,12 +12,13 @@ export {
|
|
|
12
12
|
} from "./actions"
|
|
13
13
|
export { useBatchSet, useNodeField, useNodeFieldBatch } from "./batch"
|
|
14
14
|
export { EditorContext, PageContext, useEditor, usePage } from "./editor"
|
|
15
|
-
export { useMoveable } from "./pointer/
|
|
16
|
-
export {
|
|
17
|
-
export {
|
|
18
|
-
export {
|
|
19
|
-
export {
|
|
20
|
-
export {
|
|
15
|
+
export { useMoveable } from "./pointer/useMoveable"
|
|
16
|
+
export { useMovePoint } from "./pointer/useMovePoint"
|
|
17
|
+
export { usePointer } from "./pointer/usePointer"
|
|
18
|
+
export { useResize } from "./pointer/useResize"
|
|
19
|
+
export { useRotation } from "./pointer/useRotation"
|
|
20
|
+
export { useSelectionFrame } from "./selectionFrame"
|
|
21
|
+
export { useSelector } from "./pointer/useSelector"
|
|
21
22
|
export { useTextMarks } from "./textMarks"
|
|
22
23
|
export { useEditPage } from "./page"
|
|
23
24
|
export { useVisualPositionBatch } from "./node"
|
package/lib/hooks/page.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
-
import type { Page } from "../model"
|
|
3
|
-
import { useEditor } from "./editor"
|
|
4
2
|
import { useStore } from "react-bolt"
|
|
3
|
+
import type { Page } from "../model"
|
|
5
4
|
|
|
6
5
|
export function useEditPage<Key extends keyof Page>(page: Page, key: Key) {
|
|
7
|
-
const editor = useEditor()
|
|
8
6
|
const init = useRef(page[key])
|
|
9
7
|
const value = useStore(page, key)
|
|
10
8
|
|
|
@@ -14,7 +12,7 @@ export function useEditPage<Key extends keyof Page>(page: Page, key: Key) {
|
|
|
14
12
|
page[key] = value
|
|
15
13
|
},
|
|
16
14
|
onChangeEnd(value: Page[Key]) {
|
|
17
|
-
editor.history.push({
|
|
15
|
+
page.editor.history.push({
|
|
18
16
|
redo: ["set-page-props", [page.id, { [key]: value }]],
|
|
19
17
|
undo: ["set-page-props", [page.id, { [key]: init.current }]],
|
|
20
18
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import type { LineNode } from "../../model"
|
|
3
|
+
import {
|
|
4
|
+
deg,
|
|
5
|
+
floatNorm,
|
|
6
|
+
point,
|
|
7
|
+
pointSubtract,
|
|
8
|
+
rect,
|
|
9
|
+
rectCenter,
|
|
10
|
+
rotatePoint,
|
|
11
|
+
type Point,
|
|
12
|
+
} from "../../model/geometry/math"
|
|
13
|
+
import { useEditor } from "../editor"
|
|
14
|
+
import { cursorPosition, usePointer } from "./usePointer"
|
|
15
|
+
import { useSnap } from "./useSnap"
|
|
16
|
+
|
|
17
|
+
export function useMovePoint(props: { lineNode: LineNode; pointIndex: number }) {
|
|
18
|
+
const { lineNode, pointIndex } = props
|
|
19
|
+
const editor = useEditor()
|
|
20
|
+
const page = lineNode.page
|
|
21
|
+
const snap = useSnap(page)
|
|
22
|
+
|
|
23
|
+
const state = useRef({
|
|
24
|
+
cursorOffset: point(),
|
|
25
|
+
baseVertices: [] as Point[],
|
|
26
|
+
basePoints: [] as Point[],
|
|
27
|
+
baseX: 0,
|
|
28
|
+
baseY: 0,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function unrotatedPagePoints(vertices: Point[]): Point[] {
|
|
32
|
+
const rotation = lineNode.rotation
|
|
33
|
+
const origin = point()
|
|
34
|
+
const unrotatedBounds = rect(
|
|
35
|
+
...vertices.map((p) => rotatePoint(p, origin, deg(-rotation))),
|
|
36
|
+
)
|
|
37
|
+
const center = rotatePoint(rectCenter(unrotatedBounds), origin, rotation)
|
|
38
|
+
return vertices.map((p) => rotatePoint(p, center, deg(-rotation)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setLinePoints(pagePoints: Point[]) {
|
|
42
|
+
const bounds = point(rect(...pagePoints))
|
|
43
|
+
lineNode.x = floatNorm(bounds.x)
|
|
44
|
+
lineNode.y = floatNorm(bounds.y)
|
|
45
|
+
lineNode.points = pagePoints.map((p) => pointSubtract(p, bounds))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redoProps() {
|
|
49
|
+
const { x, y, points } = lineNode
|
|
50
|
+
return { x, y, points }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function undoProps() {
|
|
54
|
+
const { baseX: x, baseY: y, basePoints: points } = state.current
|
|
55
|
+
return { x, y, points }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return usePointer({
|
|
59
|
+
onDown(event) {
|
|
60
|
+
if (lineNode.locked) return false
|
|
61
|
+
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
|
|
65
|
+
state.current = {
|
|
66
|
+
baseVertices: lineNode.vertices,
|
|
67
|
+
basePoints: lineNode.points,
|
|
68
|
+
baseX: lineNode.x,
|
|
69
|
+
baseY: lineNode.y,
|
|
70
|
+
cursorOffset: pointSubtract(
|
|
71
|
+
cursorPosition(event, page),
|
|
72
|
+
lineNode.vertices[pointIndex],
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
onMove(event) {
|
|
77
|
+
const { cursorOffset, baseVertices } = state.current
|
|
78
|
+
const cursor = pointSubtract(cursorPosition(event, page), cursorOffset)
|
|
79
|
+
const result = point(snap(!event.shiftKey, rect(cursor)))
|
|
80
|
+
const vertices = baseVertices.map((p, i) => (i === pointIndex ? result : p))
|
|
81
|
+
|
|
82
|
+
setLinePoints(unrotatedPagePoints(vertices))
|
|
83
|
+
|
|
84
|
+
editor.action = { action: "move", payload: result }
|
|
85
|
+
},
|
|
86
|
+
onCancel() {
|
|
87
|
+
editor.action = {}
|
|
88
|
+
page.snapLines = []
|
|
89
|
+
},
|
|
90
|
+
onEnd() {
|
|
91
|
+
editor.action = {}
|
|
92
|
+
page.snapLines = []
|
|
93
|
+
|
|
94
|
+
editor.pushHistory({
|
|
95
|
+
redo: ["set-node-props", [lineNode.id, redoProps()]],
|
|
96
|
+
undo: ["set-node-props", [lineNode.id, undoProps()]],
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
}
|