@lazlon-platform/html-editor 0.3.2 → 0.3.4
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.
|
@@ -14,6 +14,11 @@ type RotationState = {
|
|
|
14
14
|
nodes: Array<{ node: Node; deg: number }>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function snappedDeg(startDeg: number, delta: number, shiftKey: boolean) {
|
|
18
|
+
const rotation = startDeg + delta
|
|
19
|
+
return shiftKey ? Math.round(rotation / 45) * 45 : rotation
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function useRotation() {
|
|
18
23
|
const start = useRef<RotationState | null>(null)
|
|
19
24
|
const editor = useEditor()
|
|
@@ -47,22 +52,26 @@ export function useRotation() {
|
|
|
47
52
|
editor.action = { action: "rotate", payload: { deg: 0 } }
|
|
48
53
|
},
|
|
49
54
|
onMove({ event }) {
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const s = start.current
|
|
56
|
+
if (!s) return
|
|
52
57
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
x: event.clientX,
|
|
56
|
-
y: event.clientY,
|
|
57
|
-
}) - deg
|
|
58
|
+
const point = { x: event.clientX, y: event.clientY }
|
|
59
|
+
const deg = angle(s.center, s.anchor, point) - s.deg
|
|
58
60
|
|
|
59
|
-
for (const n of nodes) {
|
|
60
|
-
n.node.rotation = (n.deg
|
|
61
|
+
for (const n of s.nodes) {
|
|
62
|
+
n.node.rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const display =
|
|
66
|
+
s.nodes.length === 1
|
|
67
|
+
? s.nodes[0].node.rotation
|
|
68
|
+
: snappedDeg(s.deg, deg, event.shiftKey)
|
|
69
|
+
|
|
63
70
|
editor.action = {
|
|
64
71
|
action: "rotate",
|
|
65
|
-
payload: {
|
|
72
|
+
payload: {
|
|
73
|
+
deg: display,
|
|
74
|
+
},
|
|
66
75
|
}
|
|
67
76
|
},
|
|
68
77
|
onCancel() {
|
|
@@ -73,15 +82,12 @@ export function useRotation() {
|
|
|
73
82
|
const s = start.current
|
|
74
83
|
if (!s) return
|
|
75
84
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
x: event.clientX,
|
|
79
|
-
y: event.clientY,
|
|
80
|
-
}) - s.deg
|
|
85
|
+
const point = { x: event.clientX, y: event.clientY }
|
|
86
|
+
const deg = angle(s.center, s.anchor, point) - s.deg
|
|
81
87
|
|
|
82
88
|
if (s.nodes.length === 0) {
|
|
83
89
|
const [n] = s.nodes
|
|
84
|
-
const rotation = (n.deg
|
|
90
|
+
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
85
91
|
editor.history.push({
|
|
86
92
|
redo: ["set-node-props", [n.node.id, { rotation }]],
|
|
87
93
|
undo: ["set-node-props", [n.node.id, { rotation: n.deg }]],
|
|
@@ -92,7 +98,7 @@ export function useRotation() {
|
|
|
92
98
|
const undo: HistoryAction[] = []
|
|
93
99
|
|
|
94
100
|
for (const n of s.nodes) {
|
|
95
|
-
const rotation = (n.deg
|
|
101
|
+
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
96
102
|
redo.push(["set-node-props", [n.node.id, { rotation }]])
|
|
97
103
|
undo.push(["set-node-props", [n.node.id, { rotation: n.deg }]])
|
|
98
104
|
n.node.rotation = rotation
|
package/lib/hooks/textMarks.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useEffect, useState } from "react"
|
|
3
3
|
import { useComputed } from "react-bolt"
|
|
4
|
+
import { TextNode } from "../model"
|
|
4
5
|
import { EditableNode } from "../model/node/editable"
|
|
5
6
|
import { FormattableNode } from "../model/node/formattable"
|
|
6
7
|
import { useBatchSet, useNodeField, useNodeFieldBatch } from "./batch"
|
|
7
|
-
import { TextNode } from "../model"
|
|
8
8
|
|
|
9
9
|
function mergeField<T>(values: T[]): T | null {
|
|
10
10
|
if (values.length === 0) return null
|
|
@@ -14,7 +14,9 @@ function mergeField<T>(values: T[]): T | null {
|
|
|
14
14
|
|
|
15
15
|
function selector(node: EditableNode) {
|
|
16
16
|
const e = node.tiptap
|
|
17
|
-
const { color, fontSize, fontFamily, letterSpacing } =
|
|
17
|
+
const { color, fontSize, fontFamily, letterSpacing, lineHeight } =
|
|
18
|
+
e.getAttributes("textStyle")
|
|
19
|
+
|
|
18
20
|
const size = fontSize
|
|
19
21
|
? parseInt(fontSize)
|
|
20
22
|
: Math.floor(parseFloat(getComputedStyle(document.documentElement).fontSize))
|
|
@@ -31,6 +33,7 @@ function selector(node: EditableNode) {
|
|
|
31
33
|
color: color as string | null,
|
|
32
34
|
size: size,
|
|
33
35
|
spacing: letterSpacing ? parseFloat(letterSpacing) : 0,
|
|
36
|
+
lineHeight: lineHeight ? parseFloat(lineHeight) : 0,
|
|
34
37
|
family: fontFamily as string | null,
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -71,6 +74,7 @@ function formattableProps(node: FormattableNode) {
|
|
|
71
74
|
color: node.color,
|
|
72
75
|
size: node.size,
|
|
73
76
|
spacing: node.spacing,
|
|
77
|
+
lineHeight: node.lineHeight,
|
|
74
78
|
family: node.family,
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -85,6 +89,7 @@ type TextMarks = {
|
|
|
85
89
|
color: string | null
|
|
86
90
|
size: number | null
|
|
87
91
|
spacing: number
|
|
92
|
+
lineHeight: number
|
|
88
93
|
family: string | null
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -99,6 +104,7 @@ function mergeState(states: TextMarks[]) {
|
|
|
99
104
|
color: mergeField(states.map((s) => s.color)),
|
|
100
105
|
size: mergeField(states.map((s) => s.size)),
|
|
101
106
|
spacing: mergeField(states.map((s) => s.spacing)),
|
|
107
|
+
lineHeight: mergeField(states.map((s) => s.lineHeight)),
|
|
102
108
|
family: mergeField(states.map((s) => s.family)),
|
|
103
109
|
}
|
|
104
110
|
}
|
|
@@ -135,6 +141,7 @@ function useTextMarksState(
|
|
|
135
141
|
state.size ??
|
|
136
142
|
Math.floor(parseFloat(getComputedStyle(document.documentElement).fontSize)),
|
|
137
143
|
spacing: state.spacing ?? 0,
|
|
144
|
+
lineHeight: state.lineHeight ?? 0,
|
|
138
145
|
family: state.family ?? "Inter",
|
|
139
146
|
}
|
|
140
147
|
}
|
|
@@ -167,6 +174,8 @@ function getFocusedEditor(node: EditableNode[]) {
|
|
|
167
174
|
|
|
168
175
|
export function blurNode(node: EditableNode) {
|
|
169
176
|
if (lastFocusedEditor === node) {
|
|
177
|
+
node.tiptap.commands.blur()
|
|
178
|
+
node.tiptap.commands.selectAll()
|
|
170
179
|
lastFocusedEditor = null
|
|
171
180
|
}
|
|
172
181
|
}
|
|
@@ -197,6 +206,7 @@ export function useTextMarks(props: {
|
|
|
197
206
|
const focused = useFocusedTiptap(editables)
|
|
198
207
|
const formattableColors = useNodeFieldBatch(formattables, "color", "")
|
|
199
208
|
const formattableSpacings = useNodeFieldBatch(formattables, "spacing", 0)
|
|
209
|
+
const formattableLineHeight = useNodeFieldBatch(formattables, "lineHeight", 0)
|
|
200
210
|
const batchSet = useBatchSet()
|
|
201
211
|
|
|
202
212
|
return {
|
|
@@ -272,5 +282,19 @@ export function useTextMarks(props: {
|
|
|
272
282
|
}
|
|
273
283
|
}
|
|
274
284
|
},
|
|
285
|
+
setLineHeight(lineHeight: number, opts: { end: boolean }) {
|
|
286
|
+
if (focused) {
|
|
287
|
+
focused.tiptap.commands.setLineHeight(`${lineHeight}`)
|
|
288
|
+
} else {
|
|
289
|
+
editables.map((e) =>
|
|
290
|
+
e.tiptap.chain().selectAll().setLineHeight(`${lineHeight}`).run(),
|
|
291
|
+
)
|
|
292
|
+
if (opts.end) {
|
|
293
|
+
formattableLineHeight.onChangeEnd(lineHeight)
|
|
294
|
+
} else {
|
|
295
|
+
formattableLineHeight.onChange(lineHeight)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
275
299
|
}
|
|
276
300
|
}
|
package/lib/model/editor.ts
CHANGED
|
@@ -37,11 +37,11 @@ export interface EditorProps {
|
|
|
37
37
|
schema: Array<NodeConstructor>
|
|
38
38
|
options?: Partial<EditorOptions>
|
|
39
39
|
init?: Partial<SerializedEditor>
|
|
40
|
-
onChange?: () => void
|
|
40
|
+
onChange?: (editor: Editor) => void
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function unimplemented(): Promise<GoogleWebfont[]> {
|
|
44
|
-
throw Error("missing implementation")
|
|
44
|
+
throw Error("@lazlon/html-editor missing googleFonts implementation")
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export class Editor {
|
|
@@ -106,7 +106,7 @@ export class Editor {
|
|
|
106
106
|
|
|
107
107
|
constructor(props: EditorProps) {
|
|
108
108
|
const { schema, options, init } = props
|
|
109
|
-
this.#onChange = () => props.onChange?.()
|
|
109
|
+
this.#onChange = () => props.onChange?.(this)
|
|
110
110
|
this.#schema = new Map(schema.map((ctor) => [ctor.prototype.name, ctor]))
|
|
111
111
|
|
|
112
112
|
this.options = createStore({
|
|
@@ -12,8 +12,8 @@ export type EditableNodeProps = NodeProps &
|
|
|
12
12
|
|
|
13
13
|
export abstract class EditableNode extends Node {
|
|
14
14
|
@state accessor contentRef: HTMLElement | null = null
|
|
15
|
-
@state private accessor _isEmpty: boolean
|
|
16
|
-
@state private accessor _content: JSONContent
|
|
15
|
+
@state private accessor _isEmpty: boolean = true
|
|
16
|
+
@state private accessor _content: JSONContent = { type: "doc", content: [] }
|
|
17
17
|
|
|
18
18
|
@state accessor lineHeight: number
|
|
19
19
|
|
|
@@ -22,6 +22,7 @@ export abstract class EditableNode extends Node {
|
|
|
22
22
|
emitUpdate: false,
|
|
23
23
|
})
|
|
24
24
|
this._content = this.tiptap.getJSON()
|
|
25
|
+
this._isEmpty = this.tiptap.isEmpty
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
@computed get content() {
|
|
@@ -56,10 +57,8 @@ export abstract class EditableNode extends Node {
|
|
|
56
57
|
{ content, lineHeight, ...props }: EditableNodeProps,
|
|
57
58
|
) {
|
|
58
59
|
super(editor, page, props)
|
|
59
|
-
this._content = content ?? { type: "doc", content: [] }
|
|
60
|
-
this._isEmpty = this.tiptap.isEmpty
|
|
61
60
|
this.lineHeight = lineHeight ?? 1.2
|
|
62
|
-
this.content =
|
|
61
|
+
this.content = content ?? { type: "doc", content: [] }
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
blockMove(event: React.PointerEvent): boolean {
|
|
@@ -7,12 +7,14 @@ import Strike from "@tiptap/extension-strike"
|
|
|
7
7
|
import Subscript from "@tiptap/extension-subscript"
|
|
8
8
|
import Superscript from "@tiptap/extension-superscript"
|
|
9
9
|
import Text from "@tiptap/extension-text"
|
|
10
|
+
import { Selection } from "@tiptap/extensions"
|
|
10
11
|
import { TextStyleKit } from "@tiptap/extension-text-style"
|
|
11
12
|
import Underline from "@tiptap/extension-underline"
|
|
12
13
|
import { LetterSpacing } from "./letterSpacing"
|
|
13
14
|
import { PersistentMarks } from "./persistentMarks"
|
|
14
15
|
|
|
15
16
|
export const TiptapExtensions = [
|
|
17
|
+
Selection,
|
|
16
18
|
Document,
|
|
17
19
|
Paragraph,
|
|
18
20
|
Text,
|
|
@@ -24,10 +26,6 @@ export const TiptapExtensions = [
|
|
|
24
26
|
Subscript,
|
|
25
27
|
Superscript,
|
|
26
28
|
LetterSpacing,
|
|
27
|
-
TextStyleKit
|
|
28
|
-
lineHeight: {
|
|
29
|
-
types: ["paragraph"],
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
29
|
+
TextStyleKit,
|
|
32
30
|
PersistentMarks,
|
|
33
31
|
]
|
|
@@ -27,7 +27,6 @@ export function EditableContent(props: {
|
|
|
27
27
|
const onDoubleClick = useDoubleClick(
|
|
28
28
|
useCallback(() => {
|
|
29
29
|
setClicked(true)
|
|
30
|
-
console.log("hello")
|
|
31
30
|
node.tiptap.commands.focus()
|
|
32
31
|
}, [node, setClicked]),
|
|
33
32
|
)
|
|
@@ -39,7 +38,6 @@ export function EditableContent(props: {
|
|
|
39
38
|
|
|
40
39
|
useEffect(() => {
|
|
41
40
|
if (!selection.has(node)) {
|
|
42
|
-
node.tiptap.commands.blur()
|
|
43
41
|
blurNode(node)
|
|
44
42
|
setClicked(false)
|
|
45
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazlon-platform/html-editor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"lib"
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@tiptap/extension-text": "^3.20.0",
|
|
26
26
|
"@tiptap/extension-text-style": "^3.20.0",
|
|
27
27
|
"@tiptap/extension-underline": "^3.20.0",
|
|
28
|
+
"@tiptap/extensions": "^3.22.4",
|
|
28
29
|
"@tiptap/pm": "^3.20.0",
|
|
29
30
|
"@tiptap/react": "^3.20.0",
|
|
30
31
|
"clsx": "^2.1.1",
|