@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.
Files changed (64) hide show
  1. package/lib/hooks/actions.ts +136 -87
  2. package/lib/hooks/batch.ts +24 -10
  3. package/lib/hooks/index.ts +7 -6
  4. package/lib/hooks/page.ts +2 -4
  5. package/lib/hooks/pointer/useMovePoint.ts +100 -0
  6. package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +47 -39
  7. package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
  8. package/lib/hooks/pointer/useResize/index.ts +31 -0
  9. package/lib/hooks/pointer/useResize/multi.ts +161 -0
  10. package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
  11. package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
  12. package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
  13. package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
  14. package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
  15. package/lib/hooks/pointer/useRotation.ts +102 -0
  16. package/lib/hooks/pointer/{selector.ts → useSelector.ts} +18 -3
  17. package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
  18. package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
  19. package/lib/hooks/textMarks.ts +30 -21
  20. package/lib/lib/googleFonts.ts +1 -5
  21. package/lib/model/editor.ts +31 -13
  22. package/lib/model/geometry/math.ts +128 -1
  23. package/lib/model/history.ts +10 -13
  24. package/lib/model/index.ts +15 -10
  25. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  26. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  27. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  28. package/lib/model/node/{image.ts → imageNode.ts} +6 -12
  29. package/lib/model/node/lineNode.ts +80 -0
  30. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  31. package/lib/model/node/shapeNode/shape.ts +96 -0
  32. package/lib/model/node/{text.ts → textNode.ts} +9 -24
  33. package/lib/model/node.ts +27 -32
  34. package/lib/model/page.ts +4 -4
  35. package/lib/model/traversal.ts +1 -1
  36. package/lib/ui/extractor.ts +3 -3
  37. package/lib/ui/index.ts +2 -4
  38. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +10 -7
  39. package/lib/ui/node/GroupContent.tsx +1 -1
  40. package/lib/ui/node/ImageContent.tsx +1 -1
  41. package/lib/ui/node/LineContent.tsx +30 -0
  42. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  43. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  44. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  45. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  46. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  47. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  48. package/lib/ui/node/TextContent.tsx +1 -1
  49. package/lib/ui/selection.ts +6 -5
  50. package/package.json +1 -1
  51. package/lib/hooks/pointer/resize.ts +0 -247
  52. package/lib/hooks/pointer/rotation.ts +0 -103
  53. package/lib/model/node/shape/arrow.ts +0 -50
  54. package/lib/model/node/shape/ellipse.ts +0 -26
  55. package/lib/model/node/shape/polygon.ts +0 -130
  56. package/lib/model/node/shape/star.ts +0 -91
  57. package/lib/ui/node/ArrowContent.tsx +0 -60
  58. package/lib/ui/node/EllipseContent.tsx +0 -49
  59. package/lib/ui/node/PolygonContent.tsx +0 -81
  60. package/lib/ui/node/StarContent.tsx +0 -60
  61. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  62. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  63. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  64. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,130 +0,0 @@
