@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/page.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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
5
|
export interface PageProps
|
|
7
6
|
extends Partial<Pick<Page, "background" | "width" | "height">> {
|
|
@@ -15,6 +14,7 @@ export interface SerializedPage extends Omit<PageProps, "nodes"> {
|
|
|
15
14
|
|
|
16
15
|
export class Page {
|
|
17
16
|
readonly id: string
|
|
17
|
+
readonly editor: Editor
|
|
18
18
|
|
|
19
19
|
ref: HTMLElement | null = null
|
|
20
20
|
|
|
@@ -28,7 +28,8 @@ export class Page {
|
|
|
28
28
|
| { y: number; x?: number; w?: number; h?: never } // vertical
|
|
29
29
|
> = []
|
|
30
30
|
|
|
31
|
-
constructor(
|
|
31
|
+
constructor(editor: Editor, props: PageProps) {
|
|
32
|
+
this.editor = editor
|
|
32
33
|
this.id = props.id
|
|
33
34
|
this.background = props.background ?? "#ffffff"
|
|
34
35
|
this.width = props.width ?? 841
|
|
@@ -44,7 +45,7 @@ export class Page {
|
|
|
44
45
|
height: this.height,
|
|
45
46
|
nodes: this.nodes
|
|
46
47
|
.values()
|
|
47
|
-
.map((node) =>
|
|
48
|
+
.map((node) => this.editor.serializeNode(node))
|
|
48
49
|
.toArray(),
|
|
49
50
|
}
|
|
50
51
|
}
|
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: {
|
|
@@ -38,6 +38,7 @@ export function EditableContent(props: {
|
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
if (!selection.has(node)) {
|
|
40
40
|
node.blur()
|
|
41
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
41
42
|
setClicked(false)
|
|
42
43
|
}
|
|
43
44
|
}, [selection, node])
|
|
@@ -47,7 +48,7 @@ export function EditableContent(props: {
|
|
|
47
48
|
const next = node.tiptap.getJSON()
|
|
48
49
|
const prev = prevContent.current
|
|
49
50
|
if (!isEqual(prev, next)) {
|
|
50
|
-
editor.history.push<
|
|
51
|
+
editor.history.push<EditableNode>({
|
|
51
52
|
undo: ["set-node-props", [node.id, { content: prev }]],
|
|
52
53
|
redo: ["set-node-props", [node.id, { content: next }]],
|
|
53
54
|
})
|
|
@@ -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,32 @@
|
|
|
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
|
+
const polylinePoints = points.map(({ x, y }) => `${x},${y}`).join(" ")
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
className="absolute inset-0 overflow-visible"
|
|
19
|
+
width={width}
|
|
20
|
+
height={height}
|
|
21
|
+
fill="none"
|
|
22
|
+
>
|
|
23
|
+
<polyline
|
|
24
|
+
points={polylinePoints}
|
|
25
|
+
stroke={strokeColor}
|
|
26
|
+
strokeWidth={strokeWidth}
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
)
|
|
32
|
+
}
|
package/lib/ui/node/NodeView.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useEffect } from "react"
|
|
2
2
|
import { useStore } from "react-bolt"
|
|
3
3
|
import { useEditor } from "../../hooks/editor"
|
|
4
|
+
import { isLoaded, loadFont } from "../../lib/googleFonts"
|
|
4
5
|
import { Node } from "../../model/node"
|
|
5
6
|
import { getFontFamilies } from "../../ui/extractor"
|
|
6
|
-
import { isLoaded, loadFont } from "../../lib/googleFonts"
|
|
7
|
-
import { isPointerInSelectionRect } from "../selection"
|
|
8
7
|
|
|
9
8
|
export function NodeView(props: {
|
|
10
9
|
node: Node
|
|
@@ -49,17 +48,6 @@ export function NodeView(props: {
|
|
|
49
48
|
height: `${height}px`,
|
|
50
49
|
transform: `translate(${x}px,${y}px) rotate(${rotation ?? 0}deg)`,
|
|
51
50
|
}}
|
|
52
|
-
onDragStart={(e) => e.preventDefault()}
|
|
53
|
-
onPointerDown={(event) => {
|
|
54
|
-
if (event.button !== 0) return
|
|
55
|
-
|
|
56
|
-
const { selection } = editor
|
|
57
|
-
if (selection.size === 0 || event.shiftKey) {
|
|
58
|
-
editor.selection = new Set([...selection, node])
|
|
59
|
-
} else if (!isPointerInSelectionRect(selection, event)) {
|
|
60
|
-
editor.selection = new Set([node])
|
|
61
|
-
}
|
|
62
|
-
}}
|
|
63
51
|
>
|
|
64
52
|
{children}
|
|
65
53
|
{css && <style>{`#${id} { ${css} }`}</style>}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ShapeNode, StarShape } from "../../../model/node/shapeNode"
|
|
6
|
+
|
|
7
|
+
function starPoints(props: {
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
corners: number
|
|
11
|
+
depth: number
|
|
12
|
+
}): Point[] {
|
|
13
|
+
const { width, height, corners, depth } = props
|
|
14
|
+
const cx = width / 2
|
|
15
|
+
const cy = height / 2
|
|
16
|
+
const outerRx = width / 2
|
|
17
|
+
const outerRy = height / 2
|
|
18
|
+
const innerRx = outerRx * depth
|
|
19
|
+
const innerRy = outerRy * depth
|
|
20
|
+
|
|
21
|
+
return Array.from({ length: corners * 2 }, (_, i) => {
|
|
22
|
+
const angle = (i * Math.PI) / corners - Math.PI / 2
|
|
23
|
+
const isOuter = i % 2 === 0
|
|
24
|
+
const rx = isOuter ? outerRx : innerRx
|
|
25
|
+
const ry = isOuter ? outerRy : innerRy
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
x: cx + rx * Math.cos(angle),
|
|
29
|
+
y: cy + ry * Math.sin(angle),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function StarContent(props: { node: ShapeNode; shape: StarShape["props"] }) {
|
|
35
|
+
const maskId = useId()
|
|
36
|
+
const { node, shape } = props
|
|
37
|
+
|
|
38
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
39
|
+
node,
|
|
40
|
+
"background",
|
|
41
|
+
"borderWidth",
|
|
42
|
+
"borderColor",
|
|
43
|
+
)
|
|
44
|
+
const [width, height] = useStore(node, "width", "height")
|
|
45
|
+
const { corners, depth, roundness } = shape
|
|
46
|
+
|
|
47
|
+
const d = roundedPathData(
|
|
48
|
+
starPoints({
|
|
49
|
+
width,
|
|
50
|
+
height,
|
|
51
|
+
corners,
|
|
52
|
+
depth,
|
|
53
|
+
}),
|
|
54
|
+
roundness,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
59
|
+
<defs>
|
|
60
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
61
|
+
<path d={d} fill="white" />
|
|
62
|
+
</mask>
|
|
63
|
+
</defs>
|
|
64
|
+
|
|
65
|
+
<path d={d} fill={background} />
|
|
66
|
+
<path
|
|
67
|
+
d={d}
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke={borderColor}
|
|
70
|
+
strokeWidth={borderWidth}
|
|
71
|
+
mask={`url(#${maskId})`}
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import clsx from "clsx"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { ShapeNode } from "../../../model"
|
|
4
|
+
import { EditableContent } from "../EditableContent"
|
|
5
|
+
import { ArrowContent } from "./ArrowContent"
|
|
6
|
+
import { EllipseContent } from "./EllipseContent"
|
|
7
|
+
import { PolygonContent } from "./PolygonContent"
|
|
8
|
+
import { RectangleShape } from "./RectangleContent"
|
|
9
|
+
import { StarContent } from "./StarContent"
|
|
10
|
+
|
|
11
|
+
export function ShapeContent(props: { node: ShapeNode; isStatic?: boolean }) {
|
|
12
|
+
const { node, isStatic } = props
|
|
13
|
+
const [valign, halign, shape] = useStore(node, "valign", "halign", "shape")
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="relative size-full">
|
|
17
|
+
{shape.name === "rectangle" && <RectangleShape node={node} shape={shape.props} />}
|
|
18
|
+
{shape.name === "polygon" && <PolygonContent node={node} shape={shape.props} />}
|
|
19
|
+
{shape.name === "star" && <StarContent node={node} shape={shape.props} />}
|
|
20
|
+
{shape.name === "ellipse" && <EllipseContent node={node} />}
|
|
21
|
+
{shape.name === "arrow" && <ArrowContent node={node} shape={shape.props} />}
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
className={clsx(
|
|
25
|
+
"flex size-full",
|
|
26
|
+
valign === "top" && "items-start",
|
|
27
|
+
valign === "center" && "items-center",
|
|
28
|
+
valign === "bottom" && "items-end",
|
|
29
|
+
halign === "left" && "justify-start text-left",
|
|
30
|
+
halign === "center" && "justify-center text-center",
|
|
31
|
+
halign === "right" && "justify-end text-right",
|
|
32
|
+
halign === "justify" && "w-full text-justify",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<EditableContent
|
|
36
|
+
isStatic={isStatic}
|
|
37
|
+
node={node}
|
|
38
|
+
className={clsx(halign === "justify" && "w-full")}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -2,7 +2,7 @@ import clsx from "clsx"
|
|
|
2
2
|
import { useEffect } from "react"
|
|
3
3
|
import { useStore } from "react-bolt"
|
|
4
4
|
import { useEditor } from "../../hooks/editor"
|
|
5
|
-
import type { TextNode } from "../../model
|
|
5
|
+
import type { TextNode } from "../../model"
|
|
6
6
|
import { EditableContent } from "./EditableContent"
|
|
7
7
|
|
|
8
8
|
export function TextContent(props: {
|
package/lib/ui/selection.ts
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { box, boxBounds, rect, type Box, type Rect } from "../model/geometry/math"
|
|
2
2
|
import type { Node } from "../model/node"
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function selectionDOMRect(selection: Set<Node>): Rect {
|
|
5
5
|
const rects = selection
|
|
6
6
|
.values()
|
|
7
7
|
.map((node) => node.ref)
|
|
8
8
|
.filter((dom) => dom instanceof HTMLElement)
|
|
9
9
|
.map((dom) => dom.getBoundingClientRect())
|
|
10
|
-
.toArray()
|
|
11
10
|
|
|
12
|
-
return
|
|
11
|
+
return rect(...rects)
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return boundingBox(rects)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isPointerInSelectionRect(
|
|
25
|
-
selection: Set<Node>,
|
|
26
|
-
event: React.PointerEvent,
|
|
27
|
-
) {
|
|
28
|
-
const rect = getTargetDOMRect(selection)
|
|
29
|
-
const right = rect.x + rect.width
|
|
30
|
-
const bottom = rect.y + rect.height
|
|
14
|
+
export function selectionBox(selection: Set<Node>): Box {
|
|
15
|
+
if (selection.size === 1) {
|
|
16
|
+
const [node] = selection
|
|
17
|
+
return box(node)
|
|
18
|
+
}
|
|
31
19
|
|
|
32
|
-
return (
|
|
33
|
-
event.clientX >= rect.x &&
|
|
34
|
-
event.clientX <= right &&
|
|
35
|
-
event.clientY >= rect.y &&
|
|
36
|
-
event.clientY <= bottom
|
|
37
|
-
)
|
|
20
|
+
return box(boxBounds(...selection.values().map(box)))
|
|
38
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazlon-platform/html-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"lib"
|
|
@@ -8,55 +8,55 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
"./ui": "./lib/ui/index.ts",
|
|
10
10
|
"./model": "./lib/model/index.ts",
|
|
11
|
-
"./model/geometry": "./lib/model/geometry.ts",
|
|
11
|
+
"./model/geometry": "./lib/model/geometry/math.ts",
|
|
12
12
|
"./hooks": "./lib/hooks/index.ts",
|
|
13
13
|
"./googleFonts": "./lib/lib/googleFonts.ts"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tiptap/core": "^3.
|
|
17
|
-
"@tiptap/extension-bold": "^3.
|
|
18
|
-
"@tiptap/extension-document": "^3.
|
|
19
|
-
"@tiptap/extension-hard-break": "^3.
|
|
20
|
-
"@tiptap/extension-italic": "^3.
|
|
21
|
-
"@tiptap/extension-paragraph": "^3.
|
|
22
|
-
"@tiptap/extension-strike": "^3.
|
|
23
|
-
"@tiptap/extension-subscript": "^3.
|
|
24
|
-
"@tiptap/extension-superscript": "^3.
|
|
25
|
-
"@tiptap/extension-text": "^3.
|
|
26
|
-
"@tiptap/extension-text-style": "^3.
|
|
27
|
-
"@tiptap/extension-underline": "^3.
|
|
28
|
-
"@tiptap/extensions": "^3.
|
|
29
|
-
"@tiptap/pm": "^3.
|
|
30
|
-
"@tiptap/react": "^3.
|
|
16
|
+
"@tiptap/core": "^3.23.1",
|
|
17
|
+
"@tiptap/extension-bold": "^3.23.1",
|
|
18
|
+
"@tiptap/extension-document": "^3.23.1",
|
|
19
|
+
"@tiptap/extension-hard-break": "^3.23.1",
|
|
20
|
+
"@tiptap/extension-italic": "^3.23.1",
|
|
21
|
+
"@tiptap/extension-paragraph": "^3.23.1",
|
|
22
|
+
"@tiptap/extension-strike": "^3.23.1",
|
|
23
|
+
"@tiptap/extension-subscript": "^3.23.1",
|
|
24
|
+
"@tiptap/extension-superscript": "^3.23.1",
|
|
25
|
+
"@tiptap/extension-text": "^3.23.1",
|
|
26
|
+
"@tiptap/extension-text-style": "^3.23.1",
|
|
27
|
+
"@tiptap/extension-underline": "^3.23.1",
|
|
28
|
+
"@tiptap/extensions": "^3.23.1",
|
|
29
|
+
"@tiptap/pm": "^3.23.1",
|
|
30
|
+
"@tiptap/react": "^3.23.1",
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
|
-
"es-toolkit": "^1.
|
|
33
|
-
"react": "^19.2.
|
|
34
|
-
"react-aria-components": "^1.
|
|
32
|
+
"es-toolkit": "^1.46.1",
|
|
33
|
+
"react": "^19.2.6",
|
|
34
|
+
"react-aria-components": "^1.17.0",
|
|
35
35
|
"react-bolt": "^1.4.1",
|
|
36
|
-
"react-dom": "^19.2.
|
|
37
|
-
"react-hotkeys-hook": "^5.2
|
|
38
|
-
"tailwindcss": "^4.
|
|
36
|
+
"react-dom": "^19.2.6",
|
|
37
|
+
"react-hotkeys-hook": "^5.3.2",
|
|
38
|
+
"tailwindcss": "^4.3.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@eslint/js": "^9.39.
|
|
42
|
-
"@tailwindcss/vite": "^4.
|
|
41
|
+
"@eslint/js": "^9.39.4",
|
|
42
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
43
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
44
44
|
"@testing-library/react": "^16.3.2",
|
|
45
|
-
"@types/node": "^24.
|
|
45
|
+
"@types/node": "^24.12.3",
|
|
46
46
|
"@types/react": "^19.2.7",
|
|
47
47
|
"@types/react-dom": "^19.2.3",
|
|
48
|
-
"@vitejs/plugin-react": "^5.
|
|
49
|
-
"@vitest/coverage-v8": "^4.1.
|
|
50
|
-
"eslint": "^9.39.
|
|
51
|
-
"eslint-plugin-react-hooks": "^7.
|
|
48
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
49
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
50
|
+
"eslint": "^9.39.4",
|
|
51
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
52
52
|
"eslint-plugin-react-refresh": "^0.4.24",
|
|
53
53
|
"globals": "^16.5.0",
|
|
54
|
-
"happy-dom": "^20.
|
|
54
|
+
"happy-dom": "^20.9.0",
|
|
55
55
|
"typescript": "~5.9.3",
|
|
56
|
-
"typescript-eslint": "^8.
|
|
57
|
-
"vite": "^7.3.
|
|
56
|
+
"typescript-eslint": "^8.59.2",
|
|
57
|
+
"vite": "^7.3.3",
|
|
58
58
|
"vite-tsconfig-paths": "^6.1.1",
|
|
59
|
-
"vitest": "^4.1.
|
|
59
|
+
"vitest": "^4.1.5"
|
|
60
60
|
},
|
|
61
61
|
"prettier": {
|
|
62
62
|
"semi": false,
|