@lazlon-platform/html-editor 0.7.1 → 0.7.3
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/batch.ts +39 -41
- package/lib/hooks/node.ts +4 -13
- package/lib/hooks/pointer/useMoveable.ts +1 -2
- package/lib/hooks/pointer/useResize/multiLineNode.ts +9 -11
- package/lib/hooks/pointer/useResize/multiRegularNode.ts +1 -2
- package/lib/hooks/pointer/useResize/multiTextNode.ts +1 -1
- package/lib/hooks/pointer/useResize/singleRegularNode.ts +1 -1
- package/lib/hooks/pointer/useResize/singleTextNode.ts +1 -1
- package/lib/hooks/pointer/useRotation.ts +1 -1
- package/lib/hooks/pointer/useSelector.ts +6 -6
- package/lib/hooks/pointer/useSnap.ts +3 -3
- package/lib/hooks/selectionFrame.ts +24 -17
- package/lib/model/geometry/math.ts +6 -6
- package/lib/model/node/lineNode.ts +13 -0
- package/lib/model/node.ts +5 -1
- package/lib/ui/node/ImageContent.tsx +2 -1
- package/lib/ui/selection.ts +3 -13
- package/package.json +1 -1
package/lib/hooks/batch.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useRef } from "react"
|
|
3
3
|
import { useComputed } from "react-bolt"
|
|
4
|
-
import { type HistoryAction } from "../model/history"
|
|
5
4
|
import { Node } from "../model/node"
|
|
6
5
|
import { useEditor } from "./editor"
|
|
7
6
|
|
|
@@ -13,15 +12,15 @@ export function reduce<T>(values: T[], fallback: T) {
|
|
|
13
12
|
)
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export function useNodeField<N extends Node,
|
|
15
|
+
export function useNodeField<N extends Node, T>(
|
|
17
16
|
nodes: Iterable<N>,
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
fallback: T,
|
|
18
|
+
getter: (node: N) => T,
|
|
20
19
|
) {
|
|
21
|
-
return useComputed<
|
|
20
|
+
return useComputed<T>({
|
|
22
21
|
equals: isEqual,
|
|
23
22
|
fn: () => {
|
|
24
|
-
const values = Array.from(nodes).map((node) => node
|
|
23
|
+
const values = Array.from(nodes).map((node) => getter(node))
|
|
25
24
|
return reduce(values, fallback)
|
|
26
25
|
},
|
|
27
26
|
})
|
|
@@ -38,11 +37,11 @@ export function useNodeFieldBatch<N extends Node, K extends keyof N>(
|
|
|
38
37
|
key: K,
|
|
39
38
|
fallback: N[K],
|
|
40
39
|
): NodeFieldBatch<N, N[K]> {
|
|
41
|
-
const
|
|
40
|
+
const editor = useEditor()
|
|
42
41
|
const initial = useRef<Map<N, N[K]> | null>(null)
|
|
43
42
|
|
|
44
43
|
return {
|
|
45
|
-
value: useNodeField(nodes,
|
|
44
|
+
value: useNodeField(nodes, fallback, (node) => node[key]),
|
|
46
45
|
onChange(set: N[K] | ((node: N) => N[K])) {
|
|
47
46
|
if (nodes.length === 0) return
|
|
48
47
|
|
|
@@ -51,7 +50,9 @@ export function useNodeFieldBatch<N extends Node, K extends keyof N>(
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
for (const node of nodes) {
|
|
54
|
-
|
|
53
|
+
if (!node.locked) {
|
|
54
|
+
node[key] = typeof set === "function" ? (set as (node: N) => N[K])(node) : set
|
|
55
|
+
}
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
onChangeEnd(end: N[K] | ((node: N) => N[K])) {
|
|
@@ -59,19 +60,21 @@ export function useNodeFieldBatch<N extends Node, K extends keyof N>(
|
|
|
59
60
|
|
|
60
61
|
const init = initial.current || new Map(nodes.map((n) => [n, n[key]]))
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
editor.pushHistory(
|
|
64
|
+
nodes
|
|
65
|
+
.filter((node) => !node.locked)
|
|
66
|
+
.map((node) => {
|
|
67
|
+
const nextValue =
|
|
68
|
+
typeof end === "function" ? (end as (node: N) => N[K])(node) : end
|
|
69
|
+
|
|
70
|
+
node[key] = nextValue
|
|
71
|
+
return {
|
|
72
|
+
undo: ["set-node-props", [node.id, { [key]: init.get(node) }]],
|
|
73
|
+
redo: ["set-node-props", [node.id, { [key]: nextValue }]],
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
73
77
|
|
|
74
|
-
history.push({ undo: ["batch", prev], redo: ["batch", next] })
|
|
75
78
|
initial.current = null
|
|
76
79
|
},
|
|
77
80
|
}
|
|
@@ -88,30 +91,25 @@ type Writable<T> = Pick<T, WritableKeys<T>>
|
|
|
88
91
|
type Props<N> = Partial<Writable<{ [K in keyof N]: N[K] }>>
|
|
89
92
|
|
|
90
93
|
export function useBatchSet() {
|
|
91
|
-
const
|
|
94
|
+
const editor = useEditor()
|
|
92
95
|
|
|
93
96
|
return function batchSet<N extends Node>(
|
|
94
97
|
nodes: Iterable<N>,
|
|
95
98
|
set: Props<N> | ((n: N) => Props<N>),
|
|
96
99
|
) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} else if (next.length > 0) {
|
|
112
|
-
const [undo] = prev
|
|
113
|
-
const [redo] = next
|
|
114
|
-
history.push({ undo, redo })
|
|
115
|
-
}
|
|
100
|
+
editor.pushHistory(
|
|
101
|
+
Array.from(nodes)
|
|
102
|
+
.filter((node) => !node.locked)
|
|
103
|
+
.map((node) => {
|
|
104
|
+
const nextValues = typeof set === "function" ? set(node) : set
|
|
105
|
+
const keys = Object.keys(nextValues) as WritableKeys<N>[]
|
|
106
|
+
const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
|
|
107
|
+
Object.assign(node, nextValues)
|
|
108
|
+
return {
|
|
109
|
+
undo: ["set-node-props", [node.id, prevValues]],
|
|
110
|
+
redo: ["set-node-props", [node.id, nextValues]],
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
116
114
|
}
|
|
117
115
|
}
|
package/lib/hooks/node.ts
CHANGED
|
@@ -3,21 +3,12 @@ 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,
|
|
7
|
-
const width = useNodeField(nodes,
|
|
8
|
-
const height = useNodeField(nodes,
|
|
6
|
+
const rotation = useNodeField(nodes, deg(0), (node) => node.rotation)
|
|
7
|
+
const width = useNodeField(nodes, NaN, (node) => node.width)
|
|
8
|
+
const height = useNodeField(nodes, NaN, (node) => node.height)
|
|
9
9
|
const x = useNodeFieldBatch(nodes, "x", NaN)
|
|
10
10
|
const y = useNodeFieldBatch(nodes, "y", NaN)
|
|
11
|
-
|
|
12
|
-
const bounds = boxBounds(
|
|
13
|
-
box({
|
|
14
|
-
x: x.value,
|
|
15
|
-
y: y.value,
|
|
16
|
-
width,
|
|
17
|
-
height,
|
|
18
|
-
rotation,
|
|
19
|
-
}),
|
|
20
|
-
)
|
|
11
|
+
const bounds = boxBounds(box({ x: x.value, y: y.value, width, height }, rotation))
|
|
21
12
|
|
|
22
13
|
function withBoundingBox(axis: "x" | "y", fn: (value: number) => void) {
|
|
23
14
|
return function (value: number): void {
|
|
@@ -2,7 +2,6 @@ import { useRef } from "react"
|
|
|
2
2
|
import { LineNode } from "../../model"
|
|
3
3
|
import {
|
|
4
4
|
accessibleLine,
|
|
5
|
-
box,
|
|
6
5
|
boxBounds,
|
|
7
6
|
boxContainsPoint,
|
|
8
7
|
lineContainsPoint,
|
|
@@ -44,7 +43,7 @@ export function useMoveable() {
|
|
|
44
43
|
if (node instanceof LineNode) {
|
|
45
44
|
return lineContainsPoint(accessibleLine(node), cursor)
|
|
46
45
|
}
|
|
47
|
-
return boxContainsPoint(
|
|
46
|
+
return boxContainsPoint(node.toBox(), cursor)
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
return usePointer({
|
|
@@ -65,21 +65,19 @@ export function useMultiLineNodeResize(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function undoProps(state: NodeState) {
|
|
68
|
-
const { baseX
|
|
69
|
-
return { x, y, points }
|
|
68
|
+
const { baseX, baseY, basePoints } = state
|
|
69
|
+
return { x: baseX, y: baseY, points: basePoints }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
73
|
onDown({ baseSelectionRect }) {
|
|
74
|
-
state.current = nodes.map((node) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
})
|
|
74
|
+
state.current = nodes.map((node) => ({
|
|
75
|
+
node,
|
|
76
|
+
basePoints: node.points,
|
|
77
|
+
baseX: node.x,
|
|
78
|
+
baseY: node.y,
|
|
79
|
+
relativeVertices: node.vertices.map((p) => relativePoint(p, baseSelectionRect)),
|
|
80
|
+
}))
|
|
83
81
|
},
|
|
84
82
|
|
|
85
83
|
onMove({ selectionRect }) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
2
|
import type { Node } from "../../../model"
|
|
3
3
|
import {
|
|
4
|
-
box,
|
|
5
4
|
boxRect,
|
|
6
5
|
rect,
|
|
7
6
|
rectCenter,
|
|
@@ -78,7 +77,7 @@ export function useMultiRegularNodeResize(
|
|
|
78
77
|
onDown({ relativeSize }) {
|
|
79
78
|
state.current = nodes.map((node) => ({
|
|
80
79
|
node,
|
|
81
|
-
baseSize:
|
|
80
|
+
baseSize: node.toBox(),
|
|
82
81
|
relative: relativeSize(node),
|
|
83
82
|
}))
|
|
84
83
|
},
|
|
@@ -48,7 +48,7 @@ export function useRotation() {
|
|
|
48
48
|
state.current = {
|
|
49
49
|
base: angle(pivot, origin, cursorPosition(event, page)),
|
|
50
50
|
pivot,
|
|
51
|
-
nodes: nodes.map((node) => ({ node, baseBox: box(node) })),
|
|
51
|
+
nodes: nodes.map((node) => ({ node, baseBox: box(node, node.rotation) })),
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
editor.action = {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
+
import { LineNode } from "../../model"
|
|
2
3
|
import {
|
|
3
4
|
type Rect,
|
|
5
|
+
accessibleLine,
|
|
4
6
|
box,
|
|
5
7
|
boxContainsPoint,
|
|
6
|
-
pointSubtract,
|
|
7
|
-
rect,
|
|
8
8
|
boxIntersects,
|
|
9
9
|
lineContainsPoint,
|
|
10
10
|
lineIntersectsBox,
|
|
11
|
-
|
|
11
|
+
pointSubtract,
|
|
12
|
+
rect,
|
|
12
13
|
} from "../../model/geometry/math"
|
|
13
14
|
import { useEditor } from "../editor"
|
|
14
15
|
import { cursorPosition, usePointer } from "./usePointer"
|
|
15
|
-
import { LineNode } from "../../model"
|
|
16
16
|
|
|
17
17
|
function clientPoint(event: { clientX: number; clientY: number }) {
|
|
18
18
|
return { x: event.clientX, y: event.clientY }
|
|
@@ -32,7 +32,7 @@ export function useSelector(view: (props: null | Rect) => void) {
|
|
|
32
32
|
const cursor = cursorPosition(event, node.page)
|
|
33
33
|
if (
|
|
34
34
|
(node instanceof LineNode && lineContainsPoint(accessibleLine(node), cursor)) ||
|
|
35
|
-
(!(node instanceof LineNode) && boxContainsPoint(
|
|
35
|
+
(!(node instanceof LineNode) && boxContainsPoint(node.toBox(), cursor))
|
|
36
36
|
) {
|
|
37
37
|
return false
|
|
38
38
|
}
|
|
@@ -68,7 +68,7 @@ export function useSelector(view: (props: null | Rect) => void) {
|
|
|
68
68
|
if (node instanceof LineNode) {
|
|
69
69
|
return lineIntersectsBox(node, selection)
|
|
70
70
|
} else {
|
|
71
|
-
return boxIntersects(selection,
|
|
71
|
+
return boxIntersects(selection, node.toBox())
|
|
72
72
|
}
|
|
73
73
|
})
|
|
74
74
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useStore } from "react-bolt"
|
|
2
|
-
import { type Rect,
|
|
2
|
+
import { type Rect, boxBounds, rect } from "../../model/geometry/math"
|
|
3
3
|
import type { Node } from "../../model/node"
|
|
4
4
|
import type { Page } from "../../model/page"
|
|
5
5
|
import { useEditor } from "../editor"
|
|
@@ -18,7 +18,7 @@ export function useSnap(page: Page) {
|
|
|
18
18
|
|
|
19
19
|
function ySnap(nodes: Node[], rect: Rect): [number, ...Lines] {
|
|
20
20
|
const nodelines = nodes.flatMap((n) => {
|
|
21
|
-
const { y, height } = boxBounds(
|
|
21
|
+
const { y, height } = boxBounds(n.toBox())
|
|
22
22
|
return [y, y + height]
|
|
23
23
|
})
|
|
24
24
|
|
|
@@ -49,7 +49,7 @@ export function useSnap(page: Page) {
|
|
|
49
49
|
|
|
50
50
|
function xSnap(nodes: Node[], rect: Rect): [number, ...Lines] {
|
|
51
51
|
const nodelines = nodes.flatMap((n) => {
|
|
52
|
-
const { x, width } = boxBounds(
|
|
52
|
+
const { x, width } = boxBounds(n.toBox())
|
|
53
53
|
return [x, x + width]
|
|
54
54
|
})
|
|
55
55
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react"
|
|
2
2
|
import { useComputed, useStore } from "react-bolt"
|
|
3
|
-
import {
|
|
4
|
-
import { selectionDOMRect } from "../ui/selection"
|
|
3
|
+
import { boxBounds, type Point, type Size } from "../model/geometry/math"
|
|
5
4
|
import { useEditor } from "./editor"
|
|
6
5
|
|
|
7
6
|
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
|
@@ -25,6 +24,7 @@ function useObserver(onChange: () => void) {
|
|
|
25
24
|
.filter((ref) => ref instanceof HTMLElement)
|
|
26
25
|
.toArray(),
|
|
27
26
|
})
|
|
27
|
+
|
|
28
28
|
const editorRef = useStore(editor, "ref")
|
|
29
29
|
const zoom = useStore(editor, "zoom")
|
|
30
30
|
const pages = useStore(editor, "pages")
|
|
@@ -70,26 +70,33 @@ export function useSelectionFrame<E extends HTMLElement>(props?: {
|
|
|
70
70
|
const selection = useStore(editor, "selection")
|
|
71
71
|
const zoom = useStore(editor, "zoom")
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
function setRefStyle({ x, y, width, height }: Point & Size, rotation?: number) {
|
|
74
74
|
const frame = ref.current
|
|
75
75
|
const stage = editor.ref?.getBoundingClientRect()
|
|
76
|
-
|
|
76
|
+
const page = editor.selectionPage?.ref?.getBoundingClientRect()
|
|
77
|
+
if (!frame || !stage || !page) return
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
console.log(frame, stage, page)
|
|
80
|
+
|
|
81
|
+
const tx = page.x - stage.x + x * zoom
|
|
82
|
+
const ty = page.y - stage.y + y * zoom
|
|
83
|
+
|
|
84
|
+
frame.style.height = `${height * zoom}px`
|
|
85
|
+
frame.style.width = `${width * zoom}px`
|
|
86
|
+
|
|
87
|
+
if (typeof rotation === "number") {
|
|
86
88
|
frame.style.transform = `translate(${tx}px, ${ty}px) rotate(${rotation}deg)`
|
|
87
89
|
} else {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
frame.style.transform = `translate(${tx}px, ${ty}px)`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
useObserver(() => {
|
|
95
|
+
if (props?.accountForSingleSelection && selection.size === 1) {
|
|
96
|
+
const node = selection.values().next().value!
|
|
97
|
+
setRefStyle(node, node.rotation)
|
|
98
|
+
} else {
|
|
99
|
+
setRefStyle(boxBounds(...selection.values().map((node) => node.toBox())))
|
|
93
100
|
}
|
|
94
101
|
})
|
|
95
102
|
|
|
@@ -95,13 +95,13 @@ export function rect(...args: Array<(Point & Size) | Point>): Rect {
|
|
|
95
95
|
/**
|
|
96
96
|
* Create a rotatable box for a rectangle
|
|
97
97
|
*/
|
|
98
|
-
export function box(input
|
|
98
|
+
export function box(input: Size & Point = rect(), rotation = deg(0)): Box {
|
|
99
99
|
return {
|
|
100
|
-
center: rectCenter(input
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
width: input
|
|
104
|
-
height: input
|
|
100
|
+
center: rectCenter(input),
|
|
101
|
+
pivot: rectCenter(input),
|
|
102
|
+
rotation,
|
|
103
|
+
width: input.width,
|
|
104
|
+
height: input.height,
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
2
|
import {
|
|
3
|
+
deg,
|
|
3
4
|
floatNorm,
|
|
4
5
|
pointAdd,
|
|
5
6
|
rect,
|
|
6
7
|
rectCenter,
|
|
7
8
|
rotatePoint,
|
|
9
|
+
type Box,
|
|
8
10
|
type Line,
|
|
9
11
|
type Point,
|
|
10
12
|
} from "../geometry/math"
|
|
@@ -77,4 +79,15 @@ export class LineNode extends Node {
|
|
|
77
79
|
}
|
|
78
80
|
})
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
override toBox(): Box {
|
|
84
|
+
const r = rect(...this.vertices)
|
|
85
|
+
return {
|
|
86
|
+
center: rectCenter(r),
|
|
87
|
+
pivot: rectCenter(r),
|
|
88
|
+
rotation: deg(0),
|
|
89
|
+
width: r.width,
|
|
90
|
+
height: r.height,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
}
|
package/lib/model/node.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
|
-
import { deg, type Deg } from "./geometry/math"
|
|
2
|
+
import { box, deg, type Box, type Deg } from "./geometry/math"
|
|
3
3
|
import type { Page } from "./page"
|
|
4
4
|
|
|
5
5
|
export type Schema = Map<string, typeof Node>
|
|
@@ -89,4 +89,8 @@ export abstract class Node {
|
|
|
89
89
|
...(rotation && { rotation }),
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
toBox(): Box {
|
|
94
|
+
return box(this, this.rotation)
|
|
95
|
+
}
|
|
92
96
|
}
|
|
@@ -17,8 +17,9 @@ export function ImageContent(props: { node: ImageNode; fallback?: React.ReactNod
|
|
|
17
17
|
return url ? (
|
|
18
18
|
<img
|
|
19
19
|
src={url}
|
|
20
|
+
draggable={false}
|
|
20
21
|
className={clsx(
|
|
21
|
-
"size-full",
|
|
22
|
+
"size-full select-none",
|
|
22
23
|
fit === "cover" && "object-cover",
|
|
23
24
|
fit === "contain" && "object-contain",
|
|
24
25
|
fit === "fill" && "object-fill",
|
package/lib/ui/selection.ts
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import { box, boxBounds,
|
|
1
|
+
import { box, boxBounds, type Box } from "../model/geometry/math"
|
|
2
2
|
import type { Node } from "../model/node"
|
|
3
3
|
|
|
4
|
-
export function selectionDOMRect(selection: Set<Node>): Rect {
|
|
5
|
-
const rects = selection
|
|
6
|
-
.values()
|
|
7
|
-
.map((node) => node.ref)
|
|
8
|
-
.filter((dom) => dom instanceof HTMLElement)
|
|
9
|
-
.map((dom) => dom.getBoundingClientRect())
|
|
10
|
-
|
|
11
|
-
return rect(...rects)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
4
|
export function selectionBox(selection: Iterable<Node>): Box {
|
|
15
5
|
const arr = Array.from(selection)
|
|
16
6
|
|
|
17
7
|
if (arr.length === 1) {
|
|
18
|
-
return
|
|
8
|
+
return arr[0].toBox()
|
|
19
9
|
}
|
|
20
10
|
|
|
21
|
-
return box(boxBounds(...arr.map(
|
|
11
|
+
return box(boxBounds(...arr.map((n) => n.toBox())))
|
|
22
12
|
}
|