@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react"
|
|
2
2
|
import { useComputed, useStore } from "react-bolt"
|
|
3
|
-
import {
|
|
3
|
+
import { pointSubtract } from "../../model/geometry/math"
|
|
4
|
+
import { selectionDOMRect } from "../../ui/selection"
|
|
4
5
|
import { useEditor } from "../editor"
|
|
5
6
|
|
|
6
7
|
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
|
@@ -73,23 +74,19 @@ export function useSelectionFrame<E extends HTMLElement>(props?: {
|
|
|
73
74
|
|
|
74
75
|
if (props?.accountForSingleSelection && selection.size === 1 && frame) {
|
|
75
76
|
const [firstNode] = selection
|
|
76
|
-
const { page } = firstNode
|
|
77
|
-
|
|
78
77
|
const { x, y, width, height, rotation } = firstNode
|
|
79
|
-
const relative = page.ref!.getBoundingClientRect()
|
|
78
|
+
const relative = firstNode.page.ref!.getBoundingClientRect()
|
|
80
79
|
const tx = relative.x - stage.x + x * zoom
|
|
81
80
|
const ty = relative.y - stage.y + y * zoom
|
|
82
|
-
|
|
83
81
|
frame.style.height = `${height * zoom}px`
|
|
84
82
|
frame.style.width = `${width * zoom}px`
|
|
85
83
|
frame.style.transform = `translate(${tx}px, ${ty}px) rotate(${rotation}deg)`
|
|
86
84
|
} else if (frame) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
frame.style.
|
|
91
|
-
frame.style.
|
|
92
|
-
frame.style.transform = `translate(${tx}px, ${ty}px)`
|
|
85
|
+
const dom = selectionDOMRect(selection)
|
|
86
|
+
const t = pointSubtract(dom, stage)
|
|
87
|
+
frame.style.height = `${dom.height}px`
|
|
88
|
+
frame.style.width = `${dom.width}px`
|
|
89
|
+
frame.style.transform = `translate(${t.x}px, ${t.y}px)`
|
|
93
90
|
}
|
|
94
91
|
})
|
|
95
92
|
|
|
@@ -1,53 +1,78 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
type Rect,
|
|
4
|
+
box,
|
|
5
|
+
boxContainsPoint,
|
|
6
|
+
pointSubtract,
|
|
7
|
+
rect,
|
|
8
|
+
boxIntersects,
|
|
9
|
+
lineContainsPoint,
|
|
10
|
+
lineIntersectsBox,
|
|
11
|
+
accessibleLine,
|
|
12
|
+
} from "../../model/geometry/math"
|
|
2
13
|
import { useEditor } from "../editor"
|
|
3
|
-
import { usePointer } from "./pointer"
|
|
14
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
15
|
+
import { LineNode } from "../../model"
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
function clientPoint(event: { clientX: number; clientY: number }) {
|
|
18
|
+
return { x: event.clientX, y: event.clientY }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useSelector(view: (props: null | Rect) => void) {
|
|
6
22
|
const editor = useEditor()
|
|
7
23
|
|
|
24
|
+
const dragAnchor = useRef({
|
|
25
|
+
clientX: 0,
|
|
26
|
+
clientY: 0,
|
|
27
|
+
})
|
|
28
|
+
|
|
8
29
|
return usePointer({
|
|
9
30
|
onDown(event) {
|
|
10
|
-
const
|
|
31
|
+
for (const node of editor.nodes.values()) {
|
|
32
|
+
const cursor = cursorPosition(event, node.page)
|
|
33
|
+
if (
|
|
34
|
+
(node instanceof LineNode && lineContainsPoint(accessibleLine(node), cursor)) ||
|
|
35
|
+
(!(node instanceof LineNode) && boxContainsPoint(box(node), cursor))
|
|
36
|
+
) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
11
40
|
|
|
12
|
-
if (!
|
|
41
|
+
if (!editor.action.action) {
|
|
13
42
|
editor.action = { action: "select" }
|
|
43
|
+
dragAnchor.current = event
|
|
14
44
|
}
|
|
15
|
-
|
|
16
|
-
return !isPointerInsideRect
|
|
17
45
|
},
|
|
18
|
-
onMove(
|
|
19
|
-
const div = props.ref.current
|
|
20
|
-
if (!div) throw Error("selector div ref is null")
|
|
46
|
+
onMove(event) {
|
|
21
47
|
if (editor.action.action !== "select") return
|
|
48
|
+
const stage = editor.ref!.getBoundingClientRect()
|
|
22
49
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const height = Math.floor(Math.abs(y))
|
|
29
|
-
const tx = (x > 0 ? start.clientX : start.clientX - width) - rect.x
|
|
30
|
-
const ty = (y > 0 ? start.clientY : start.clientY - height) - rect.y
|
|
31
|
-
|
|
32
|
-
const { left, right, top, bottom } = div.getBoundingClientRect()
|
|
50
|
+
/* world view */ {
|
|
51
|
+
const p1 = clientPoint(dragAnchor.current)
|
|
52
|
+
const p2 = clientPoint(event)
|
|
53
|
+
view(rect(pointSubtract(p1, stage), pointSubtract(p2, stage)))
|
|
54
|
+
}
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
const selection = editor.pages
|
|
57
|
+
.values()
|
|
58
|
+
.toArray()
|
|
59
|
+
.flatMap((page) => {
|
|
60
|
+
const p1 = cursorPosition(dragAnchor.current, page)
|
|
61
|
+
const p2 = cursorPosition(event, page)
|
|
62
|
+
const selection = box(rect(p1, p2))
|
|
63
|
+
return page.nodes
|
|
64
|
+
.values()
|
|
65
|
+
.toArray()
|
|
66
|
+
.filter((node) => {
|
|
67
|
+
if (node instanceof LineNode) {
|
|
68
|
+
return lineIntersectsBox(node, selection)
|
|
69
|
+
} else {
|
|
70
|
+
return boxIntersects(selection, box(node))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
div.style.height = `${height}px`
|
|
49
|
-
div.style.transform = `translate(${tx}px, ${ty}px)`
|
|
50
|
-
div.style.display = "block"
|
|
75
|
+
editor.selection = new Set(selection)
|
|
51
76
|
},
|
|
52
77
|
onCancel() {
|
|
53
78
|
editor.selection = new Set() // click away
|
|
@@ -55,10 +80,7 @@ export function useSelector(props: { ref: React.RefObject<HTMLDivElement | null>
|
|
|
55
80
|
},
|
|
56
81
|
onEnd() {
|
|
57
82
|
editor.action = {}
|
|
58
|
-
|
|
59
|
-
const div = props.ref.current
|
|
60
|
-
if (!div) throw Error("selector div ref is null")
|
|
61
|
-
div.style.display = "none"
|
|
83
|
+
view(null)
|
|
62
84
|
},
|
|
63
85
|
})
|
|
64
86
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Rect, box, boxBounds, rect } from "../../model/geometry/math"
|
|
2
2
|
import type { Node } from "../../model/node"
|
|
3
3
|
import type { Page } from "../../model/page"
|
|
4
|
-
import { useEditor
|
|
4
|
+
import { useEditor } from "../editor"
|
|
5
5
|
|
|
6
6
|
type Lines = Page["snapLines"]
|
|
7
7
|
const isN = (n?: number) => typeof n === "number"
|
|
8
8
|
|
|
9
|
-
export function useSnap() {
|
|
10
|
-
const page = usePage()
|
|
9
|
+
export function useSnap(page: Page) {
|
|
11
10
|
const editor = useEditor()
|
|
11
|
+
|
|
12
12
|
const threshold = editor.options.snapThreshold
|
|
13
13
|
|
|
14
14
|
function shouldSnap(point: number) {
|
|
@@ -17,13 +17,13 @@ export function useSnap() {
|
|
|
17
17
|
|
|
18
18
|
function ySnap(nodes: Node[], rect: Rect): [number, ...Lines] {
|
|
19
19
|
const nodelines = nodes.flatMap((n) => {
|
|
20
|
-
const { y, height } = n
|
|
20
|
+
const { y, height } = boxBounds(box(n))
|
|
21
21
|
return [y, y + height]
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
const hlines = [0, page.height, ...nodelines]
|
|
25
|
-
const top = hlines.find(shouldSnap(rect.
|
|
26
|
-
const bottom = hlines.find(shouldSnap(rect.
|
|
25
|
+
const top = hlines.find(shouldSnap(rect.top))
|
|
26
|
+
const bottom = hlines.find(shouldSnap(rect.bottom))
|
|
27
27
|
|
|
28
28
|
if (isN(top) && isN(bottom)) {
|
|
29
29
|
return [
|
|
@@ -46,15 +46,15 @@ export function useSnap() {
|
|
|
46
46
|
return [rect.y]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function xSnap(nodes: Node[],
|
|
49
|
+
function xSnap(nodes: Node[], rect: Rect): [number, ...Lines] {
|
|
50
50
|
const nodelines = nodes.flatMap((n) => {
|
|
51
|
-
const { x, width } = n
|
|
51
|
+
const { x, width } = boxBounds(box(n))
|
|
52
52
|
return [x, x + width]
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
const hlines = [0, page.width, ...nodelines]
|
|
56
|
-
const left = hlines.find(shouldSnap(
|
|
57
|
-
const right = hlines.find(shouldSnap(
|
|
56
|
+
const left = hlines.find(shouldSnap(rect.left))
|
|
57
|
+
const right = hlines.find(shouldSnap(rect.right))
|
|
58
58
|
|
|
59
59
|
if (isN(left) && isN(right)) {
|
|
60
60
|
return [left, { x: left - 1 }, { x: right }]
|
|
@@ -65,16 +65,16 @@ export function useSnap() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (isN(right)) {
|
|
68
|
-
return [right -
|
|
68
|
+
return [right - rect.width, { x: right }]
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
return [
|
|
71
|
+
return [rect.x]
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
return function snap(snap: boolean,
|
|
74
|
+
return function snap(snap: boolean, r: Rect): Rect {
|
|
75
75
|
if (!snap) {
|
|
76
76
|
page.snapLines = []
|
|
77
|
-
return
|
|
77
|
+
return r
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const nodes = page.nodes
|
|
@@ -82,16 +82,16 @@ export function useSnap() {
|
|
|
82
82
|
.filter((node) => !editor.selection.has(node))
|
|
83
83
|
.toArray()
|
|
84
84
|
|
|
85
|
-
const [
|
|
86
|
-
const [
|
|
85
|
+
const [x, ...hlines] = xSnap(nodes, r)
|
|
86
|
+
const [y, ...vlines] = ySnap(nodes, r)
|
|
87
87
|
|
|
88
88
|
page.snapLines = [...vlines, ...hlines]
|
|
89
89
|
|
|
90
|
-
return {
|
|
91
|
-
x
|
|
92
|
-
y
|
|
93
|
-
width:
|
|
94
|
-
height:
|
|
95
|
-
}
|
|
90
|
+
return rect({
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
93
|
+
width: r.width,
|
|
94
|
+
height: r.height,
|
|
95
|
+
})
|
|
96
96
|
}
|
|
97
97
|
}
|
package/lib/hooks/textMarks.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useCallback, useRef, useSyncExternalStore } from "react"
|
|
3
3
|
import { effect, useComputed } from "react-bolt"
|
|
4
|
-
import { TextNode } from "../model"
|
|
5
|
-
import { EditableNode } from "../model/node/editable"
|
|
6
|
-
import { FormattableNode } from "../model/node/formattable"
|
|
4
|
+
import { TextNode, EditableNode, FormattableNode } from "../model"
|
|
7
5
|
import { useBatchSet, useNodeFieldBatch } from "./batch"
|
|
8
6
|
|
|
9
7
|
function mergeField<T>(values: T[]): T | null {
|
package/lib/lib/googleFonts.ts
CHANGED
|
@@ -55,8 +55,6 @@ export type GoogleWebfont = {
|
|
|
55
55
|
category: FontCategory
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const variants: Variant[] = ["regular", "italic", "700", "700italic"]
|
|
59
|
-
|
|
60
58
|
// server only
|
|
61
59
|
export async function getGoogleFonts(
|
|
62
60
|
key: string,
|
|
@@ -81,9 +79,7 @@ export async function getGoogleFonts(
|
|
|
81
79
|
const res = await fetch(url)
|
|
82
80
|
const { items = [] } = (await res.json()) as { items?: GoogleWebfont[] }
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
const result = items.filter((font) => variants.some((v) => font.variants.includes(v)))
|
|
86
|
-
return sort ? result : result.sort()
|
|
82
|
+
return items
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
export function getLink(font: GoogleWebfont) {
|
package/lib/model/editor.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { computed, createStore, state } from "react-bolt"
|
|
2
2
|
import type { GoogleWebfont } from "../lib/googleFonts"
|
|
3
3
|
import { HistoryStore, type HistoryEntry } from "./history"
|
|
4
|
-
import type { Node,
|
|
4
|
+
import type { Node, SerializedNode } from "./node"
|
|
5
5
|
import { Page, type SerializedPage } from "./page"
|
|
6
6
|
|
|
7
7
|
const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
8
8
|
|
|
9
|
-
export interface NodeConstructor<
|
|
10
|
-
prototype:
|
|
11
|
-
new (
|
|
9
|
+
export interface NodeConstructor<N extends Node = Node> {
|
|
10
|
+
prototype: N
|
|
11
|
+
new (page: Page, props: N["props"]): Node
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface SerializedEditor {
|
|
@@ -144,13 +144,17 @@ export class Editor {
|
|
|
144
144
|
)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
deserializeNode<T extends
|
|
148
|
-
page: Page,
|
|
149
|
-
{ name, props }: SerializedNode<string, T>,
|
|
150
|
-
): Node {
|
|
147
|
+
deserializeNode<T extends Node>(page: Page, { name, props }: SerializedNode<T>): T {
|
|
151
148
|
const NodeClass = this.#schema.get(name)
|
|
152
149
|
if (!NodeClass) throw Error(`cannot deserialize unknown Node: ${name}`)
|
|
153
|
-
return new NodeClass(
|
|
150
|
+
return new NodeClass(page, props) as T
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
serializeNode<T extends Node>(node: T): SerializedNode<T> {
|
|
154
|
+
return {
|
|
155
|
+
name: node.name,
|
|
156
|
+
props: node.props,
|
|
157
|
+
}
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
serialize(): SerializedEditor {
|