@lazlon-platform/html-editor 0.5.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 (53) hide show
  1. package/lib/hooks/actions.ts +51 -24
  2. package/lib/hooks/batch.ts +17 -7
  3. package/lib/hooks/index.ts +1 -0
  4. package/lib/hooks/pointer/movePoint.ts +75 -0
  5. package/lib/hooks/pointer/moveable.ts +22 -8
  6. package/lib/hooks/pointer/pointer.ts +2 -3
  7. package/lib/hooks/pointer/resize.ts +2 -2
  8. package/lib/hooks/pointer/rotation.ts +74 -39
  9. package/lib/hooks/pointer/selector.ts +16 -2
  10. package/lib/hooks/pointer/snap.ts +3 -3
  11. package/lib/hooks/textMarks.ts +1 -3
  12. package/lib/lib/googleFonts.ts +1 -5
  13. package/lib/model/editor.ts +13 -9
  14. package/lib/model/geometry/math.ts +139 -0
  15. package/lib/model/history.ts +10 -13
  16. package/lib/model/index.ts +7 -10
  17. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  18. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  19. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  20. package/lib/model/node/{image.ts → imageNode.ts} +5 -11
  21. package/lib/model/node/lineNode.ts +59 -0
  22. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  23. package/lib/model/node/shapeNode/shape.ts +96 -0
  24. package/lib/model/node/{text.ts → textNode.ts} +6 -12
  25. package/lib/model/node.ts +9 -23
  26. package/lib/model/page.ts +1 -2
  27. package/lib/model/traversal.ts +1 -1
  28. package/lib/ui/extractor.ts +3 -3
  29. package/lib/ui/index.ts +2 -4
  30. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
  31. package/lib/ui/node/GroupContent.tsx +1 -1
  32. package/lib/ui/node/ImageContent.tsx +1 -1
  33. package/lib/ui/node/LineContent.tsx +32 -0
  34. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  35. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  36. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  37. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  38. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  39. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  40. package/lib/ui/node/TextContent.tsx +1 -1
  41. package/package.json +1 -1
  42. package/lib/model/node/shape/arrow.ts +0 -50
  43. package/lib/model/node/shape/ellipse.ts +0 -26
  44. package/lib/model/node/shape/polygon.ts +0 -130
  45. package/lib/model/node/shape/star.ts +0 -91
  46. package/lib/ui/node/ArrowContent.tsx +0 -60
  47. package/lib/ui/node/EllipseContent.tsx +0 -49
  48. package/lib/ui/node/PolygonContent.tsx +0 -81
  49. package/lib/ui/node/StarContent.tsx +0 -60
  50. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  51. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  52. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  53. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,14 +1,14 @@
1
1
  import { computed, createStore, state } from "react-bolt"
2
2
  import type { GoogleWebfont } from "../lib/googleFonts"
3
3
  import { HistoryStore, type HistoryEntry } from "./history"
4
- import type { Node, NodeProps, SerializedNode } from "./node"
4
+ import type { Node, SerializedNode } from "./node"
5
5
  import { Page, type SerializedPage } from "./page"
6
6
 
7
7
  const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
8
8
 
