@lazlon-platform/html-editor 0.5.0 → 0.7.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 +136 -87
- package/lib/hooks/batch.ts +24 -10
- package/lib/hooks/index.ts +7 -6
- package/lib/hooks/page.ts +2 -4
- package/lib/hooks/pointer/useMovePoint.ts +100 -0
- package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +47 -39
- package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
- package/lib/hooks/pointer/useResize/index.ts +31 -0
- package/lib/hooks/pointer/useResize/multi.ts +161 -0
- package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
- package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
- package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
- package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
- package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
- package/lib/hooks/pointer/useRotation.ts +102 -0
- package/lib/hooks/pointer/{selector.ts → useSelector.ts} +18 -3
- package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
- package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
- package/lib/hooks/textMarks.ts +30 -21
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +31 -13
- package/lib/model/geometry/math.ts +128 -1
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +15 -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} +6 -12
- package/lib/model/node/lineNode.ts +80 -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} +9 -24
- package/lib/model/node.ts +27 -32
- package/lib/model/page.ts +4 -4
- 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} +10 -7
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +30 -0
- 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 +6 -5
- package/package.json +1 -1
- package/lib/hooks/pointer/resize.ts +0 -247
- package/lib/hooks/pointer/rotation.ts +0 -103
- 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 -130
- package/lib/model/node/shape/star.ts +0 -91
- 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
|
@@ -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,40 +1,29 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../editor"
|
|
3
2
|
import type { Page } from "../page"
|
|
4
|
-
import type
|
|
5
|
-
import { EditableNode, type EditableNodeProps } from "./editable"
|
|
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
|
|
|
14
12
|
@state accessor halign: "left" | "center" | "right" | "justify"
|
|
15
13
|
@state accessor size: number = 16
|
|
16
|
-
@state accessor contentHeight: number = 24
|
|
17
14
|
|
|
18
15
|
private observer = new ResizeObserver(([entry]) => {
|
|
19
|
-
this.
|
|
16
|
+
this.height = entry.target.clientHeight
|
|
20
17
|
})
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
set height(_) {
|
|
27
|
-
// no-op: TextNode's height can be set using its font-size
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
constructor(editor: Editor, page: Page, { halign, size, ...props }: TextNodeProps) {
|
|
31
|
-
super(editor, page, props)
|
|
19
|
+
constructor(page: Page, { halign, size, height = 24, ...props }: TextNodeProps) {
|
|
20
|
+
super(page, { height, ...props })
|
|
32
21
|
this.halign = halign ?? "center"
|
|
33
22
|
this.size = size ?? 16
|
|
34
23
|
|
|
35
24
|
this.tiptap.on("transaction", () => {
|
|
36
25
|
const height = this.tiptap.view.dom.clientHeight
|
|
37
|
-
if (height > 0) this.
|
|
26
|
+
if (height > 0) this.height = height
|
|
38
27
|
})
|
|
39
28
|
}
|
|
40
29
|
|
|
@@ -43,7 +32,7 @@ export class TextNode extends EditableNode {
|
|
|
43
32
|
|
|
44
33
|
if (ref) {
|
|
45
34
|
this.observer.observe(ref)
|
|
46
|
-
this.
|
|
35
|
+
this.height = ref.clientHeight
|
|
47
36
|
} else {
|
|
48
37
|
this.observer.disconnect()
|
|
49
38
|
}
|
|
@@ -53,12 +42,8 @@ export class TextNode extends EditableNode {
|
|
|
53
42
|
return super.contentRef
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
props(): TextNodeProps {
|
|
45
|
+
get props(): TextNodeProps {
|
|
57
46
|
const { halign, size } = this
|
|
58
|
-
return { ...super.props
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
serialize(): SerializedNode<this["name"], TextNodeProps> {
|
|
62
|
-
return super.serialize()
|
|
47
|
+
return { ...super.props, halign, size }
|
|
63
48
|
}
|
|
64
49
|
}
|
package/lib/model/node.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "./editor"
|
|
3
|
-
import type { Page } from "./page"
|
|
4
2
|
import { deg, type Deg } from "./geometry/math"
|
|
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
|
|
17
|
-
extends
|
|
13
|
+
extends
|
|
14
|
+
Pick<Node, "id" | "x" | "y" | "width" | "height">,
|
|
18
15
|
Partial<Pick<Node, "locked" | "rotation" | "css">> {
|
|
19
16
|
// serialized as a list in case we need to allow
|
|
20
17
|
// nodes to have multiple roles in the future
|
|
@@ -23,6 +20,7 @@ export interface NodeProps
|
|
|
23
20
|
|
|
24
21
|
export abstract class Node {
|
|
25
22
|
abstract get name(): string
|
|
23
|
+
|
|
26
24
|
readonly id: string
|
|
27
25
|
readonly page: Page
|
|
28
26
|
|
|
@@ -31,12 +29,12 @@ export abstract class Node {
|
|
|
31
29
|
@state private accessor _width: number
|
|
32
30
|
@state private accessor _height: number
|
|
33
31
|
|
|
34
|
-
@state accessor rotation: Deg
|
|
35
|
-
@state accessor role: string | null
|
|
36
|
-
@state accessor locked: boolean
|
|
37
|
-
@state accessor x: number
|
|
38
|
-
@state accessor y: number
|
|
39
|
-
@state accessor css: string
|
|
32
|
+
@state accessor rotation: Deg = deg(0)
|
|
33
|
+
@state accessor role: string | null = null
|
|
34
|
+
@state accessor locked: boolean = false
|
|
35
|
+
@state accessor x: number = 0
|
|
36
|
+
@state accessor y: number = 0
|
|
37
|
+
@state accessor css: string = ""
|
|
40
38
|
|
|
41
39
|
@computed get width() {
|
|
42
40
|
return this._width
|
|
@@ -56,24 +54,28 @@ export abstract class Node {
|
|
|
56
54
|
this._height = n
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
constructor(
|
|
57
|
+
constructor(page: Page, props: NodeProps) {
|
|
60
58
|
this.page = page
|
|
61
59
|
this.id = props.id
|
|
62
60
|
this.x = props.x
|
|
63
61
|
this.y = props.y
|
|
64
62
|
this._width = props.width
|
|
65
63
|
this._height = props.height
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
if (typeof props.role === "object") {
|
|
65
|
+
this.role = props.role?.[0]
|
|
66
|
+
}
|
|
67
|
+
if (typeof props.locked === "boolean") {
|
|
68
|
+
this.locked = props.locked
|
|
69
|
+
}
|
|
70
|
+
if (typeof props.rotation === "number") {
|
|
71
|
+
this.rotation = props.rotation
|
|
72
|
+
}
|
|
73
|
+
if (typeof props.css === "string") {
|
|
74
|
+
this.css = props.css
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
props(): NodeProps {
|
|
78
|
+
get props(): NodeProps {
|
|
77
79
|
const { id, role, locked, rotation, css, x, y, width, height } = this
|
|
78
80
|
return {
|
|
79
81
|
id,
|
|
@@ -87,11 +89,4 @@ export abstract class Node {
|
|
|
87
89
|
...(rotation && { rotation }),
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
|
|
91
|
-
serialize(): SerializedNode {
|
|
92
|
-
return {
|
|
93
|
-
name: this.name,
|
|
94
|
-
props: this.props(),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
92
|
}
|
package/lib/model/page.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
2
|
import type { Editor } from "./editor"
|
|
3
3
|
import { Node, type SerializedNode } from "./node"
|
|
4
|
-
// import { createRef } from "react"
|
|
5
4
|
|
|
6
|
-
export interface PageProps
|
|
7
|
-
|
|
5
|
+
export interface PageProps extends Partial<
|
|
6
|
+
Pick<Page, "background" | "width" | "height">
|
|
7
|
+
> {
|
|
8
8
|
id: string
|
|
9
9
|
nodes?: Node[]
|
|
10
10
|
}
|
|
@@ -46,7 +46,7 @@ export class Page {
|
|
|
46
46
|
height: this.height,
|
|
47
47
|
nodes: this.nodes
|
|
48
48
|
.values()
|
|
49
|
-
.map((node) =>
|
|
49
|
+
.map((node) => this.editor.serializeNode(node))
|
|
50
50
|
.toArray(),
|
|
51
51
|
}
|
|
52
52
|
}
|
package/lib/model/traversal.ts
CHANGED
package/lib/ui/extractor.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { JSONContent } from "@tiptap/core"
|
|
2
2
|
import { uniq } from "es-toolkit"
|
|
3
3
|
import { Node } from "../model/node"
|
|
4
|
-
import { EditableNode } from "../model/node/
|
|
5
|
-
import { FormattableNode } from "../model/node/
|
|
6
|
-
import { ShapeNode } from "../model/node/
|
|
4
|
+
import { EditableNode } from "../model/node/editableNode"
|
|
5
|
+
import { FormattableNode } from "../model/node/formattableNode"
|
|
6
|
+
import { ShapeNode } from "../model/node/shapeNode"
|
|
7
7
|
import { Page } from "../model/page"
|
|
8
8
|
import { flattenNodes } from "../model/traversal"
|
|
9
9
|
|
package/lib/ui/index.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
export { getReadableForeground } from "./colors"
|
|
2
2
|
export { getColors, getFontFamilies } from "./extractor"
|
|
3
|
-
export { ArrowContent } from "./node/ArrowContent"
|
|
4
3
|
export { EditableContent } from "./node/EditableContent"
|
|
5
|
-
export { EllipseContent } from "./node/EllipseContent"
|
|
6
4
|
export { GroupContent } from "./node/GroupContent"
|
|
7
5
|
export { ImageContent } from "./node/ImageContent"
|
|
6
|
+
export { LineContent } from "./node/LineContent"
|
|
8
7
|
export { NodeView } from "./node/NodeView"
|
|
9
|
-
export {
|
|
10
|
-
export { StarContent } from "./node/StarContent"
|
|
8
|
+
export { ShapeContent } from "./node/ShapeContent"
|
|
11
9
|
export { TextContent } from "./node/TextContent"
|
|
@@ -3,8 +3,8 @@ import clsx from "clsx"
|
|
|
3
3
|
import { debounce, isEqual } from "es-toolkit"
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
5
5
|
import { useStore } from "react-bolt"
|
|
6
|
-
import { useEditor } from "
|
|
7
|
-
import type { EditableNode
|
|
6
|
+
import { useEditor } from "../../../hooks/editor"
|
|
7
|
+
import type { EditableNode } from "../../../model"
|
|
8
8
|
import { useDoubleClick } from "./useDoubleClick"
|
|
9
9
|
|
|
10
10
|
export function EditableContent(props: {
|
|
@@ -18,6 +18,7 @@ export function EditableContent(props: {
|
|
|
18
18
|
const editor = useEditor()
|
|
19
19
|
const selection = useStore(editor, "selection")
|
|
20
20
|
const { action } = useStore(editor, "action")
|
|
21
|
+
const locked = useStore(node, "locked")
|
|
21
22
|
const prevContent = useRef(node.tiptap.getJSON())
|
|
22
23
|
|
|
23
24
|
// start with selection.has(node) so that newly added editable nodes can be focused
|
|
@@ -25,9 +26,10 @@ export function EditableContent(props: {
|
|
|
25
26
|
|
|
26
27
|
const onDoubleClick = useDoubleClick(
|
|
27
28
|
useCallback(() => {
|
|
29
|
+
if (locked) return
|
|
28
30
|
setClicked(true)
|
|
29
31
|
node.tiptap.commands.focus()
|
|
30
|
-
}, [node, setClicked]),
|
|
32
|
+
}, [node, locked, setClicked]),
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
const html = useEditorState({
|
|
@@ -36,18 +38,19 @@ export function EditableContent(props: {
|
|
|
36
38
|
})
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
39
|
-
if (!selection.has(node)) {
|
|
41
|
+
if (!selection.has(node) || locked) {
|
|
40
42
|
node.blur()
|
|
43
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
41
44
|
setClicked(false)
|
|
42
45
|
}
|
|
43
|
-
}, [selection, node])
|
|
46
|
+
}, [selection, node, locked])
|
|
44
47
|
|
|
45
48
|
useEffect(() => {
|
|
46
49
|
const onUpdate = debounce(() => {
|
|
47
50
|
const next = node.tiptap.getJSON()
|
|
48
51
|
const prev = prevContent.current
|
|
49
52
|
if (!isEqual(prev, next)) {
|
|
50
|
-
editor.history.push<
|
|
53
|
+
editor.history.push<EditableNode>({
|
|
51
54
|
undo: ["set-node-props", [node.id, { content: prev }]],
|
|
52
55
|
redo: ["set-node-props", [node.id, { content: next }]],
|
|
53
56
|
})
|
|
@@ -70,7 +73,7 @@ export function EditableContent(props: {
|
|
|
70
73
|
})
|
|
71
74
|
|
|
72
75
|
const lineHeight = useStore(node, "lineHeight")
|
|
73
|
-
const showEditor = !isStatic && selection.has(node) && !action && clicked
|
|
76
|
+
const showEditor = !isStatic && selection.has(node) && !action && clicked && !locked
|
|
74
77
|
|
|
75
78
|
return (
|
|
76
79
|
<div
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useStore } from "react-bolt"
|
|
2
2
|
import type { Node } from "../../model/node"
|
|
3
|
-
import type { GroupNode } from "../../model
|
|
3
|
+
import type { GroupNode } from "../../model"
|
|
4
4
|
|
|
5
5
|
function GroupedNodeContent(props: { node: Node; children: React.ReactNode }) {
|
|
6
6
|
const { node, children } = props
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import clsx from "clsx"
|
|
2
2
|
import { useStore } from "react-bolt"
|
|
3
|
-
import type { ImageNode } from "../../model
|
|
3
|
+
import type { ImageNode } from "../../model"
|
|
4
4
|
|
|
5
5
|
export function ImageContent(props: { node: ImageNode; fallback?: React.ReactNode }) {
|
|
6
6
|
const { node, fallback } = props
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { LineNode } from "../../model/node/lineNode"
|
|
3
|
+
|
|
4
|
+
export function LineContent(props: { node: LineNode }) {
|
|
5
|
+
const [width, height, points, strokeWidth, strokeColor] = useStore(
|
|
6
|
+
props.node,
|
|
7
|
+
"width",
|
|
8
|
+
"height",
|
|
9
|
+
"points",
|
|
10
|
+
"strokeWidth",
|
|
11
|
+
"strokeColor",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
className="absolute inset-0 overflow-visible"
|
|
17
|
+
width={width}
|
|
18
|
+
height={height}
|
|
19
|
+
fill="none"
|
|
20
|
+
>
|
|
21
|
+
<polyline
|
|
22
|
+
points={points.map(({ x, y }) => `${x},${y}`).join(" ")}
|
|
23
|
+
stroke={strokeColor}
|
|
24
|
+
strokeWidth={strokeWidth}
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
4
|
+
import type { ArrowShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
5
|
+
|
|
6
|
+
function arrowPath(props: { height: number; width: number; roundness: number }) {
|
|
7
|
+
const { height, width, roundness } = props
|
|
8
|
+
const bodyRight = width * 0.68
|
|
9
|
+
const inset = height * 0.28
|
|
10
|
+
|
|
11
|
+
const arrowPoints = [
|
|
12
|
+
{ x: 0, y: inset },
|
|
13
|
+
{ x: bodyRight, y: inset },
|
|
14
|
+
{ x: bodyRight, y: 0 },
|
|
15
|
+
{ x: width, y: height / 2 },
|
|
16
|
+
{ x: bodyRight, y: height },
|
|
17
|
+
{ x: bodyRight, y: height - inset },
|
|
18
|
+
{ x: 0, y: height - inset },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
return roundedPathData(arrowPoints, roundness)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ArrowContent(props: { node: ShapeNode; shape: ArrowShape["props"] }) {
|
|
25
|
+
const maskId = useId()
|
|
26
|
+
const { node, shape } = props
|
|
27
|
+
|
|
28
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
29
|
+
node,
|
|
30
|
+
"background",
|
|
31
|
+
"borderWidth",
|
|
32
|
+
"borderColor",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const [width, height] = useStore(node, "width", "height")
|
|
36
|
+
const { roundness } = shape
|
|
37
|
+
const d = arrowPath({ width, height, roundness })
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
41
|
+
<defs>
|
|
42
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
43
|
+
<path d={d} fill="white" />
|
|
44
|
+
</mask>
|
|
45
|
+
</defs>
|
|
46
|
+
|
|
47
|
+
<path d={d} fill={background} />
|
|
48
|
+
<path
|
|
49
|
+
d={d}
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke={borderColor}
|
|
52
|
+
strokeWidth={borderWidth}
|
|
53
|
+
mask={`url(#${maskId})`}
|
|
54
|
+
/>
|
|
55
|
+
</svg>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { ShapeNode } from "../../../model/node/shapeNode"
|
|
3
|
+
|
|
4
|
+
export function EllipseContent(props: { node: ShapeNode }) {
|
|
5
|
+
const { node } = props
|
|
6
|
+
|
|
7
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
8
|
+
node,
|
|
9
|
+
"background",
|
|
10
|
+
"borderWidth",
|
|
11
|
+
"borderColor",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
className="absolute"
|
|
18
|
+
onDragStart={(e) => e.preventDefault()}
|
|
19
|
+
style={{
|
|
20
|
+
inset: borderWidth / 2,
|
|
21
|
+
width: `calc(100% - ${borderWidth}px)`,
|
|
22
|
+
height: `calc(100% - ${borderWidth}px)`,
|
|
23
|
+
overflow: "visible",
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<ellipse
|
|
27
|
+
cx="50%"
|
|
28
|
+
cy="50%"
|
|
29
|
+
rx="50%"
|
|
30
|
+
ry="50%"
|
|
31
|
+
fill={background}
|
|
32
|
+
stroke={borderColor}
|
|
33
|
+
strokeWidth={borderWidth}
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { Point } from "../../../model/geometry/math"
|
|
4
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
5
|
+
import type { PolygonShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
6
|
+
|
|
7
|
+
function regularPolygonPoints(props: {
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
sides: number
|
|
11
|
+
}): Point[] {
|
|
12
|
+
const { width, height, sides } = props
|
|
13
|
+
const rotation = sides % 2 === 0 ? Math.PI / sides : 0
|
|
14
|
+
const cx = width / 2
|
|
15
|
+
const cy = height / 2
|
|
16
|
+
const rx = width / 2
|
|
17
|
+
const ry = height / 2
|
|
18
|
+
|
|
19
|
+
return Array.from({ length: sides }, (_, i) => {
|
|
20
|
+
const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
|
|
21
|
+
return {
|
|
22
|
+
x: cx + rx * Math.cos(angle),
|
|
23
|
+
y: cy + ry * Math.sin(angle),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function PolygonContent(props: { node: ShapeNode; shape: PolygonShape["props"] }) {
|
|
29
|
+
const maskId = useId()
|
|
30
|
+
const { node, shape } = props
|
|
31
|
+
|
|
32
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
33
|
+
node,
|
|
34
|
+
"background",
|
|
35
|
+
"borderWidth",
|
|
36
|
+
"borderColor",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const [width, height] = useStore(node, "width", "height")
|
|
40
|
+
const { sides, roundness } = shape
|
|
41
|
+
|
|
42
|
+
const d = roundedPathData(regularPolygonPoints({ width, height, sides }), roundness)
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
46
|
+
<defs>
|
|
47
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
48
|
+
<path d={d} fill="white" />
|
|
49
|
+
</mask>
|
|
50
|
+
</defs>
|
|
51
|
+
|
|
52
|
+
<path d={d} fill={background} />
|
|
53
|
+
<path
|
|
54
|
+
d={d}
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke={borderColor}
|
|
57
|
+
strokeWidth={borderWidth}
|
|
58
|
+
mask={`url(#${maskId})`}
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { RectangleShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
3
|
+
|
|
4
|
+
export function RectangleShape(props: {
|
|
5
|
+
node: ShapeNode
|
|
6
|
+
shape: RectangleShape["props"]
|
|
7
|
+
}) {
|
|
8
|
+
const { node, shape } = props
|
|
9
|
+
|
|
10
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
11
|
+
node,
|
|
12
|
+
"background",
|
|
13
|
+
"borderWidth",
|
|
14
|
+
"borderColor",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const radii = [
|
|
18
|
+
shape["cornerTopLeft"],
|
|
19
|
+
shape["cornerTopRight"],
|
|
20
|
+
shape["cornerBottomRight"],
|
|
21
|
+
shape["cornerBottomLeft"],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="absolute inset-0 size-full border-4"
|
|
27
|
+
onDragStart={(e) => e.preventDefault()}
|
|
28
|
+
style={{
|
|
29
|
+
borderRadius: radii.map((r) => `${r}px`).join(" "),
|
|
30
|
+
background: background,
|
|
31
|
+
border: `${borderWidth}px solid ${borderColor}`,
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|