@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.
Files changed (59) hide show
  1. package/lib/hooks/actions.ts +54 -26
  2. package/lib/hooks/batch.ts +17 -7
  3. package/lib/hooks/index.ts +1 -0
  4. package/lib/hooks/node.ts +14 -6
  5. package/lib/hooks/pointer/movePoint.ts +75 -0
  6. package/lib/hooks/pointer/moveable.ts +92 -57
  7. package/lib/hooks/pointer/pointer.ts +21 -11
  8. package/lib/hooks/pointer/resize.ts +176 -210
  9. package/lib/hooks/pointer/rotation.ts +89 -68
  10. package/lib/hooks/pointer/selectionFrame.ts +8 -11
  11. package/lib/hooks/pointer/selector.ts +62 -40
  12. package/lib/hooks/pointer/snap.ts +23 -23
  13. package/lib/hooks/textMarks.ts +1 -3
  14. package/lib/lib/googleFonts.ts +1 -5
  15. package/lib/model/editor.ts +13 -9
  16. package/lib/model/geometry/math.ts +623 -0
  17. package/lib/model/geometry/svg.ts +55 -0
  18. package/lib/model/history.ts +10 -13
  19. package/lib/model/index.ts +7 -10
  20. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  21. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  22. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  23. package/lib/model/node/{image.ts → imageNode.ts} +5 -11
  24. package/lib/model/node/lineNode.ts +59 -0
  25. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  26. package/lib/model/node/shapeNode/shape.ts +96 -0
  27. package/lib/model/node/{text.ts → textNode.ts} +19 -21
  28. package/lib/model/node.ts +11 -29
  29. package/lib/model/page.ts +4 -3
  30. package/lib/model/traversal.ts +1 -1
  31. package/lib/ui/extractor.ts +3 -3
  32. package/lib/ui/index.ts +2 -4
  33. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
  34. package/lib/ui/node/GroupContent.tsx +1 -1
  35. package/lib/ui/node/ImageContent.tsx +1 -1
  36. package/lib/ui/node/LineContent.tsx +32 -0
  37. package/lib/ui/node/NodeView.tsx +1 -13
  38. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  39. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  40. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  41. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  42. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  43. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  44. package/lib/ui/node/TextContent.tsx +1 -1
  45. package/lib/ui/selection.ts +9 -26
  46. package/package.json +34 -34
  47. package/lib/model/geometry.ts +0 -247
  48. package/lib/model/node/shape/arrow.ts +0 -50
  49. package/lib/model/node/shape/ellipse.ts +0 -26
  50. package/lib/model/node/shape/polygon.ts +0 -108
  51. package/lib/model/node/shape/star.ts +0 -63
  52. package/lib/ui/node/ArrowContent.tsx +0 -60
  53. package/lib/ui/node/EllipseContent.tsx +0 -49
  54. package/lib/ui/node/PolygonContent.tsx +0 -81
  55. package/lib/ui/node/StarContent.tsx +0 -60
  56. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  57. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  58. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  59. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,16 +1,13 @@
1
1
  import { computed, state } from "react-bolt"
2
2
  import type { Editor } from "./editor"
3
- import type { Node, NodeProps, SerializedNode } from "./node"
3
+ import type { Node, SerializedNode } from "./node"
4
4
  import type { PageProps } from "./page"
5
5
 
6
- export type HistoryAction<Props extends NodeProps = NodeProps> =
7
- | [action: "batch", payload: Array<HistoryAction>]
8
- | [
9
- action: "add-node",
10
- payload: [pageId: string, nodes: Array<SerializedNode<string, Props>>],
11
- ]
6
+ export type HistoryAction<N extends Node = Node> =
7
+ | [action: "batch", payload: Array<HistoryAction<N>>]
8
+ | [action: "add-node", payload: [pageId: string, nodes: Array<SerializedNode<N>>]]
12
9
  | [action: "delete-node", payload: [pageId: string, nodes: Array<string>]]
13
- | [action: "set-node-props", payload: [nodeId: string, props: Partial<Props>]]
10
+ | [action: "set-node-props", payload: [nodeId: string, props: Partial<N["props"]>]]
14
11
  | [action: "set-page-props", payload: [pageId: string, props: Partial<PageProps>]]
15
12
  | [action: "stack-order", payload: [pageId: string, order: Array<string>]]