9
- export interface NodeConstructor<Props extends NodeProps = NodeProps> {
10
- prototype: Node
11
- new (ctx: Editor, page: Page, props: Props): Node
9
+ export interface NodeConstructor<N extends Node = Node> {
10
+ prototype: N
11
+ new (page: Page, props: N["props"]): Node
12
12
  }
13
13
 
14
14
  export interface SerializedEditor {
@@ -144,13 +144,17 @@ export class Editor {
144
144
  )
145
145
  }
146
146
 
147
- deserializeNode<T extends NodeProps>(
148
- page: Page,
149
- { name, props }: SerializedNode<string, T>,
150
- ): Node {
147
+ deserializeNode<T extends Node>(page: Page, { name, props }: SerializedNode<T>): T {
151
148
  const NodeClass = this.#schema.get(name)
152
149
  if (!NodeClass) throw Error(`cannot deserialize unknown Node: ${name}`)
153
- return new NodeClass(this, page, props)
150
+ return new NodeClass(page, props) as T
151
+ }
152
+
153
+ serializeNode<T extends Node>(node: T): SerializedNode<T> {
154
+ return {
155
+ name: node.name,
156
+ props: node.props,
157
+ }
154
158
  }
155
159
 
156
160
  serialize(): SerializedEditor {
@@ -20,6 +20,11 @@ export interface Rect extends Point, Size {
20
20
  readonly right: number
21
21
  }
22
22
 
23
+ export interface Line extends Point {
24
+ readonly points: Point[]
25
+ readonly strokeWidth: number
26
+ }
27
+
23
28
  /**
24
29
  * A rotated rectangle around a pivot point.
25
30
  * Pivot point is relative to the center point.
@@ -349,6 +354,140 @@ export function boxContainsPoint(target: Box, point: Point): boolean {
349
354
  )
350
355
  }
351
356
 
357
+ function pointSegmentDistance(target: Point, a: Point, b: Point): number {
358
+ const segment = pointSubtract(b, a)
359
+ const lengthSq = vectorDotProd(segment, segment)
360
+
361
+ if (lengthSq === 0) {
362
+ return pointDist(target, a)
363
+ }
364
+
365
+ const fromStart = pointSubtract(target, a)
366
+ const projection = clamp(vectorDotProd(fromStart, segment) / lengthSq, 0, 1)
367
+ const closest = pointAdd(a, pointMultiply(segment, projection))
368
+
369
+ return pointDist(target, closest)
370
+ }
371
+
372
+ function segmentCross(a: Point, b: Point, c: Point): number {
373
+ return floatNorm((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
374
+ }
375
+
376
+ function pointOnSegment(target: Point, a: Point, b: Point): boolean {
377
+ const x = floatNorm(target.x)
378
+ const y = floatNorm(target.y)
379
+
380
+ return (
381
+ segmentCross(a, b, target) === 0 &&
382
+ x >= floatNorm(Math.min(a.x, b.x)) &&
383
+ x <= floatNorm(Math.max(a.x, b.x)) &&
384
+ y >= floatNorm(Math.min(a.y, b.y)) &&
385
+ y <= floatNorm(Math.max(a.y, b.y))
386
+ )
387
+ }
388
+
389
+ function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
390
+ const abc = segmentCross(a, b, c)
391
+ const abd = segmentCross(a, b, d)
392
+ const cda = segmentCross(c, d, a)
393
+ const cdb = segmentCross(c, d, b)
394
+
395
+ return (
396
+ (abc === 0 && pointOnSegment(c, a, b)) ||
397
+ (abd === 0 && pointOnSegment(d, a, b)) ||
398
+ (cda === 0 && pointOnSegment(a, c, d)) ||
399
+ (cdb === 0 && pointOnSegment(b, c, d)) ||
400
+ (abc * abd < 0 && cda * cdb < 0)
401
+ )
402
+ }
403
+
404
+ function segmentDistance(a: Point, b: Point, c: Point, d: Point): number {
405
+ if (segmentsIntersect(a, b, c, d)) return 0
406
+
407
+ return Math.min(
408
+ pointSegmentDistance(a, c, d),
409
+ pointSegmentDistance(b, c, d),
410
+ pointSegmentDistance(c, a, b),
411
+ pointSegmentDistance(d, a, b),
412
+ )
413
+ }
414
+
415
+ function linePoints(line: Line): Point[] {
416
+ return line.points.map((p) => pointAdd(line, p))
417
+ }
418
+
419
+ /**
420
+ * @returns Whether point is contained in the stroked polyline.
421
+ */
422
+ export function lineContainsPoint(line: Line, point: Point): boolean {
423
+ const points = linePoints(line)
424
+ const radius = line.strokeWidth / 2
425
+
426
+ if (points.length === 1) {
427
+ return pointDist(point, points[0]) <= radius
428
+ }
429
+
430
+ for (let i = 0; i < points.length - 1; i++) {
431
+ const a = points[i]
432
+ const b = points[i + 1]
433
+
434
+ if (pointSegmentDistance(point, a, b) <= radius) return true
435
+ }
436
+
437
+ return false
438
+ }
439
+
440
+ /**
441
+ * @returns Whether the stroked polyline intersects the box.
442
+ */
443
+ export function lineIntersectsBox(line: Line, box: Box): boolean {
444
+ const points = linePoints(line)
445
+
446
+ if (points.length === 0) return false
447
+
448
+ for (const point of points) {
449
+ if (boxContainsPoint(box, point)) return true
450
+ }
451
+
452
+ const radius = line.strokeWidth / 2
453
+ const corners = boxCorners(box)
454
+ const edges = corners.map(
455
+ (corner, i) => [corner, corners[(i + 1) % corners.length]] as const,
456
+ )
457
+
458
+ for (const corner of corners) {
459
+ if (lineContainsPoint(line, corner)) return true
460
+ }
461
+
462
+ if (points.length === 1) {
463
+ return edges.some(([a, b]) => pointSegmentDistance(points[0], a, b) <= radius)
464
+ }
465
+
466
+ for (let i = 0; i < points.length - 1; i++) {
467
+ const a = points[i]
468
+ const b = points[i + 1]
469
+
470
+ for (const [c, d] of edges) {
471
+ if (segmentDistance(a, b, c, d) <= radius) return true
472
+ }
473
+ }
474
+
475
+ return false
476
+ }
477
+
478
+ /**
479
+ * @returns Line with increased strokeWidth for accessibility reasons.
480
+ */
481
+ export function accessibleLine(line: Line): Line {
482
+ const { x, y, strokeWidth, points } = line
483
+ return {
484
+ x,
485
+ y,
486
+ points,
487
+ strokeWidth: Math.max(18, strokeWidth),
488
+ }
489
+ }
490
+
352
491
  /**
353
492
  * @returns Four corners of a box.
354
493
  * ⠀⠀ ●
@@ -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
  }