@lazlon-platform/html-editor 0.3.6 → 0.4.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/batch.ts +3 -2
- package/lib/hooks/textMarks.ts +67 -50
- package/lib/model/node/text.ts +14 -9
- package/lib/ui/node/TextContent.tsx +13 -16
- package/package.json +1 -1
package/lib/hooks/batch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEqual
|
|
1
|
+
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useRef } from "react"
|
|
3
3
|
import { useComputed } from "react-bolt"
|
|
4
4
|
import { type HistoryAction } from "../model/history"
|
|
@@ -90,7 +90,8 @@ export function useBatchSet() {
|
|
|
90
90
|
push = true
|
|
91
91
|
const nextValues = typeof set === "function" ? set(node) : set
|
|
92
92
|
const keys = Object.keys(nextValues) as WritableKeys<N>[]
|
|
93
|
-
|
|
93
|
+
const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
|
|
94
|
+
prev.push(["set-node-props", [node.id, prevValues]])
|
|
94
95
|
next.push(["set-node-props", [node.id, nextValues]])
|
|
95
96
|
Object.assign(node, nextValues)
|
|
96
97
|
}
|
package/lib/hooks/textMarks.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isEqual } from "es-toolkit"
|
|
2
|
-
import {
|
|
3
|
-
import { useComputed } from "react-bolt"
|
|
2
|
+
import { useCallback, useRef, useSyncExternalStore } from "react"
|
|
3
|
+
import { effect, useComputed } from "react-bolt"
|
|
4
4
|
import { TextNode } from "../model"
|
|
5
5
|
import { EditableNode } from "../model/node/editable"
|
|
6
6
|
import { FormattableNode } from "../model/node/formattable"
|
|
7
|
-
import { useBatchSet,
|
|
7
|
+
import { useBatchSet, useNodeFieldBatch } from "./batch"
|
|
8
8
|
|
|
9
9
|
function mergeField<T>(values: T[]): T | null {
|
|
10
10
|
if (values.length === 0) return null
|
|
@@ -12,14 +12,17 @@ function mergeField<T>(values: T[]): T | null {
|
|
|
12
12
|
return rest.some((v) => v !== first) ? null : first
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
15
|
+
function editableProps(node: EditableNode) {
|
|
16
16
|
const e = node.tiptap
|
|
17
17
|
const { color, fontSize, fontFamily, letterSpacing, lineHeight } =
|
|
18
18
|
e.getAttributes("textStyle")
|
|
19
19
|
|
|
20
|
-
const size =
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const size =
|
|
21
|
+
node instanceof TextNode
|
|
22
|
+
? node.size
|
|
23
|
+
: fontSize
|
|
24
|
+
? parseInt(fontSize)
|
|
25
|
+
: Math.floor(parseFloat(getComputedStyle(document.documentElement).fontSize))
|
|
23
26
|
|
|
24
27
|
return {
|
|
25
28
|
node,
|
|
@@ -39,28 +42,36 @@ function selector(node: EditableNode) {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
function useTiptapState(editables: Array<EditableNode>) {
|
|
42
|
-
const
|
|
45
|
+
const props = useRef(editables.map(editableProps))
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
return useSyncExternalStore(
|
|
48
|
+
useCallback(
|
|
49
|
+
(callback) => {
|
|
50
|
+
editables.map((e) => e.tiptap.on("transaction", callback))
|
|
51
|
+
const textNodes = editables.filter((e) => e instanceof TextNode)
|
|
52
|
+
// HACK: react-bolt does not expose .subscribe on store fields
|
|
53
|
+
const dispose = effect(() => {
|
|
54
|
+
for (const text of textNodes) {
|
|
55
|
+
void text.size
|
|
56
|
+
}
|
|
57
|
+
callback()
|
|
58
|
+
})
|
|
59
|
+
return () => {
|
|
60
|
+
dispose()
|
|
61
|
+
editables.map((e) => e.tiptap.off("transaction", callback))
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[editables],
|
|
65
|
+
),
|
|
66
|
+
useCallback(() => {
|
|
67
|
+
const nextProps = editables.map(editableProps)
|
|
68
|
+
if (isEqual(props.current, nextProps)) {
|
|
69
|
+
return props.current
|
|
70
|
+
} else {
|
|
71
|
+
return (props.current = nextProps)
|
|
72
|
+
}
|
|
73
|
+
}, [editables]),
|
|
48
74
|
)
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
function update() {
|
|
52
|
-
const newState = editables.map(selector)
|
|
53
|
-
if (!isEqual(newState, state)) setState(newState)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
editables.map((e) => e.tiptap.on("transaction", update))
|
|
57
|
-
return () => void editables.map((e) => e.tiptap.off("transaction", update))
|
|
58
|
-
}, [editables, state, setState])
|
|
59
|
-
|
|
60
|
-
return state.map(({ node, ...s }) => {
|
|
61
|
-
const scale = node instanceof TextNode ? node.scale : 1
|
|
62
|
-
return { ...s, size: s.size * scale }
|
|
63
|
-
})
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
function formattableProps(node: FormattableNode) {
|
|
@@ -150,19 +161,24 @@ function useTextMarksState(
|
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
function useFocusedTiptap(editables: EditableNode[]) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
return () => void dispose.map((cb) => cb())
|
|
163
|
-
}, [editables, setFocused])
|
|
164
|
+
return useSyncExternalStore(
|
|
165
|
+
useCallback(
|
|
166
|
+
(callback) => {
|
|
167
|
+
const dispose = editables.map((e) => {
|
|
168
|
+
e.tiptap.on("focus", callback)
|
|
169
|
+
return () => e.tiptap.off("focus", callback)
|
|
170
|
+
})
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
return () => {
|
|
173
|
+
dispose.forEach((cb) => cb())
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[editables],
|
|
177
|
+
),
|
|
178
|
+
useCallback(() => {
|
|
179
|
+
return EditableNode.getFocused(editables)
|
|
180
|
+
}, [editables]),
|
|
181
|
+
)
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
// don't forget a unique key `selection.map((n) => n.id).join("")` on the parent component
|
|
@@ -207,18 +223,19 @@ export function useTextMarks(props: {
|
|
|
207
223
|
},
|
|
208
224
|
setSize(size: number) {
|
|
209
225
|
if (focused) {
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
if (focused instanceof TextNode) {
|
|
227
|
+
batchSet([focused], { size })
|
|
228
|
+
} else {
|
|
229
|
+
focused.tiptap.commands.setFontSize(`${size}px`)
|
|
230
|
+
}
|
|
212
231
|
} else {
|
|
213
|
-
editables
|
|
214
|
-
|
|
215
|
-
e
|
|
216
|
-
.chain()
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
221
|
-
batchSet(formattables, { size })
|
|
232
|
+
editables
|
|
233
|
+
.filter((e) => !(e instanceof TextNode))
|
|
234
|
+
.map((e) => {
|
|
235
|
+
e.tiptap.chain().selectAll().setFontSize(`${size}px`).run()
|
|
236
|
+
})
|
|
237
|
+
const textNodes = editables.filter((e) => e instanceof TextNode)
|
|
238
|
+
batchSet([...formattables, ...textNodes], { size })
|
|
222
239
|
}
|
|
223
240
|
},
|
|
224
241
|
setFamily(family: string | null) {
|
package/lib/model/node/text.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type { Page } from "../page"
|
|
|
4
4
|
import { EditableNode, type EditableNodeProps } from "./editable"
|
|
5
5
|
import type { SerializedNode } from "../node"
|
|
6
6
|
|
|
7
|
-
export type TextNodeProps = EditableNodeProps &
|
|
8
|
-
Partial<Pick<TextNode, "halign" | "scale">>
|
|
7
|
+
export type TextNodeProps = EditableNodeProps & Partial<Pick<TextNode, "halign" | "size">>
|
|
9
8
|
|
|
10
9
|
export class TextNode extends EditableNode {
|
|
11
10
|
get name() {
|
|
@@ -13,21 +12,27 @@ export class TextNode extends EditableNode {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
@state accessor halign: "left" | "center" | "right" | "justify"
|
|
16
|
-
@state accessor
|
|
15
|
+
@state accessor size: number = 16
|
|
17
16
|
@state accessor contentHeight: number = 24
|
|
18
17
|
|
|
19
18
|
@computed get height(): number {
|
|
20
|
-
return this.contentHeight
|
|
19
|
+
return this.contentHeight
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
set height(n: number) {
|
|
24
|
-
|
|
23
|
+
const target = Math.max(1, n)
|
|
24
|
+
|
|
25
|
+
if (this.contentHeight > 0) {
|
|
26
|
+
this.size = (this.size * target) / this.contentHeight
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.contentHeight = target
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
constructor(editor: Editor, page: Page, { halign,
|
|
32
|
+
constructor(editor: Editor, page: Page, { halign, size, ...props }: TextNodeProps) {
|
|
28
33
|
super(editor, page, props)
|
|
29
34
|
this.halign = halign ?? "center"
|
|
30
|
-
this.
|
|
35
|
+
this.size = size ?? 16
|
|
31
36
|
|
|
32
37
|
this.tiptap.on("transaction", () => {
|
|
33
38
|
const height = this.tiptap.view.dom.clientHeight
|
|
@@ -45,8 +50,8 @@ export class TextNode extends EditableNode {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
props(): TextNodeProps {
|
|
48
|
-
const { halign,
|
|
49
|
-
return { ...super.props(), halign,
|
|
53
|
+
const { halign, size } = this
|
|
54
|
+
return { ...super.props(), halign, size }
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
serialize(): SerializedNode<this["name"], TextNodeProps> {
|
|
@@ -13,7 +13,7 @@ export function TextContent(props: {
|
|
|
13
13
|
const { node, placeholder, isStatic } = props
|
|
14
14
|
const editor = useEditor()
|
|
15
15
|
const selection = useStore(editor, "selection")
|
|
16
|
-
const [halign,
|
|
16
|
+
const [halign, size] = useStore(node, "halign", "size")
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (node.tiptap.isEmpty && !selection.has(node)) {
|
|
@@ -22,20 +22,17 @@ export function TextContent(props: {
|
|
|
22
22
|
}, [node, selection])
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)}
|
|
38
|
-
/>
|
|
39
|
-
</div>
|
|
25
|
+
<EditableContent
|
|
26
|
+
node={node}
|
|
27
|
+
isStatic={isStatic}
|
|
28
|
+
placeholder={placeholder}
|
|
29
|
+
style={{ fontSize: `${size}px` }}
|
|
30
|
+
className={clsx(
|
|
31
|
+
halign === "left" && "text-left origin-top-left",
|
|
32
|
+
halign === "center" && "text-center origin-top",
|
|
33
|
+
halign === "right" && "text-right origin-top-right",
|
|
34
|
+
halign === "justify" && "text-justify *:w-full origin-top",
|
|
35
|
+
)}
|
|
36
|
+
/>
|
|
40
37
|
)
|
|
41
38
|
}
|