@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.
@@ -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, K extends keyof N>(
15
+ export function useNodeField<N extends Node, T>(
17
16
  nodes: Iterable<N>,
18
- key: K,
19
- fallback: N[K],
17
+ fallback: T,
18
+ getter: (node: N) => T,
20
19
  ) {
21
- return useComputed<N[K]>({
20
+ return useComputed<T>({
22
21
  equals: isEqual,
23
22
  fn: () => {
24
- const values = Array.from(nodes).map((node) => node[key])
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 { history } = useEditor()
40
+ const editor = useEditor()
42
41
  const initial = useRef<Map<N, N[K]> | null>(null)
43
42
 
44
43
  return {
45
- value: useNodeField(nodes, key, fallback),
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
- node[key] = typeof set === "function" ? (set as (node: N) => N[K])(node) : set
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
- const prev: HistoryAction[] = []
63
- const next: HistoryAction[] = []
64
-
65
- for (const node of nodes) {
66
- const nextValue =
67
- typeof end === "function" ? (end as (node: N) => N[K])(node) : end
68
-
69
- prev.push(["set-node-props", [node.id, { [key]: init.get(node) }]])
70
- next.push(["set-node-props", [node.id, { [key]: nextValue }]])
71
- node[key] = nextValue
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 { history } = useEditor()
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
- const prev: HistoryAction[] = []
98
- const next: HistoryAction[] = []
99
-
100
- for (const node of nodes) {
101
- const nextValues = typeof set === "function" ? set(node) : set
102
- const keys = Object.keys(nextValues) as WritableKeys<N>[]
103
- const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
104
- prev.push(["set-node-props", [node.id, prevValues]])
105
- next.push(["set-node-props", [node.id, nextValues]])
106
- Object.assign(node, nextValues)
107
- }
108
-
109
- if (next.length > 1) {
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 })
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, "rotation", deg(0))
7
- const width = useNodeField(nodes, "width", NaN)
8
- const height = useNodeField(nodes, "height", NaN)
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(box(node), cursor)
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: x, baseY: y, basePoints: points } = state
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
- return {
76
- node,
77
- basePoints: node.points,
78
- baseX: node.x,
79
- baseY: node.y,
80
- relativeVertices: node.vertices.map((p) => relativePoint(p, baseSelectionRect)),
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: box(node),
80
+ baseSize: node.toBox(),
82
81
  relative: relativeSize(node),
83
82
  }))
84
83
  },
@@ -64,7 +64,7 @@ export function useMultiTextNodeResize(
64
64
  state.current = nodes.map((node) => ({
65
65
  node,
66
66
  baseFontSize: node.size,
67
- baseBoxSize: box(node),
67
+ baseBoxSize: node.toBox(),
68
68
  relative: relativeSize(node),
69
69
  }))
70
70
  },
@@ -49,7 +49,7 @@ export function useSingleRegularNodeResize(direction: Edge | Corner) {
49
49
  if (!node || node.locked) return false
50
50
 
51
51
  state.current = {
52
- base: box(node),
52
+ base: node.toBox(),
53
53
  anchorPoint: cursorPosition(event, node.page),
54
54
  }
55
55
  },
@@ -80,7 +80,7 @@ export function useSingleTextNodeResize(direction: Edge | Corner) {
80
80
  state.current = {
81
81
  anchorPoint: cursorPosition(event, node.page),
82
82
  baseFontSize: node.size,
83
- baseBoxSize: box(node),
83
+ baseBoxSize: node.toBox(),
84
84
  }
85
85
  }
86
86
  return enable
@@ -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
- accessibleLine,
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(box(node), cursor))
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, box(node))
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, box, boxBounds, rect } from "../../model/geometry/math"
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(box(n))
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(box(n))
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 { pointSubtract } from "../model/geometry/math"
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
- useObserver(() => {
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
- if (!frame || !stage) return
76
+ const page = editor.selectionPage?.ref?.getBoundingClientRect()
77
+ if (!frame || !stage || !page) return
77
78
 
78
- if (props?.accountForSingleSelection && selection.size === 1) {
79
- const [firstNode] = selection
80
- const { x, y, width, height, rotation } = firstNode
81
- const relative = firstNode.page.ref!.getBoundingClientRect()
82
- const tx = relative.x - stage.x + x * zoom
83
- const ty = relative.y - stage.y + y * zoom
84
- frame.style.height = `${height * zoom}px`
85
- frame.style.width = `${width * zoom}px`
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
- const dom = selectionDOMRect(selection)
89
- const t = pointSubtract(dom, stage)
90
- frame.style.height = `${dom.height}px`
91
- frame.style.width = `${dom.width}px`
92
- frame.style.transform = `translate(${t.x}px, ${t.y}px)`
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?: Size & Point & { rotation?: Deg }): Box {
98
+ export function box(input: Size & Point = rect(), rotation = deg(0)): Box {
99
99
  return {
100
- center: rectCenter(input ?? rect()),
101
- rotation: input?.rotation ?? deg(0),
102
- pivot: rectCenter(input ?? rect()),
103
- width: input?.width ?? 0,
104
- height: input?.height ?? 0,
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",
@@ -1,22 +1,12 @@
1
- import { box, boxBounds, rect, type Box, type Rect } from "../model/geometry/math"
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 box(arr[0])
8
+ return arr[0].toBox()
19
9
  }
20
10
 
21
- return box(boxBounds(...arr.map(box)))
11
+ return box(boxBounds(...arr.map((n) => n.toBox())))
22
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazlon-platform/html-editor",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "lib"