1
- import { computed, state } from "react-bolt"
2
- import type { Editor } from "../../editor"
3
- import type { Point } from "../../geometry/math"
4
- import { roundedPathData } from "../../geometry/svg"
5
- import type { SerializedNode } from "../../node"
6
- import type { Page } from "../../page"
7
- import { ShapeNode, type ShapeNodeProps } from "./shape"
8
-
9
- export type PolygonNodeProps = ShapeNodeProps &
10
- Partial<
11
- Pick<
12
- PolygonNode,
13
- | "roundness"
14
- | "sides"
15
- | "cornerTopLeft"
16
- | "cornerTopRight"
17
- | "cornerBottomLeft"
18
- | "cornerBottomRight"
19
- >
20
- >
21
-
22
- export class PolygonNode extends ShapeNode {
23
- get name() {
24
- return "polygon"
25
- }
26
-
27
- @state private accessor _roundness: number
28
- @state accessor sides: number
29
-
30
- // rectangles are special cased where they support by corner rounding
31
- @state accessor cornerTopLeft: number
32
- @state accessor cornerTopRight: number
33
- @state accessor cornerBottomLeft: number
34
- @state accessor cornerBottomRight: number
35
-
36
- set roundness(r: number) {
37
- if (this.sides === 4) {
38
- this.cornerTopLeft = r
39
- this.cornerTopRight = r
40
- this.cornerBottomLeft = r
41
- this.cornerBottomRight = r
42
- }
43
-
44
- this._roundness = r
45
- }
46
-
47
- @computed get roundness() {
48
- if (this.sides === 4) {
49
- const [first, ...radii] = [
50
- this.cornerTopLeft,
51
- this.cornerTopRight,
52
- this.cornerBottomLeft,
53
- this.cornerBottomRight,
54
- ]
55
-
56
- return radii.reduce((acc, it) => (it === acc ? acc : 0), first)
57
- }
58
-
59
- return this._roundness
60
- }
61
-
62
- constructor(
63
- editor: Editor,
64
- page: Page,
65
- {
66
- sides,
67
- cornerTopLeft,
68
- cornerTopRight,
69
- cornerBottomLeft,
70
- cornerBottomRight,
71
- roundness = 0,
72
- ...props
73
- }: PolygonNodeProps,
74
- ) {
75
- super(editor, page, props)
76
- this._roundness = roundness
77
- this.sides = sides ?? 4
78
- this.cornerTopLeft = cornerTopLeft ?? roundness
79
- this.cornerTopRight = cornerTopRight ?? roundness
80
- this.cornerBottomLeft = cornerBottomLeft ?? roundness
81
- this.cornerBottomRight = cornerBottomRight ?? roundness
82
- }
83
-
84
- props(): PolygonNodeProps {
85
- return {
86
- ...super.props(),
87
- sides: this.sides,
88
- cornerTopLeft: this.cornerTopLeft,
89
- cornerTopRight: this.cornerTopRight,
90
- cornerBottomLeft: this.cornerBottomLeft,
91
- cornerBottomRight: this.cornerBottomRight,
92
- }
93
- }
94
-
95
- serialize(): SerializedNode<this["name"], PolygonNodeProps> {
96
- return super.serialize()
97
- }
98
-
99
- @computed get svgPathData() {
100
- return roundedPathData(
101
- regularPolygonPoints({
102
- width: this.width,
103
- height: this.height,
104
- sides: this.sides,
105
- }),
106
- this._roundness,
107
- )
108
- }
109
- }
110
-
111
- function regularPolygonPoints(props: {
112
- width: number
113
- height: number
114
- sides: number
115
- }): Point[] {
116
- const { width, height, sides } = props
117
- const rotation = sides % 2 === 0 ? Math.PI / sides : 0
118
- const cx = width / 2
119
- const cy = height / 2
120
- const rx = width / 2
121
- const ry = height / 2
122
-
123
- return Array.from({ length: sides }, (_, i) => {
124
- const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
125
- return {
126
- x: cx + rx * Math.cos(angle),
127
- y: cy + ry * Math.sin(angle),
128
- }
129
- })
130
- }
@@ -1,91 +0,0 @@
1
- import { computed, state } from "react-bolt"
2
- import type { Editor } from "../../editor"
3
- import type { SerializedNode } from "../../node"
4
- import type { Page } from "../../page"
5
- import { ShapeNode, type ShapeNodeProps } from "./shape"
6
- import { clamp, type Point } from "../../geometry/math"
7
- import { roundedPathData } from "../../geometry/svg"
8
-
9
- export type StarNodeProps = ShapeNodeProps &
10
- Partial<Pick<StarNode, "corners" | "roundness" | "depth">>
11
-
12
- export class StarNode extends ShapeNode {
13
- get name() {
14
- return "star"
15
- }
16
-
17
- @state accessor corners: number
18
- @state accessor roundness: number
19
- @state private accessor _depth: number
20
-
21
- @computed get depth() {
22
- return this._depth
23
- }
24
-
25
- set depth(value: number) {
26
- this._depth = clamp(value, 0, 1)
27
- }
28
-
29
- constructor(
30
- editor: Editor,
31
- page: Page,
32
- { corners = 5, roundness = 0, depth = 0.4, ...props }: StarNodeProps,
33
- ) {
34
- super(editor, page, props)
35
- this.corners = corners
36
- this.roundness = roundness
37
- this._depth = clamp(depth, 0, 1)
38
- }
39
-
40
- props(): StarNodeProps {
41
- return {
42
- ...super.props(),
43
- corners: this.corners,
44
- roundness: this.roundness,
45
- depth: this.depth,
46
- }
47
- }
48
-
49
- serialize(): SerializedNode<this["name"], StarNodeProps> {
50
- return super.serialize()
51
- }
52
-
53
- @computed get svgPathData() {
54
- return roundedPathData(
55
- starPoints({
56
- width: this.width,
57
- height: this.height,
58
- corners: this.corners,
59
- depth: this.depth,
60
- }),
61
- this.roundness,
62
- )
63
- }
64
- }
65
-
66
- function starPoints(props: {
67
- width: number
68
- height: number
69
- corners: number
70
- depth: number
71
- }): Point[] {
72
- const { width, height, corners, depth } = props
73
- const cx = width / 2
74
- const cy = height / 2
75
- const outerRx = width / 2
76
- const outerRy = height / 2
77
- const innerRx = outerRx * depth
78
- const innerRy = outerRy * depth
79
-
80
- return Array.from({ length: corners * 2 }, (_, i) => {
81
- const angle = (i * Math.PI) / corners - Math.PI / 2
82
- const isOuter = i % 2 === 0
83
- const rx = isOuter ? outerRx : innerRx
84
- const ry = isOuter ? outerRy : innerRy
85
-
86
- return {
87
- x: cx + rx * Math.cos(angle),
88
- y: cy + ry * Math.sin(angle),
89
- }
90
- })
91
- }
@@ -1,60 +0,0 @@
1
- import clsx from "clsx"
2
- import { useId } from "react"
3
- import { useStore } from "react-bolt"
4
- import { type ArrowNode } from "../../model/node/shape/arrow"
5
- import { EditableContent } from "./EditableContent"
6
-
7
- export function ArrowContent(props: { node: ArrowNode; isStatic?: boolean }) {
8
- const maskId = useId()
9
- const { node, isStatic } = props
10
-
11
- const [valign, halign, background, borderWidth, borderColor] = useStore(
12
- node,
13
- "valign",
14
- "halign",
15
- "background",
16
- "borderWidth",
17
- "borderColor",
18
- )
19
-
20
- const [w, h, d] = useStore(node, "width", "height", "svgPathData")
21
-
22
- return (
23
- <div className="relative size-full">
24
- <svg width={w} height={h} className="absolute inset-0">
25
- <defs>
26
- <mask id={maskId} maskUnits="userSpaceOnUse">
27
- <path d={d} fill="white" />
28
- </mask>
29
- </defs>
30
-
31
- <path d={d} fill={background} />
32
- <path
33
- d={d}
34
- fill="none"
35
- stroke={borderColor}
36
- strokeWidth={borderWidth}
37
- mask={`url(#${maskId})`}
38
- />
39
- </svg>
40
- <div
41
- className={clsx(
42
- "flex size-full",
43
- valign === "top" && "items-start",
44
- valign === "center" && "items-center",
45
- valign === "bottom" && "items-end",
46
- halign === "left" && "justify-start text-left",
47
- halign === "center" && "justify-center text-center",
48
- halign === "right" && "justify-end text-right",
49
- halign === "justify" && "w-full text-justify",
50
- )}
51
- >
52
- <EditableContent
53
- isStatic={isStatic}
54
- node={node}
55
- className={clsx(halign === "justify" && "w-full")}
56
- />
57
- </div>
58
- </div>
59
- )
60
- }
@@ -1,49 +0,0 @@
1
- import clsx from "clsx"
2
- import { useStore } from "react-bolt"
3
- import { type EllipseNode } from "../../model/node/shape/ellipse"
4
- import { EditableContent } from "./EditableContent"
5
-
6
- export function EllipseContent(props: { node: EllipseNode; isStatic?: boolean }) {
7
- const { node, isStatic } = props
8
-
9
- const [valign, halign, background, borderWidth, borderColor] = useStore(
10
- node,
11
- "valign",
12
- "halign",
13
- "background",
14
- "borderWidth",
15
- "borderColor",
16
- )
17
-
18
- return (
19
- <div className="relative size-full">
20
- <div
21
- className="absolute inset-0 size-full"
22
- onDragStart={(e) => e.preventDefault()}
23
- style={{
24
- borderRadius: "50%",
25
- background,
26
- border: `${borderWidth}px solid ${borderColor}`,
27
- }}
28
- />
29
- <div
30
- className={clsx(
31
- "flex size-full",
32
- valign === "top" && "items-start",
33
- valign === "center" && "items-center",
34
- valign === "bottom" && "items-end",
35
- halign === "left" && "justify-start text-left",
36
- halign === "center" && "justify-center text-center",
37
- halign === "right" && "justify-end text-right",
38
- halign === "justify" && "w-full text-justify",
39
- )}
40
- >
41
- <EditableContent
42
- isStatic={isStatic}
43
- node={node}
44
- className={clsx(halign === "justify" && "w-full")}
45
- />
46
- </div>
47
- </div>
48
- )
49
- }
@@ -1,81 +0,0 @@
1
- import clsx from "clsx"
2
- import { useId } from "react"
3
- import { useStore } from "react-bolt"
4
- import { type PolygonNode } from "../../model/node/shape/polygon"
5
- import { EditableContent } from "./EditableContent"
6
-
7
- export function PolygonContent(props: { node: PolygonNode; isStatic?: boolean }) {
8
- const maskId = useId()
9
- const { node, isStatic } = props
10
-
11
- const [valign, halign, background, borderWidth, borderColor, sides] = useStore(
12
- node,
13
- "valign",
14
- "halign",
15
- "background",
16
- "borderWidth",
17
- "borderColor",
18
- "sides",
19
- )
20
-
21
- const [w, h, d] = useStore(node, "width", "height", "svgPathData")
22
-
23
- const radii = useStore(
24
- node,
25
- "cornerTopLeft",
26
- "cornerTopRight",
27
- "cornerBottomRight",
28
- "cornerBottomLeft",
29
- )
30
-
31
- return (
32
- <div className="relative size-full">
33
- {sides !== 4 ? (
34
- <svg width={w} height={h} className="absolute inset-0">
35
- <defs>
36
- <mask id={maskId} maskUnits="userSpaceOnUse">
37
- <path d={d} fill="white" />
38
- </mask>
39
- </defs>
40
-
41
- <path d={d} fill={background} />
42
- <path
43
- d={d}
44
- fill="none"
45
- stroke={borderColor}
46
- strokeWidth={borderWidth}
47
- mask={`url(#${maskId})`}
48
- />
49
- </svg>
50
- ) : (
51
- <div
52
- className="absolute inset-0 size-full"
53
- onDragStart={(e) => e.preventDefault()}
54
- style={{
55
- borderRadius: radii.map((r) => `${r}px`).join(" "),
56
- background: background,
57
- border: `${borderWidth}px solid ${borderColor}`,
58
- }}
59
- />
60
- )}
61
- <div
62
- className={clsx(
63
- "flex size-full",
64
- valign === "top" && "items-start",
65
- valign === "center" && "items-center",
66
- valign === "bottom" && "items-end",
67
- halign === "left" && "justify-start text-left",
68
- halign === "center" && "justify-center text-center",
69
- halign === "right" && "justify-end text-right",
70
- halign === "justify" && "w-full text-justify",
71
- )}
72
- >
73
- <EditableContent
74
- isStatic={isStatic}
75
- node={node}
76
- className={clsx(halign === "justify" && "w-full")}
77
- />
78
- </div>
79
- </div>
80
- )
81
- }
@@ -1,60 +0,0 @@
1
- import clsx from "clsx"
2
- import { useId } from "react"
3
- import { useStore } from "react-bolt"
4
- import { type StarNode } from "../../model/node/shape/star"
5
- import { EditableContent } from "./EditableContent"
6
-
7
- export function StarContent(props: { node: StarNode; isStatic?: boolean }) {
8
- const maskId = useId()
9
- const { node, isStatic } = props
10
-
11
- const [valign, halign, background, borderWidth, borderColor] = useStore(
12
- node,
13
- "valign",
14
- "halign",
15
- "background",
16
- "borderWidth",
17
- "borderColor",
18
- )
19
- const [w, h, d] = useStore(node, "width", "height", "svgPathData")
20
-
21
- return (
22
- <div className="relative size-full">
23
- <svg width={w} height={h} className="absolute inset-0">
24
- <defs>
25
- <mask id={maskId} maskUnits="userSpaceOnUse">
26
- <path d={d} fill="white" />
27
- </mask>
28
- </defs>
29
-
30
- <path d={d} fill={background} />
31
- <path
32
- d={d}
33
- fill="none"
34
- stroke={borderColor}
35
- strokeWidth={borderWidth}
36
- mask={`url(#${maskId})`}
37
- />
38
- </svg>
39
-
40
- <div
41
- className={clsx(
42
- "flex size-full",
43
- valign === "top" && "items-start",
44
- valign === "center" && "items-center",
45
- valign === "bottom" && "items-end",
46
- halign === "left" && "justify-start text-left",
47
- halign === "center" && "justify-center text-center",
48
- halign === "right" && "justify-end text-right",
49
- halign === "justify" && "w-full text-justify",
50
- )}
51
- >
52
- <EditableContent
53
- isStatic={isStatic}
54
- node={node}
55
- className={clsx(halign === "justify" && "w-full")}
56
- />
57
- </div>
58
- </div>
59
- )
60
- }