@lazlon-platform/html-editor 0.3.2 → 0.3.3

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
- if (!start.current) return
51
- const { center, anchor, deg, nodes } = start.current
55
+ const s = start.current
56
+ if (!s) return
52
57
 
53
- const next =
54
- angle(center, anchor, {
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 + next) % 360
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: { deg: nodes.length === 1 ? nodes[0].node.rotation : next },
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 deg =
77
- angle(s.center, s.anchor, {
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 + deg) % 360
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 + deg) % 360
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
@@ -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 } = e.getAttributes("textStyle")
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
  }
@@ -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({
@@ -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.configure({
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.2",
3
+ "version": "0.3.3",
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",