@lazlon-platform/html-editor 0.5.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 +51 -24
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +22 -8
- package/lib/hooks/pointer/pointer.ts +2 -3
- package/lib/hooks/pointer/resize.ts +2 -2
- package/lib/hooks/pointer/rotation.ts +74 -39
- package/lib/hooks/pointer/selector.ts +16 -2
- package/lib/hooks/pointer/snap.ts +3 -3
- 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 +139 -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} +6 -12
- package/lib/model/node.ts +9 -23
- package/lib/model/page.ts +1 -2
- 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/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/package.json +1 -1
- 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
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 {
|
|
@@ -20,6 +20,11 @@ export interface Rect extends Point, Size {
|
|
|
20
20
|
readonly right: number
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface Line extends Point {
|
|
24
|
+
readonly points: Point[]
|
|
25
|
+
readonly strokeWidth: number
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* A rotated rectangle around a pivot point.
|
|
25
30
|
* Pivot point is relative to the center point.
|
|
@@ -349,6 +354,140 @@ export function boxContainsPoint(target: Box, point: Point): boolean {
|
|
|
349
354
|
)
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
function pointSegmentDistance(target: Point, a: Point, b: Point): number {
|
|
358
|
+
const segment = pointSubtract(b, a)
|
|
359
|
+
const lengthSq = vectorDotProd(segment, segment)
|
|
360
|
+
|
|
361
|
+
if (lengthSq === 0) {
|
|
362
|
+
return pointDist(target, a)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const fromStart = pointSubtract(target, a)
|
|
366
|
+
const projection = clamp(vectorDotProd(fromStart, segment) / lengthSq, 0, 1)
|
|
367
|
+
const closest = pointAdd(a, pointMultiply(segment, projection))
|
|
368
|
+
|
|
369
|
+
return pointDist(target, closest)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function segmentCross(a: Point, b: Point, c: Point): number {
|
|
373
|
+
return floatNorm((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function pointOnSegment(target: Point, a: Point, b: Point): boolean {
|
|
377
|
+
const x = floatNorm(target.x)
|
|
378
|
+
const y = floatNorm(target.y)
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
segmentCross(a, b, target) === 0 &&
|
|
382
|
+
x >= floatNorm(Math.min(a.x, b.x)) &&
|
|
383
|
+
x <= floatNorm(Math.max(a.x, b.x)) &&
|
|
384
|
+
y >= floatNorm(Math.min(a.y, b.y)) &&
|
|
385
|
+
y <= floatNorm(Math.max(a.y, b.y))
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
390
|
+
const abc = segmentCross(a, b, c)
|
|
391
|
+
const abd = segmentCross(a, b, d)
|
|
392
|
+
const cda = segmentCross(c, d, a)
|
|
393
|
+
const cdb = segmentCross(c, d, b)
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
(abc === 0 && pointOnSegment(c, a, b)) ||
|
|
397
|
+
(abd === 0 && pointOnSegment(d, a, b)) ||
|
|
398
|
+
(cda === 0 && pointOnSegment(a, c, d)) ||
|
|
399
|
+
(cdb === 0 && pointOnSegment(b, c, d)) ||
|
|
400
|
+
(abc * abd < 0 && cda * cdb < 0)
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function segmentDistance(a: Point, b: Point, c: Point, d: Point): number {
|
|
405
|
+
if (segmentsIntersect(a, b, c, d)) return 0
|
|
406
|
+
|
|
407
|
+
return Math.min(
|
|
408
|
+
pointSegmentDistance(a, c, d),
|
|
409
|
+
pointSegmentDistance(b, c, d),
|
|
410
|
+
pointSegmentDistance(c, a, b),
|
|
411
|
+
pointSegmentDistance(d, a, b),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function linePoints(line: Line): Point[] {
|
|
416
|
+
return line.points.map((p) => pointAdd(line, p))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @returns Whether point is contained in the stroked polyline.
|
|
421
|
+
*/
|
|
422
|
+
export function lineContainsPoint(line: Line, point: Point): boolean {
|
|
423
|
+
const points = linePoints(line)
|
|
424
|
+
const radius = line.strokeWidth / 2
|
|
425
|
+
|
|
426
|
+
if (points.length === 1) {
|
|
427
|
+
return pointDist(point, points[0]) <= radius
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
431
|
+
const a = points[i]
|
|
432
|
+
const b = points[i + 1]
|
|
433
|
+
|
|
434
|
+
if (pointSegmentDistance(point, a, b) <= radius) return true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return false
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @returns Whether the stroked polyline intersects the box.
|
|
442
|
+
*/
|
|
443
|
+
export function lineIntersectsBox(line: Line, box: Box): boolean {
|
|
444
|
+
const points = linePoints(line)
|
|
445
|
+
|
|
446
|
+
if (points.length === 0) return false
|
|
447
|
+
|
|
448
|
+
for (const point of points) {
|
|
449
|
+
if (boxContainsPoint(box, point)) return true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const radius = line.strokeWidth / 2
|
|
453
|
+
const corners = boxCorners(box)
|
|
454
|
+
const edges = corners.map(
|
|
455
|
+
(corner, i) => [corner, corners[(i + 1) % corners.length]] as const,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
for (const corner of corners) {
|
|
459
|
+
if (lineContainsPoint(line, corner)) return true
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (points.length === 1) {
|
|
463
|
+
return edges.some(([a, b]) => pointSegmentDistance(points[0], a, b) <= radius)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
467
|
+
const a = points[i]
|
|
468
|
+
const b = points[i + 1]
|
|
469
|
+
|
|
470
|
+
for (const [c, d] of edges) {
|
|
471
|
+
if (segmentDistance(a, b, c, d) <= radius) return true
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return false
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @returns Line with increased strokeWidth for accessibility reasons.
|
|
480
|
+
*/
|
|
481
|
+
export function accessibleLine(line: Line): Line {
|
|
482
|
+
const { x, y, strokeWidth, points } = line
|
|
483
|
+
return {
|
|
484
|
+
x,
|
|
485
|
+
y,
|
|
486
|
+
points,
|
|
487
|
+
strokeWidth: Math.max(18, strokeWidth),
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
352
491
|
/**
|
|
353
492
|
* @returns Four corners of a box.
|
|
354
493
|
* ⠀⠀ ●
|
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
|
}
|