@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.
- package/lib/hooks/actions.ts +54 -26
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/node.ts +14 -6
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +92 -57
- package/lib/hooks/pointer/pointer.ts +21 -11
- package/lib/hooks/pointer/resize.ts +176 -210
- package/lib/hooks/pointer/rotation.ts +89 -68
- package/lib/hooks/pointer/selectionFrame.ts +8 -11
- package/lib/hooks/pointer/selector.ts +62 -40
- package/lib/hooks/pointer/snap.ts +23 -23
- package/lib/hooks/textMarks.ts +1 -3
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +13 -9
- package/lib/model/geometry/math.ts +623 -0
- package/lib/model/geometry/svg.ts +55 -0
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +7 -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} +5 -11
- package/lib/model/node/lineNode.ts +59 -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} +19 -21
- package/lib/model/node.ts +11 -29
- package/lib/model/page.ts +4 -3
- 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} +4 -3
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +32 -0
- package/lib/ui/node/NodeView.tsx +1 -13
- 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 +9 -26
- package/package.json +34 -34
- package/lib/model/geometry.ts +0 -247
- 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 -108
- package/lib/model/node/shape/star.ts +0 -63
- 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/model/history.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
2
|
import type { Editor } from "./editor"
|
|
3
|
-
import type { Node,
|
|
3
|
+
import type { Node, SerializedNode } from "./node"
|
|
4
4
|
import type { PageProps } from "./page"
|
|
5
5
|
|
|
6
|
-
export type HistoryAction<
|
|
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<
|
|
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<
|
|
23
|
-
undo: HistoryAction<
|
|
24
|
-
redo: HistoryAction<
|
|
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 = <
|
|
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))
|
package/lib/model/index.ts
CHANGED
|
@@ -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/
|
|
5
|
-
export { FormattableNode, type FormattableNodeProps } from "./node/
|
|
6
|
-
export { GroupNode, type GroupNodeProps } from "./node/
|
|
7
|
-
export { ImageNode, type ImageNodeProps } from "./node/
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
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 &&
|
|
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
|
|
3
|
-
import
|
|
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(
|
|
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(
|
|
18
|
-
super(
|
|
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:
|
|
68
|
+
...super.props,
|
|
69
|
+
nodes: nodes
|
|
70
70
|
.values()
|
|
71
|
-
.map((node) =>
|
|
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
|
|
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(
|
|
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
|
|
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 "../
|
|
5
|
-
import type
|
|
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<
|
|
10
|
+
Pick<
|
|
11
|
+
ShapeNode,
|
|
12
|
+
"valign" | "halign" | "background" | "borderWidth" | "borderColor" | "shape"
|
|
13
|
+
>
|
|
10
14
|
>
|
|
11
15
|
|
|
12
|
-
export
|
|
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
|
-
{
|
|
32
|
+
{
|
|
33
|
+
valign,
|
|
34
|
+
halign,
|
|
35
|
+
background,
|
|
36
|
+
borderColor,
|
|
37
|
+
borderWidth,
|
|
38
|
+
shape,
|
|
39
|
+
...props
|
|
40
|
+
}: ShapeNodeProps,
|
|
24
41
|
) {
|
|
25
|
-
super(
|
|
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 "./
|
|
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(
|
|
23
|
-
|
|
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(
|
|
33
|
-
super(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
}
|