@lazlon-platform/html-editor 0.1.0 → 0.2.1
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/package.json +14 -11
- package/.claude/settings.local.json +0 -9
- package/.github/workflows/ci.yml +0 -34
- package/demo/App.tsx +0 -62
- package/demo/EditorView/PageView/NodeContent.tsx +0 -35
- package/demo/EditorView/PageView/SnapLines.tsx +0 -28
- package/demo/EditorView/PageView/index.tsx +0 -45
- package/demo/EditorView/SelectionFrame/Corner.tsx +0 -24
- package/demo/EditorView/SelectionFrame/Edge.tsx +0 -21
- package/demo/EditorView/SelectionFrame/index.tsx +0 -27
- package/demo/EditorView/SelectionOverlay/ActionHud.tsx +0 -32
- package/demo/EditorView/SelectionOverlay/Rotation.tsx +0 -39
- package/demo/EditorView/SelectionOverlay/Toolbar.tsx +0 -128
- package/demo/EditorView/SelectionOverlay/index.tsx +0 -21
- package/demo/EditorView/Toolbar/index.tsx +0 -68
- package/demo/EditorView/index.tsx +0 -47
- package/demo/Navbar/index.tsx +0 -33
- package/demo/Sidebar/index.tsx +0 -71
- package/demo/hotkeys.ts +0 -93
- package/demo/main.tsx +0 -10
- package/demo/style.css +0 -1
- package/eslint.config.js +0 -43
- package/index.html +0 -14
- package/tests/createTestEditor.ts +0 -19
- package/tests/hooks/actions.test.tsx +0 -736
- package/tests/hooks/batch.test.tsx +0 -332
- package/tests/hooks/editor.test.tsx +0 -56
- package/tests/hooks/page.test.tsx +0 -135
- package/tests/hooks/pointer/pointer.test.tsx +0 -244
- package/tests/hooks/textMarks.test.tsx +0 -624
- package/tests/model/editor.test.ts +0 -384
- package/tests/model/history.test.ts +0 -293
- package/tests/model/node/group.test.ts +0 -294
- package/tests/model/node/image.test.ts +0 -150
- package/tests/model/node/polygon.test.ts +0 -408
- package/tests/model/node/text.test.ts +0 -158
- package/tests/model/node.test.ts +0 -276
- package/tests/model/page.test.ts +0 -150
- package/tests/setup.ts +0 -7
- package/tsconfig.json +0 -28
- package/vite.config.ts +0 -9
- package/vitest.config.ts +0 -13
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from "vitest"
|
|
2
|
-
import { GroupNode } from "../../../lib/model/node/group"
|
|
3
|
-
import { Page } from "../../../lib/model/page"
|
|
4
|
-
import { createTestEditor } from "../../createTestEditor"
|
|
5
|
-
import { ImageNodeProps } from "../../../lib/model"
|
|
6
|
-
|
|
7
|
-
describe("GroupNode", () => {
|
|
8
|
-
let editor: ReturnType<typeof createTestEditor>
|
|
9
|
-
let page: Page
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
editor = createTestEditor()
|
|
13
|
-
page = new Page(editor, { id: "page-1" })
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe("construction", () => {
|
|
17
|
-
it("has name 'group'", () => {
|
|
18
|
-
const group = new GroupNode(editor, page, {
|
|
19
|
-
id: "group-1",
|
|
20
|
-
x: 0,
|
|
21
|
-
y: 0,
|
|
22
|
-
width: 200,
|
|
23
|
-
height: 200,
|
|
24
|
-
})
|
|
25
|
-
expect(group.name).toBe("group")
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it("initializes with empty nodes set", () => {
|
|
29
|
-
const group = new GroupNode(editor, page, {
|
|
30
|
-
id: "group-1",
|
|
31
|
-
x: 0,
|
|
32
|
-
y: 0,
|
|
33
|
-
width: 200,
|
|
34
|
-
height: 200,
|
|
35
|
-
})
|
|
36
|
-
expect(group.nodes.size).toBe(0)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it("deserializes child nodes from props", () => {
|
|
40
|
-
const group = new GroupNode(editor, page, {
|
|
41
|
-
id: "group-1",
|
|
42
|
-
x: 0,
|
|
43
|
-
y: 0,
|
|
44
|
-
width: 200,
|
|
45
|
-
height: 200,
|
|
46
|
-
nodes: [
|
|
47
|
-
{
|
|
48
|
-
name: "image",
|
|
49
|
-
props: { id: "child-1", x: 10, y: 10, width: 50, height: 50 },
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "image",
|
|
53
|
-
props: { id: "child-2", x: 70, y: 10, width: 50, height: 50 },
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
expect(group.nodes.size).toBe(2)
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe("proportional scaling", () => {
|
|
63
|
-
it("scales child x positions when width changes", () => {
|
|
64
|
-
const group = new GroupNode(editor, page, {
|
|
65
|
-
id: "group-1",
|
|
66
|
-
x: 0,
|
|
67
|
-
y: 0,
|
|
68
|
-
width: 100,
|
|
69
|
-
height: 100,
|
|
70
|
-
nodes: [
|
|
71
|
-
{ name: "image", props: { id: "child-1", x: 50, y: 0, width: 20, height: 20 } },
|
|
72
|
-
],
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const child = [...group.nodes][0]
|
|
76
|
-
expect(child.x).toBe(50) // 50% of 100
|
|
77
|
-
|
|
78
|
-
group.width = 200
|
|
79
|
-
|
|
80
|
-
expect(child.x).toBe(100) // 50% of 200
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it("scales child widths when group width changes", () => {
|
|
84
|
-
const group = new GroupNode(editor, page, {
|
|
85
|
-
id: "group-1",
|
|
86
|
-
x: 0,
|
|
87
|
-
y: 0,
|
|
88
|
-
width: 100,
|
|
89
|
-
height: 100,
|
|
90
|
-
nodes: [
|
|
91
|
-
{ name: "image", props: { id: "child-1", x: 0, y: 0, width: 50, height: 20 } },
|
|
92
|
-
],
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
const child = [...group.nodes][0]
|
|
96
|
-
expect(child.width).toBe(50) // 50% of 100
|
|
97
|
-
|
|
98
|
-
group.width = 200
|
|
99
|
-
|
|
100
|
-
expect(child.width).toBe(100) // 50% of 200
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it("scales child y positions when height changes", () => {
|
|
104
|
-
const group = new GroupNode(editor, page, {
|
|
105
|
-
id: "group-1",
|
|
106
|
-
x: 0,
|
|
107
|
-
y: 0,
|
|
108
|
-
width: 100,
|
|
109
|
-
height: 100,
|
|
110
|
-
nodes: [
|
|
111
|
-
{ name: "image", props: { id: "child-1", x: 0, y: 25, width: 20, height: 20 } },
|
|
112
|
-
],
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
const child = [...group.nodes][0]
|
|
116
|
-
expect(child.y).toBe(25) // 25% of 100
|
|
117
|
-
|
|
118
|
-
group.height = 200
|
|
119
|
-
|
|
120
|
-
expect(child.y).toBe(50) // 25% of 200
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it("scales child heights when group height changes", () => {
|
|
124
|
-
const group = new GroupNode(editor, page, {
|
|
125
|
-
id: "group-1",
|
|
126
|
-
x: 0,
|
|
127
|
-
y: 0,
|
|
128
|
-
width: 100,
|
|
129
|
-
height: 100,
|
|
130
|
-
nodes: [
|
|
131
|
-
{ name: "image", props: { id: "child-1", x: 0, y: 0, width: 20, height: 40 } },
|
|
132
|
-
],
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const child = [...group.nodes][0]
|
|
136
|
-
expect(child.height).toBe(40) // 40% of 100
|
|
137
|
-
|
|
138
|
-
group.height = 200
|
|
139
|
-
|
|
140
|
-
expect(child.height).toBe(80) // 40% of 200
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it("scales all children proportionally", () => {
|
|
144
|
-
const group = new GroupNode(editor, page, {
|
|
145
|
-
id: "group-1",
|
|
146
|
-
x: 0,
|
|
147
|
-
y: 0,
|
|
148
|
-
width: 200,
|
|
149
|
-
height: 200,
|
|
150
|
-
nodes: [
|
|
151
|
-
{
|
|
152
|
-
name: "image",
|
|
153
|
-
props: { id: "child-1", x: 0, y: 0, width: 100, height: 100 },
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "image",
|
|
157
|
-
props: { id: "child-2", x: 100, y: 100, width: 100, height: 100 },
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
const [child1, child2] = [...group.nodes]
|
|
163
|
-
|
|
164
|
-
group.width = 400
|
|
165
|
-
group.height = 400
|
|
166
|
-
|
|
167
|
-
// Both children should scale proportionally
|
|
168
|
-
expect(child1.x).toBe(0)
|
|
169
|
-
expect(child1.y).toBe(0)
|
|
170
|
-
expect(child1.width).toBe(200)
|
|
171
|
-
expect(child1.height).toBe(200)
|
|
172
|
-
|
|
173
|
-
expect(child2.x).toBe(200)
|
|
174
|
-
expect(child2.y).toBe(200)
|
|
175
|
-
expect(child2.width).toBe(200)
|
|
176
|
-
expect(child2.height).toBe(200)
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
describe("serialization", () => {
|
|
181
|
-
it("includes serialized child nodes", () => {
|
|
182
|
-
const group = new GroupNode(editor, page, {
|
|
183
|
-
id: "group-1",
|
|
184
|
-
x: 0,
|
|
185
|
-
y: 0,
|
|
186
|
-
width: 200,
|
|
187
|
-
height: 200,
|
|
188
|
-
nodes: [
|
|
189
|
-
{
|
|
190
|
-
name: "image",
|
|
191
|
-
props: { id: "child-1", x: 10, y: 10, width: 50, height: 50 },
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const serialized = group.serialize()
|
|
197
|
-
|
|
198
|
-
expect(serialized.name).toBe("group")
|
|
199
|
-
expect(serialized.props.nodes).toHaveLength(1)
|
|
200
|
-
expect(serialized.props.nodes![0].name).toBe("image")
|
|
201
|
-
expect(serialized.props.nodes![0].props.id).toBe("child-1")
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it("serializes empty group", () => {
|
|
205
|
-
const group = new GroupNode(editor, page, {
|
|
206
|
-
id: "group-1",
|
|
207
|
-
x: 0,
|
|
208
|
-
y: 0,
|
|
209
|
-
width: 200,
|
|
210
|
-
height: 200,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const serialized = group.serialize()
|
|
214
|
-
|
|
215
|
-
expect(serialized.props.nodes).toEqual([])
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
describe("deserialization round-trip", () => {
|
|
220
|
-
it("produces identical props after serialize -> deserialize -> serialize", () => {
|
|
221
|
-
const imageProps1: ImageNodeProps = {
|
|
222
|
-
id: "child-1",
|
|
223
|
-
x: 10,
|
|
224
|
-
y: 10,
|
|
225
|
-
width: 100,
|
|
226
|
-
height: 80,
|
|
227
|
-
url: "https://example.com/img1.jpg",
|
|
228
|
-
}
|
|
229
|
-
const imageProps2: ImageNodeProps = {
|
|
230
|
-
id: "child-2",
|
|
231
|
-
x: 120,
|
|
232
|
-
y: 10,
|
|
233
|
-
width: 100,
|
|
234
|
-
height: 80,
|
|
235
|
-
url: "https://example.com/img2.jpg",
|
|
236
|
-
}
|
|
237
|
-
const original = new GroupNode(editor, page, {
|
|
238
|
-
id: "group-1",
|
|
239
|
-
x: 50,
|
|
240
|
-
y: 100,
|
|
241
|
-
width: 300,
|
|
242
|
-
height: 250,
|
|
243
|
-
rotation: 15,
|
|
244
|
-
nodes: [
|
|
245
|
-
{ name: "image", props: imageProps1 },
|
|
246
|
-
{ name: "image", props: imageProps2 },
|
|
247
|
-
],
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
const firstSerialized = original.serialize()
|
|
251
|
-
const restored = editor.deserializeNode(page, firstSerialized) as GroupNode
|
|
252
|
-
const secondSerialized = restored.serialize()
|
|
253
|
-
|
|
254
|
-
expect(secondSerialized).toEqual(firstSerialized)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it("preserves nested groups through round-trip", () => {
|
|
258
|
-
const groupProps = {
|
|
259
|
-
id: "inner-group",
|
|
260
|
-
x: 50,
|
|
261
|
-
y: 50,
|
|
262
|
-
width: 200,
|
|
263
|
-
height: 200,
|
|
264
|
-
nodes: [
|
|
265
|
-
{
|
|
266
|
-
name: "image",
|
|
267
|
-
props: { id: "deep-child", x: 10, y: 10, width: 50, height: 50 },
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const original = new GroupNode(editor, page, {
|
|
273
|
-
id: "outer-group",
|
|
274
|
-
x: 0,
|
|
275
|
-
y: 0,
|
|
276
|
-
width: 400,
|
|
277
|
-
height: 400,
|
|
278
|
-
nodes: [{ name: "group", props: groupProps }],
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
const firstSerialized = original.serialize()
|
|
282
|
-
const restored = editor.deserializeNode(page, firstSerialized) as GroupNode
|
|
283
|
-
const secondSerialized = restored.serialize()
|
|
284
|
-
|
|
285
|
-
expect(secondSerialized).toEqual(firstSerialized)
|
|
286
|
-
|
|
287
|
-
// Verify nested structure
|
|
288
|
-
expect(restored.nodes.size).toBe(1)
|
|
289
|
-
const innerGroup = [...restored.nodes][0] as GroupNode
|
|
290
|
-
expect(innerGroup.name).toBe("group")
|
|
291
|
-
expect(innerGroup.nodes.size).toBe(1)
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
})
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest"
|
|
2
|
-
import { Page } from "../../../lib/model/page"
|
|
3
|
-
import { ImageNode } from "../../../lib/model/node/image"
|
|
4
|
-
import { createTestEditor } from "../../createTestEditor"
|
|
5
|
-
|
|
6
|
-
describe("ImageNode", () => {
|
|
7
|
-
let editor: ReturnType<typeof createTestEditor>
|
|
8
|
-
let page: Page
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
editor = createTestEditor()
|
|
12
|
-
page = new Page(editor, { id: "page-1" })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
describe("construction", () => {
|
|
16
|
-
it("has name 'image'", () => {
|
|
17
|
-
const node = new ImageNode(editor, page, {
|
|
18
|
-
id: "img-1",
|
|
19
|
-
x: 0,
|
|
20
|
-
y: 0,
|
|
21
|
-
width: 100,
|
|
22
|
-
height: 100,
|
|
23
|
-
})
|
|
24
|
-
expect(node.name).toBe("image")
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it("applies default values", () => {
|
|
28
|
-
const node = new ImageNode(editor, page, {
|
|
29
|
-
id: "img-1",
|
|
30
|
-
x: 0,
|
|
31
|
-
y: 0,
|
|
32
|
-
width: 100,
|
|
33
|
-
height: 100,
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
expect(node.url).toBeNull()
|
|
37
|
-
expect(node.fit).toBe("cover")
|
|
38
|
-
expect(node.roundness).toBe(0)
|
|
39
|
-
expect(node.borderWidth).toBe(0)
|
|
40
|
-
expect(node.borderColor).toBe("#000000")
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it("accepts custom properties", () => {
|
|
44
|
-
const node = new ImageNode(editor, page, {
|
|
45
|
-
id: "img-1",
|
|
46
|
-
x: 0,
|
|
47
|
-
y: 0,
|
|
48
|
-
width: 100,
|
|
49
|
-
height: 100,
|
|
50
|
-
url: "https://example.com/image.png",
|
|
51
|
-
fit: "contain",
|
|
52
|
-
roundness: 20,
|
|
53
|
-
borderWidth: 3,
|
|
54
|
-
borderColor: "#ff0000",
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
expect(node.url).toBe("https://example.com/image.png")
|
|
58
|
-
// Note: fit is hardcoded to "cover" in constructor (bug?)
|
|
59
|
-
expect(node.roundness).toBe(20)
|
|
60
|
-
expect(node.borderWidth).toBe(3)
|
|
61
|
-
expect(node.borderColor).toBe("#ff0000")
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
describe("serialization", () => {
|
|
66
|
-
it("includes all ImageNode-specific properties", () => {
|
|
67
|
-
const node = new ImageNode(editor, page, {
|
|
68
|
-
id: "img-1",
|
|
69
|
-
x: 10,
|
|
70
|
-
y: 20,
|
|
71
|
-
width: 200,
|
|
72
|
-
height: 150,
|
|
73
|
-
url: "https://example.com/photo.jpg",
|
|
74
|
-
fit: "fill",
|
|
75
|
-
roundness: 15,
|
|
76
|
-
borderWidth: 2,
|
|
77
|
-
borderColor: "#333333",
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const serialized = node.serialize()
|
|
81
|
-
|
|
82
|
-
expect(serialized.name).toBe("image")
|
|
83
|
-
expect(serialized.props).toMatchObject({
|
|
84
|
-
id: "img-1",
|
|
85
|
-
x: 10,
|
|
86
|
-
y: 20,
|
|
87
|
-
width: 200,
|
|
88
|
-
height: 150,
|
|
89
|
-
url: "https://example.com/photo.jpg",
|
|
90
|
-
roundness: 15,
|
|
91
|
-
borderWidth: 2,
|
|
92
|
-
borderColor: "#333333",
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it("serializes null url", () => {
|
|
97
|
-
const node = new ImageNode(editor, page, {
|
|
98
|
-
id: "img-1",
|
|
99
|
-
x: 0,
|
|
100
|
-
y: 0,
|
|
101
|
-
width: 100,
|
|
102
|
-
height: 100,
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const props = node.props()
|
|
106
|
-
expect(props.url).toBeNull()
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe("deserialization round-trip", () => {
|
|
111
|
-
it("produces identical props after serialize -> deserialize -> serialize", () => {
|
|
112
|
-
const original = new ImageNode(editor, page, {
|
|
113
|
-
id: "img-1",
|
|
114
|
-
x: 50,
|
|
115
|
-
y: 100,
|
|
116
|
-
width: 300,
|
|
117
|
-
height: 200,
|
|
118
|
-
url: "https://cdn.example.com/images/photo.jpg",
|
|
119
|
-
fit: "cover",
|
|
120
|
-
roundness: 25,
|
|
121
|
-
borderWidth: 4,
|
|
122
|
-
borderColor: "#00ff00",
|
|
123
|
-
rotation: 45,
|
|
124
|
-
locked: true,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const firstSerialized = original.serialize()
|
|
128
|
-
const restored = editor.deserializeNode(page, firstSerialized) as ImageNode
|
|
129
|
-
const secondSerialized = restored.serialize()
|
|
130
|
-
|
|
131
|
-
expect(secondSerialized).toEqual(firstSerialized)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it("preserves default values through round-trip", () => {
|
|
135
|
-
const original = new ImageNode(editor, page, {
|
|
136
|
-
id: "img-1",
|
|
137
|
-
x: 0,
|
|
138
|
-
y: 0,
|
|
139
|
-
width: 100,
|
|
140
|
-
height: 100,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const firstSerialized = original.serialize()
|
|
144
|
-
const restored = editor.deserializeNode(page, firstSerialized) as ImageNode
|
|
145
|
-
const secondSerialized = restored.serialize()
|
|
146
|
-
|
|
147
|
-
expect(secondSerialized).toEqual(firstSerialized)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
})
|