16
13
 
@@ -19,9 +16,9 @@ function assert<T>(instance?: T): T {
19
16
  return instance!
20
17
  }
21
18
 
22
- export type HistoryEntry<Props extends NodeProps = NodeProps> = {
23
- undo: HistoryAction<Props>
24
- redo: HistoryAction<Props>
19
+ export type HistoryEntry<N extends Node = Node> = {
20
+ undo: HistoryAction<N>
21
+ redo: HistoryAction<N>
25
22
  }
26
23
 
27
24
  export class HistoryStore {
@@ -39,7 +36,7 @@ export class HistoryStore {
39
36
  return this._redoHistory
40
37
  }
41
38
 
42
- readonly push = <Props extends NodeProps = NodeProps>(entry: HistoryEntry<Props>) => {
39
+ readonly push = <N extends Node>(entry: HistoryEntry<N>) => {
43
40
  if (this._redoHistory.length > 0) {
44
41
  this._redoHistory = []
45
42
  }
@@ -73,7 +70,7 @@ export class HistoryStore {
73
70
  this.emitChange()
74
71
  }
75
72
 
76
- private execute([action, payload]: HistoryAction) {
73
+ private execute<N extends Node>([action, payload]: HistoryAction<N>) {
77
74
  switch (action) {
78
75
  case "batch": {
79
76
  payload.map((action) => this.execute(action))
@@ -1,15 +1,12 @@
1
1
  export { Editor, type NodeConstructor, type SerializedEditor } from "./editor"
2
2
  export { type HistoryAction, type HistoryEntry } from "./history"
3
3
  export { Node, type NodeProps, type SerializedNode } from "./node"
4
- export { EditableNode, type EditableNodeProps } from "./node/editable"
5
- export { FormattableNode, type FormattableNodeProps } from "./node/formattable"
6
- export { GroupNode, type GroupNodeProps } from "./node/group"
7
- export { ImageNode, type ImageNodeProps } from "./node/image"
8
- export { ArrowNode, type ArrowNodeProps } from "./node/shape/arrow"
9
- export { EllipseNode, type EllipseNodeProps } from "./node/shape/ellipse"
10
- export { PolygonNode, type PolygonNodeProps } from "./node/shape/polygon"
11
- export { ShapeNode, type ShapeNodeProps } from "./node/shape/shape"
12
- export { StarNode, type StarNodeProps } from "./node/shape/star"
13
- export { TextNode, type TextNodeProps } from "./node/text"
4
+ export { EditableNode, type EditableNodeProps } from "./node/editableNode"
5
+ export { FormattableNode, type FormattableNodeProps } from "./node/formattableNode"
6
+ export { GroupNode, type GroupNodeProps } from "./node/groupNode"
7
+ export { ImageNode, type ImageNodeProps } from "./node/imageNode"
8
+ export { LineNode, type LineNodeProps } from "./node/lineNode"
9
+ export { ShapeNode, type ShapeNodeProps } from "./node/shapeNode"
10
+ export { TextNode, type TextNodeProps } from "./node/textNode"
14
11
  export { Page, type PageProps, type SerializedPage } from "./page"
15
12
  export { flattenNodes } from "./traversal"
@@ -1,7 +1,6 @@
1
1
  import { Editor as Tiptap, type JSONContent } from "@tiptap/react"
2
2
  import { computed, state } from "react-bolt"
3
- import type { Editor } from "../../editor"
4
- import { Node, type NodeProps, type SerializedNode } from "../../node"
3
+ import { Node, type NodeProps } from "../../node"
5
4
  import type { Page } from "../../page"
6
5
  import { TiptapExtensions } from "./tiptapExtensions"
7
6
 
@@ -69,35 +68,15 @@ export abstract class EditableNode extends Node {
69
68
  EditableNode.lastFocused = null
70
69
  }
71
70
 
72
- constructor(
73
- editor: Editor,
74
- page: Page,
75
- { content, lineHeight, ...props }: EditableNodeProps,
76
- ) {
77
- super(editor, page, props)
71
+ constructor(page: Page, { content, lineHeight, ...props }: EditableNodeProps) {
72
+ super(page, props)
78
73
  this.lineHeight = lineHeight ?? 1.2
79
74
  this.content = content ?? { type: "doc", content: [] }
80
75
  }
81
76
 
82
- blockMove(event: React.PointerEvent): boolean {
83
- const rect = this.tiptap.view.dom.getBoundingClientRect()
84
- return (
85
- super.blockMove(event) ||
86
- (this.tiptap.isFocused &&
87
- event.clientX >= rect.left &&
88
- event.clientX <= rect.right &&
89
- event.clientY >= rect.top &&
90
- event.clientY <= rect.bottom)
91
- )
92
- }
93
-
94
- props(): EditableNodeProps {
77
+ get props(): EditableNodeProps {
95
78
  const { content, lineHeight } = this
96
- return { ...super.props(), content, lineHeight }
97
- }
98
-
99
- serialize(): SerializedNode<string, EditableNodeProps> {
100
- return super.serialize()
79
+ return { ...super.props, content, lineHeight }
101
80
  }
102
81
 
103
82
  /**
@@ -107,14 +86,19 @@ export abstract class EditableNode extends Node {
107
86
  */
108
87
  private static lastFocused: EditableNode | null = null
109
88
 
110
- static getFocused(node: EditableNode[]) {
111
- const focused = node.find((e) => e.tiptap.isFocused)
89
+ /**
90
+ * Get the current focused node from {@link Editor.selection}.
91
+ */
92
+ static getFocused(selection: Iterable<EditableNode>) {
93
+ const nodes = Array.from(selection)
94
+
95
+ const focused = nodes.find((e) => e.tiptap.isFocused)
112
96
 
113
97
  if (focused) {
114
98
  return (this.lastFocused = focused)
115
99
  }
116
100
 
117
- if (this.lastFocused && node.includes(this.lastFocused)) {
101
+ if (this.lastFocused && nodes.includes(this.lastFocused)) {
118
102
  return this.lastFocused
119
103
  }
120
104
 
@@ -1,7 +1,6 @@
1
1
  import { state } from "react-bolt"
2
- import type { Editor } from "../editor"
3
- import { Node, type NodeProps, type SerializedNode } from "../node"
4
- import type { Page } from "../page"
2
+ import { Node, type NodeProps } from "../../node"
3
+ import type { Page } from "../../page"
5
4
 
6
5
  export type FormattableNodeProps = NodeProps &
7
6
  Partial<
@@ -37,7 +36,6 @@ export abstract class FormattableNode extends Node {
37
36
  @state accessor casing: "capitalize" | "uppercase" | "lowercase" | "normal"
38
37
 
39
38
  constructor(
40
- editor: Editor,
41
39
  page: Page,
42
40
  {
43
41
  bold,
@@ -55,7 +53,7 @@ export abstract class FormattableNode extends Node {
55
53
  ...props
56
54
  }: FormattableNodeProps,
57
55
  ) {
58
- super(editor, page, props)
56
+ super(page, props)
59
57
  this.bold = bold ?? false
60
58
  this.italic = italic ?? false
61
59
  this.underline = underline ?? false
@@ -70,7 +68,7 @@ export abstract class FormattableNode extends Node {
70
68
  this.casing = casing ?? "normal"
71
69
  }
72
70
 
73
- props(): FormattableNodeProps {
71
+ get props(): FormattableNodeProps {
74
72
  const {
75
73
  bold,
76
74
  italic,
@@ -86,7 +84,7 @@ export abstract class FormattableNode extends Node {
86
84
  casing,
87
85
  } = this
88
86
  return {
89
- ...super.props(),
87
+ ...super.props,
90
88
  size,
91
89
  spacing,
92
90
  lineHeight,
@@ -101,8 +99,4 @@ export abstract class FormattableNode extends Node {
101
99
  ...(family && { family }),
102
100
  }
103
101
  }
104
-
105
- serialize(): SerializedNode<string, FormattableNodeProps> {
106
- return super.serialize()
107
- }
108
102
  }
@@ -1,5 +1,4 @@
1
1
  import { state } from "react-bolt"
2
- import type { Editor } from "../editor"
3
2
  import { Node, type NodeProps, type SerializedNode } from "../node"
4
3
  import type { Page } from "../page"
5
4
 
@@ -8,15 +7,15 @@ export interface GroupNodeProps extends NodeProps {
8
7
  }
9
8
 
10
9
  export class GroupNode extends Node {
11
- get name() {
10
+ get name(): "group" {
12
11
  return "group"
13
12
  }
14
13
 
15
14
  @state accessor nodes = new Set<Node>()
16
15
 
17
- constructor(editor: Editor, page: Page, { nodes = [], ...props }: GroupNodeProps) {
18
- super(editor, page, props)
19
- this.nodes = new Set(nodes.map((node) => editor.deserializeNode(page, node)))
16
+ constructor(page: Page, { nodes = [], ...props }: GroupNodeProps) {
17
+ super(page, props)
18
+ this.nodes = new Set(nodes.map((node) => page.editor.deserializeNode(page, node)))
20
19
  }
21
20
 
22
21
  get width() {
@@ -63,17 +62,14 @@ export class GroupNode extends Node {
63
62
  }
64
63
  }
65
64
 
66
- props(): GroupNodeProps {
65
+ get props(): GroupNodeProps {
66
+ const { nodes, page } = this
67
67
  return {
68
- ...super.props(),
69
- nodes: this.nodes
68
+ ...super.props,
69
+ nodes: nodes
70
70
  .values()
71
- .map((node) => node.serialize())
71
+ .map((node) => page.editor.serializeNode(node))
72
72
  .toArray(),
73
73
  }
74
74
  }
75
-
76
- serialize(): SerializedNode<this["name"], GroupNodeProps> {
77
- return super.serialize()
78
- }
79
75
  }
@@ -1,13 +1,12 @@
1
1
  import { state } from "react-bolt"
2
- import type { Editor } from "../editor"
3
- import { Node, type NodeProps, type SerializedNode } from "../node"
2
+ import { Node, type NodeProps } from "../node"
4
3
  import type { Page } from "../page"
5
4
 
6
5
  export type ImageNodeProps = NodeProps &
7
6
  Partial<Pick<ImageNode, "url" | "fit" | "roundness" | "borderWidth" | "borderColor">>
8
7
 
9
8
  export class ImageNode extends Node {
10
- get name() {
9
+ get name(): "image" {
11
10
  return "image"
12
11
  }
13
12
 
@@ -18,11 +17,10 @@ export class ImageNode extends Node {
18
17
  @state accessor borderColor: string
19
18
 
20
19
  constructor(
21
- editor: Editor,
22
20
  page: Page,
23
21
  { url, fit, roundness, borderWidth, borderColor, ...props }: ImageNodeProps,
24
22
  ) {
25
- super(editor, page, props)
23
+ super(page, props)
26
24
  this.url = url ?? null
27
25
  this.fit = "cover"
28
26
  this.roundness = roundness ?? 0
@@ -30,12 +28,8 @@ export class ImageNode extends Node {
30
28
  this.borderWidth = borderWidth ?? 0
31
29
  }
32
30
 
33
- props(): ImageNodeProps {
31
+ get props(): ImageNodeProps {
34
32
  const { url, fit, roundness, borderWidth, borderColor } = this
35
- return { ...super.props(), url, fit, roundness, borderWidth, borderColor }
36
- }
37
-
38
- serialize(): SerializedNode<this["name"], ImageNodeProps> {
39
- return super.serialize()
33
+ return { ...super.props, url, fit, roundness, borderWidth, borderColor }
40
34
  }
41
35
  }
@@ -0,0 +1,59 @@
1
+ import { computed, state } from "react-bolt"
2
+ import { point, pointAdd, rect, type Point } from "../geometry/math"
3
+ import { Node, type NodeProps } from "../node"
4
+ import type { Page } from "../page"
5
+
6
+ export type LineNodeProps = NodeProps &
7
+ Partial<Pick<LineNode, "points" | "strokeWidth" | "strokeColor">>
8
+
9
+ export class LineNode extends Node {
10
+ get name(): "line" {
11
+ return "line"
12
+ }
13
+
14
+ /** Relative to {@link x},{@link y} */
15
+ @state accessor points: Point[]
16
+ @state accessor strokeWidth: number
17
+ @state accessor strokeColor: string
18
+
19
+ @computed get height(): number {
20
+ return this.rect.height
21
+ }
22
+
23
+ set height(_) {
24
+ // no-op: size is calculated from x,y and points
25
+ }
26
+
27
+ @computed get width(): number {
28
+ return this.rect.width
29
+ }
30
+
31
+ set width(_) {
32
+ // no-op: size is calculated from x,y and points
33
+ }
34
+
35
+ @computed private get rect() {
36
+ const local = point({
37
+ x: super.x,
38
+ y: super.y,
39
+ })
40
+
41
+ return rect(...this.points.map((p) => pointAdd(p, local)))
42
+ }
43
+
44
+ constructor(page: Page, { points, strokeWidth, strokeColor, ...props }: LineNodeProps) {
45
+ super(page, props)
46
+ this.points = points ?? [
47
+ { x: -50, y: -50 },
48
+ { x: 100, y: 50 },
49
+ { x: 100, y: 0 },
50
+ ]
51
+ this.strokeWidth = strokeWidth ?? 1
52
+ this.strokeColor = strokeColor ?? "#000000"
53
+ }
54
+
55
+ get props(): LineNodeProps {
56
+ const { points, strokeWidth, strokeColor } = this
57
+ return { ...super.props, points, strokeWidth, strokeColor }
58
+ }
59
+ }
@@ -1,15 +1,23 @@
1
1
  import { state } from "react-bolt"
2
- import type { Editor } from "../../editor"
3
2
  import type { Page } from "../../page"
4
- import { EditableNode, type EditableNodeProps } from "../editable"
5
- import type { SerializedNode } from "../../node"
3
+ import { EditableNode, type EditableNodeProps } from "../editableNode"
4
+ import { newRectangleShape, type Shape } from "./shape"
5
+
6
+ export * from "./shape"
6
7
 
7
8
  export type ShapeNodeProps = EditableNodeProps &
8
9
  Partial<
9
- Pick<ShapeNode, "valign" | "halign" | "background" | "borderWidth" | "borderColor">
10
+ Pick<
11
+ ShapeNode,
12
+ "valign" | "halign" | "background" | "borderWidth" | "borderColor" | "shape"
13
+ >
10
14
  >
11
15
 
12
- export abstract class ShapeNode extends EditableNode {
16
+ export class ShapeNode extends EditableNode {
17
+ get name(): "shape" {
18
+ return "shape"
19
+ }
20
+
13
21
  @state accessor background: string
14
22
  @state accessor borderWidth: number
15
23
  @state accessor borderColor: string
@@ -17,32 +25,39 @@ export abstract class ShapeNode extends EditableNode {
17
25
  @state accessor halign: "left" | "center" | "right" | "justify"
18
26
  @state accessor valign: "top" | "center" | "bottom"
19
27
 
28
+ @state accessor shape: Shape
29
+
20
30
  constructor(
21
- editor: Editor,
22
31
  page: Page,
23
- { valign, halign, background, borderColor, borderWidth, ...props }: ShapeNodeProps,
32
+ {
33
+ valign,
34
+ halign,
35
+ background,
36
+ borderColor,
37
+ borderWidth,
38
+ shape,
39
+ ...props
40
+ }: ShapeNodeProps,
24
41
  ) {
25
- super(editor, page, props)
42
+ super(page, props)
26
43
  this.valign = valign ?? "center"
27
44
  this.halign = halign ?? "center"
28
45
  this.background = background ?? "#7f7f7f"
29
46
  this.borderColor = borderColor ?? "#000000"
30
47
  this.borderWidth = borderWidth ?? 0
48
+ this.shape = shape ?? newRectangleShape()
31
49
  }
32
50
 
33
- props(): ShapeNodeProps {
34
- const { valign, halign, background, borderColor, borderWidth } = this
51
+ get props(): ShapeNodeProps {
52
+ const { valign, halign, background, borderColor, borderWidth, shape } = this
35
53
  return {
36
- ...super.props(),
54
+ ...super.props,
37
55
  valign,
38
56
  halign,
39
57
  background,
58
+ shape,
40
59
  ...(borderWidth && { borderColor }),
41
60
  ...(borderWidth && { borderWidth }),
42
61
  }
43
62
  }
44
-
45
- serialize(): SerializedNode<this["name"], EditableNodeProps> {
46
- return super.serialize()
47
- }
48
63
  }
@@ -0,0 +1,96 @@
1
+ // shapes are not reactive stores because the batch dispatcher and history model
2
+ // is designed for non-nested stores
3
+
4
+ import { clamp } from "es-toolkit"
5
+
6
+ export interface StarShape {
7
+ name: "star"
8
+ props: {
9
+ corners: number
10
+ roundness: number
11
+ depth: number
12
+ }
13
+ }
14
+
15
+ export function newStarShape(props?: Partial<StarShape["props"]>): StarShape {
16
+ return {
17
+ name: "star",
18
+ props: {
19
+ corners: Math.max(props?.corners ?? 5, 3),
20
+ roundness: Math.max(props?.roundness ?? 0, 0),
21
+ depth: clamp(props?.depth ?? 0.4, 0, 1),
22
+ },
23
+ }
24
+ }
25
+
26
+ export interface ArrowShape {
27
+ name: "arrow"
28
+ props: {
29
+ roundness: number
30
+ }
31
+ }
32
+
33
+ export function newArrowShape(props?: Partial<ArrowShape["props"]>): ArrowShape {
34
+ return {
35
+ name: "arrow",
36
+ props: {
37
+ roundness: Math.max(props?.roundness ?? 0, 0),
38
+ },
39
+ }
40
+ }
41
+
42
+ export interface EllipseShape {
43
+ name: "ellipse"
44
+ props: { _?: never }
45
+ }
46
+
47
+ export function newEllipseShape(): EllipseShape {
48
+ return {
49
+ name: "ellipse",
50
+ props: {},
51
+ }
52
+ }
53
+
54
+ export interface PolygonShape {
55
+ name: "polygon"
56
+ props: {
57
+ roundness: number
58
+ sides: number
59
+ }
60
+ }
61
+
62
+ export function newPolygonShape(props?: Partial<PolygonShape["props"]>): PolygonShape {
63
+ return {
64
+ name: "polygon",
65
+ props: {
66
+ roundness: Math.max(props?.roundness ?? 0, 0),
67
+ sides: clamp(props?.sides ?? 5, 3, 20),
68
+ },
69
+ }
70
+ }
71
+
72
+ export interface RectangleShape {
73
+ name: "rectangle"
74
+ props: {
75
+ cornerTopLeft: number
76
+ cornerTopRight: number
77
+ cornerBottomLeft: number
78
+ cornerBottomRight: number
79
+ }
80
+ }
81
+
82
+ export function newRectangleShape(
83
+ props?: Partial<RectangleShape["props"]>,
84
+ ): RectangleShape {
85
+ return {
86
+ name: "rectangle",
87
+ props: {
88
+ cornerTopLeft: Math.max(props?.cornerTopLeft ?? 0, 0),
89
+ cornerTopRight: Math.max(props?.cornerTopRight ?? 0, 0),
90
+ cornerBottomLeft: Math.max(props?.cornerBottomLeft ?? 0, 0),
91
+ cornerBottomRight: Math.max(props?.cornerBottomRight ?? 0, 0),
92
+ },
93
+ }
94
+ }
95
+
96
+ export type Shape = RectangleShape | ArrowShape | PolygonShape | StarShape | EllipseShape
@@ -1,13 +1,11 @@
1
1
  import { computed, state } from "react-bolt"
2
- import type { Editor } from "../editor"
3
2
  import type { Page } from "../page"
4
- import { EditableNode, type EditableNodeProps } from "./editable"
5
- import type { SerializedNode } from "../node"
3
+ import { EditableNode, type EditableNodeProps } from "./editableNode"
6
4
 
7
5
  export type TextNodeProps = EditableNodeProps & Partial<Pick<TextNode, "halign" | "size">>
8
6
 
9
7
  export class TextNode extends EditableNode {
10
- get name() {
8
+ get name(): "text" {
11
9
  return "text"
12
10
  }
13
11
 
@@ -15,22 +13,20 @@ export class TextNode extends EditableNode {
15
13
  @state accessor size: number = 16
16
14
  @state accessor contentHeight: number = 24
17
15
 
16
+ private observer = new ResizeObserver(([entry]) => {
17
+ this.contentHeight = entry.target.clientHeight
18
+ })
19
+
18
20
  @computed get height(): number {
19
21
  return this.contentHeight
20
22
  }
21
23
 
22
- set height(n: number) {
23
- const target = Math.max(1, n)
24
-
25
- if (this.contentHeight > 0) {
26
- this.size = (this.size * target) / this.contentHeight
27
- }
28
-
29
- this.contentHeight = target
24
+ set height(_) {
25
+ // no-op: TextNode's height can be set using its font-size
30
26
  }
31
27
 
32
- constructor(editor: Editor, page: Page, { halign, size, ...props }: TextNodeProps) {
33
- super(editor, page, props)
28
+ constructor(page: Page, { halign, size, ...props }: TextNodeProps) {
29
+ super(page, props)
34
30
  this.halign = halign ?? "center"
35
31
  this.size = size ?? 16
36
32
 
@@ -42,19 +38,21 @@ export class TextNode extends EditableNode {
42
38
 
43
39
  set contentRef(ref: HTMLElement | null) {
44
40
  super.contentRef = ref
45
- if (ref) this.contentHeight = ref.clientHeight
41
+
42
+ if (ref) {
43
+ this.observer.observe(ref)
44
+ this.contentHeight = ref.clientHeight
45
+ } else {
46
+ this.observer.disconnect()
47
+ }
46
48
  }
47
49
 
48
50
  @computed get contentRef() {
49
51
  return super.contentRef
50
52
  }
51
53
 
52
- props(): TextNodeProps {
54
+ get props(): TextNodeProps {
53
55
  const { halign, size } = this
54
- return { ...super.props(), halign, size }
55
- }
56
-
57
- serialize(): SerializedNode<this["name"], TextNodeProps> {
58
- return super.serialize()
56
+ return { ...super.props, halign, size }
59
57
  }
60
58
  }
package/lib/model/node.ts CHANGED
@@ -1,16 +1,12 @@
1
1
  import { computed, state } from "react-bolt"
2
- import type { Editor } from "./editor"
3
- import { rotatedAABB } from "./geometry"
2
+ import { deg, type Deg } from "./geometry/math"
4
3
  import type { Page } from "./page"
5
4
 
6
5
  export type Schema = Map<string, typeof Node>
7
6
 
8
- export type SerializedNode<
9
- Name extends string = string,
10
- Props extends NodeProps = NodeProps,
11
- > = {
12
- name: Name
13
- props: Props
7
+ export type SerializedNode<N extends Node = Node> = {
8
+ name: N["name"]
9
+ props: N["props"]
14
10
  }
15
11
 
16
12
  export interface NodeProps
@@ -23,6 +19,7 @@ export interface NodeProps
23
19
 
24
20
  export abstract class Node {
25
21
  abstract get name(): string
22
+
26
23
  readonly id: string
27
24
  readonly page: Page
28
25
 
@@ -31,11 +28,11 @@ export abstract class Node {
31
28
  @state private accessor _width: number
32
29
  @state private accessor _height: number
33
30
 
34
- @state accessor rotation: number
31
+ @state accessor rotation: Deg
35
32
  @state accessor role: string | null
36
33
  @state accessor locked: boolean
37
- @state accessor x: number
38
- @state accessor y: number
34
+ @state accessor x: number = 0
35
+ @state accessor y: number = 0
39
36
  @state accessor css: string
40
37
 
41
38
  @computed get width() {
@@ -56,7 +53,7 @@ export abstract class Node {
56
53
  this._height = n
57
54
  }
58
55
 
59
- constructor(_: Editor, page: Page, props: NodeProps) {
56
+ constructor(page: Page, props: NodeProps) {
60
57
  this.page = page
61
58
  this.id = props.id
62
59
  this.x = props.x
@@ -65,15 +62,11 @@ export abstract class Node {
65
62
  this._height = props.height
66
63
  this.role = props.role?.[0] || null
67
64
  this.locked = props.locked ?? false
68
- this.rotation = props.rotation ?? 0
65
+ this.rotation = props.rotation ?? deg(0)
69
66
  this.css = props.css ?? ""
70
67
  }
71
68
 
72
- blockMove(_: React.PointerEvent): boolean {
73
- return !!this.locked
74
- }
75
-
76
- props(): NodeProps {
69
+ get props(): NodeProps {
77
70
  const { id, role, locked, rotation, css, x, y, width, height } = this
78
71
  return {
79
72
  id,
@@ -87,15 +80,4 @@ export abstract class Node {
87
80
  ...(rotation && { rotation }),
88
81
  }
89
82
  }
90
-
91
- serialize(): SerializedNode {
92
- return {
93
- name: this.name,
94
- props: this.props(),
95
- }
96
- }
97
-
98
- @computed get boundingBox() {
99
- return rotatedAABB(this, this.rotation)
100
- }
101
83
  }