@nuasite/cms 0.19.1 → 0.20.2
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/dist/editor.js +12615 -12689
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +185 -189
- package/src/editor/api.ts +0 -251
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +46 -70
- package/src/editor/components/markdown-inline-editor.tsx +34 -165
- package/src/editor/components/mdx-block-view.tsx +351 -47
- package/src/editor/components/mdx-component-picker.tsx +35 -11
- package/src/editor/components/media-library.tsx +1 -15
- package/src/editor/components/modal-shell.tsx +1 -1
- package/src/editor/components/toolbar.tsx +0 -75
- package/src/editor/constants.ts +0 -4
- package/src/editor/editor.ts +2 -192
- package/src/editor/hooks/index.ts +0 -3
- package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
- package/src/editor/hooks/useTooltipState.ts +1 -2
- package/src/editor/index.tsx +2 -18
- package/src/editor/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/post-message.ts +0 -6
- package/src/editor/signals.ts +0 -183
- package/src/editor/styles.css +0 -108
- package/src/editor/types.ts +0 -76
- package/src/html-processor.ts +9 -7
- package/src/source-finder/cache.ts +47 -0
- package/src/source-finder/collection-finder.ts +181 -0
- package/src/source-finder/index.ts +5 -2
- package/src/source-finder/search-index.ts +79 -0
- package/src/source-finder/snippet-utils.ts +36 -61
- package/src/types.ts +0 -4
- package/src/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/ai.ts +0 -185
- package/src/editor/components/ai-chat.tsx +0 -631
- package/src/editor/components/ai-tooltip.tsx +0 -180
- package/src/editor/components/mdx-props-editor.tsx +0 -94
- package/src/editor/hooks/useAIHandlers.ts +0 -345
|
@@ -4,12 +4,21 @@ import type { MarkdownNode, SerializerState } from '@milkdown/transformer'
|
|
|
4
4
|
import { $command, $node, $remark, $view } from '@milkdown/utils'
|
|
5
5
|
import { render } from 'preact'
|
|
6
6
|
import remarkMdx from 'remark-mdx'
|
|
7
|
+
import remarkParse from 'remark-parse'
|
|
8
|
+
import { unified } from 'unified'
|
|
7
9
|
import { MdxBlockCard } from './components/mdx-block-view'
|
|
8
|
-
import {
|
|
10
|
+
import { getComponentDefinition } from './manifest'
|
|
11
|
+
import { manifest } from './signals'
|
|
9
12
|
|
|
10
13
|
/** Prefix used to distinguish expression attributes from string literals in serialized props */
|
|
11
14
|
export const MDX_EXPR_PREFIX = '__mdx_expr__:'
|
|
12
15
|
|
|
16
|
+
/** HTML void elements that should not be treated as editable MDX components */
|
|
17
|
+
const HTML_VOID_ELEMENTS = new Set(['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'])
|
|
18
|
+
|
|
19
|
+
/** Cached unified processor for parsing markdown children during serialization */
|
|
20
|
+
const remarkParser = unified().use(remarkParse)
|
|
21
|
+
|
|
13
22
|
export const remarkMdxPlugin: any = $remark('remarkMdx', () => remarkMdx)
|
|
14
23
|
|
|
15
24
|
function parseJsxAttributes(attributes: any[]): { props: Record<string, string>; hasExpressions: boolean } {
|
|
@@ -64,21 +73,72 @@ function serializePropsToAttributes(props: Record<string, string>): any[] {
|
|
|
64
73
|
return attributes
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
/** Serialize mdast children back to markdown text */
|
|
77
|
+
function serializeChildren(children: any[]): string {
|
|
78
|
+
if (!children || children.length === 0) return ''
|
|
79
|
+
const parts: string[] = []
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
if (child.type === 'paragraph') {
|
|
82
|
+
const text = serializeInlineChildren(child.children ?? [])
|
|
83
|
+
parts.push(text)
|
|
84
|
+
} else if (child.type === 'text') {
|
|
85
|
+
parts.push(child.value ?? '')
|
|
86
|
+
} else if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
|
|
87
|
+
// Nested JSX — reconstruct as string
|
|
88
|
+
const name = child.name ?? ''
|
|
89
|
+
const attrs = (child.attributes ?? [])
|
|
90
|
+
.map((a: any) => {
|
|
91
|
+
if (a.type === 'mdxJsxAttribute' && a.name) {
|
|
92
|
+
if (typeof a.value === 'string') return `${a.name}="${a.value}"`
|
|
93
|
+
if (a.value?.type === 'mdxJsxAttributeValueExpression') return `${a.name}={${a.value.value}}`
|
|
94
|
+
}
|
|
95
|
+
return ''
|
|
96
|
+
})
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(' ')
|
|
99
|
+
const inner = serializeChildren(child.children ?? [])
|
|
100
|
+
if (inner) {
|
|
101
|
+
parts.push(`<${name}${attrs ? ' ' + attrs : ''}>${inner}</${name}>`)
|
|
102
|
+
} else {
|
|
103
|
+
parts.push(`<${name}${attrs ? ' ' + attrs : ''} />`)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// Fallback — use value if present
|
|
107
|
+
if (child.value) parts.push(child.value)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return parts.join('\n\n')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function serializeInlineChildren(children: any[]): string {
|
|
114
|
+
return children.map((c: any) => {
|
|
115
|
+
if (c.type === 'text') return c.value ?? ''
|
|
116
|
+
if (c.type === 'strong') return `**${serializeInlineChildren(c.children ?? [])}**`
|
|
117
|
+
if (c.type === 'emphasis') return `*${serializeInlineChildren(c.children ?? [])}*`
|
|
118
|
+
if (c.type === 'inlineCode') return `\`${c.value ?? ''}\``
|
|
119
|
+
if (c.type === 'link') return `[${serializeInlineChildren(c.children ?? [])}](${c.url ?? ''})`
|
|
120
|
+
return c.value ?? ''
|
|
121
|
+
}).join('')
|
|
122
|
+
}
|
|
123
|
+
|
|
67
124
|
export const mdxComponentNode = $node('mdx_component', () => ({
|
|
68
125
|
group: 'block',
|
|
69
126
|
atom: true,
|
|
70
127
|
isolating: true,
|
|
71
128
|
selectable: true,
|
|
72
|
-
draggable:
|
|
129
|
+
draggable: false,
|
|
73
130
|
attrs: {
|
|
74
131
|
componentName: { default: '' },
|
|
75
132
|
props: { default: '{}' },
|
|
76
133
|
hasExpressions: { default: false },
|
|
134
|
+
children: { default: '' },
|
|
77
135
|
},
|
|
78
136
|
toDOM: (node: PmNode) => {
|
|
79
137
|
const div = document.createElement('div')
|
|
80
138
|
div.setAttribute('data-mdx-component', node.attrs.componentName)
|
|
81
139
|
div.setAttribute('data-mdx-props', node.attrs.props)
|
|
140
|
+
if (node.attrs.children) div.setAttribute('data-mdx-children', node.attrs.children)
|
|
141
|
+
if (node.attrs.hasExpressions) div.setAttribute('data-mdx-expressions', 'true')
|
|
82
142
|
div.className = 'mdx-component-block'
|
|
83
143
|
div.textContent = `<${node.attrs.componentName} />`
|
|
84
144
|
return div as any
|
|
@@ -88,6 +148,8 @@ export const mdxComponentNode = $node('mdx_component', () => ({
|
|
|
88
148
|
getAttrs: (dom: HTMLElement) => ({
|
|
89
149
|
componentName: dom.getAttribute('data-mdx-component') || '',
|
|
90
150
|
props: dom.getAttribute('data-mdx-props') || '{}',
|
|
151
|
+
children: dom.getAttribute('data-mdx-children') || '',
|
|
152
|
+
hasExpressions: dom.getAttribute('data-mdx-expressions') === 'true',
|
|
91
153
|
}),
|
|
92
154
|
}],
|
|
93
155
|
parseMarkdown: {
|
|
@@ -95,13 +157,17 @@ export const mdxComponentNode = $node('mdx_component', () => ({
|
|
|
95
157
|
runner: (state, node, proseType: NodeType) => {
|
|
96
158
|
const name = (node as any).name as string | null
|
|
97
159
|
if (!name) return // Skip fragments
|
|
160
|
+
// Skip HTML void elements — they are not editable components
|
|
161
|
+
if (HTML_VOID_ELEMENTS.has(name.toLowerCase())) return
|
|
98
162
|
|
|
99
163
|
const { props, hasExpressions } = parseJsxAttributes((node as any).attributes)
|
|
164
|
+
const children = serializeChildren((node as any).children ?? [])
|
|
100
165
|
|
|
101
166
|
state.addNode(proseType, {
|
|
102
167
|
componentName: name,
|
|
103
168
|
props: JSON.stringify(props),
|
|
104
169
|
hasExpressions,
|
|
170
|
+
children,
|
|
105
171
|
})
|
|
106
172
|
},
|
|
107
173
|
},
|
|
@@ -110,13 +176,24 @@ export const mdxComponentNode = $node('mdx_component', () => ({
|
|
|
110
176
|
runner: (state: SerializerState, node: PmNode) => {
|
|
111
177
|
const componentName = node.attrs.componentName as string
|
|
112
178
|
const props: Record<string, string> = JSON.parse(node.attrs.props as string)
|
|
179
|
+
const childrenText = (node.attrs.children as string) || ''
|
|
113
180
|
const attributes = serializePropsToAttributes(props)
|
|
114
181
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
182
|
+
if (childrenText.trim()) {
|
|
183
|
+
// Parse children markdown into proper mdast nodes so headings, lists, etc. are preserved
|
|
184
|
+
const childrenAst = remarkParser.parse(childrenText)
|
|
185
|
+
state.addNode('mdxJsxFlowElement', undefined, undefined, {
|
|
186
|
+
name: componentName,
|
|
187
|
+
attributes,
|
|
188
|
+
children: childrenAst.children,
|
|
189
|
+
} as any)
|
|
190
|
+
} else {
|
|
191
|
+
state.addNode('mdxJsxFlowElement', undefined, undefined, {
|
|
192
|
+
name: componentName,
|
|
193
|
+
attributes,
|
|
194
|
+
children: [],
|
|
195
|
+
} as any)
|
|
196
|
+
}
|
|
120
197
|
},
|
|
121
198
|
},
|
|
122
199
|
}))
|
|
@@ -124,6 +201,7 @@ export const mdxComponentNode = $node('mdx_component', () => ({
|
|
|
124
201
|
export interface InsertMdxComponentPayload {
|
|
125
202
|
componentName: string
|
|
126
203
|
props: Record<string, string>
|
|
204
|
+
children?: string
|
|
127
205
|
}
|
|
128
206
|
|
|
129
207
|
export const insertMdxComponentCommand = $command('insertMdxComponent', (ctx) => {
|
|
@@ -137,6 +215,7 @@ export const insertMdxComponentCommand = $command('insertMdxComponent', (ctx) =>
|
|
|
137
215
|
componentName: payload.componentName,
|
|
138
216
|
props: JSON.stringify(payload.props),
|
|
139
217
|
hasExpressions: false,
|
|
218
|
+
children: payload.children || '',
|
|
140
219
|
})
|
|
141
220
|
|
|
142
221
|
if (dispatch) {
|
|
@@ -156,27 +235,36 @@ export const mdxComponentView = $view(mdxComponentNode, () => {
|
|
|
156
235
|
container.setAttribute('data-cms-ui', '')
|
|
157
236
|
container.contentEditable = 'false'
|
|
158
237
|
|
|
159
|
-
let lastAttrs: { componentName: string; props: string; hasExpressions: boolean } | null = null
|
|
238
|
+
let lastAttrs: { componentName: string; props: string; hasExpressions: boolean; children: string } | null = null
|
|
160
239
|
|
|
161
240
|
const renderCard = (node: PmNode) => {
|
|
162
241
|
const componentName = node.attrs.componentName as string
|
|
163
242
|
const propsJson = node.attrs.props as string
|
|
164
243
|
const props: Record<string, string> = JSON.parse(propsJson)
|
|
165
244
|
const hasExpressions = node.attrs.hasExpressions as boolean
|
|
245
|
+
const children = (node.attrs.children as string) || ''
|
|
246
|
+
const definition = getComponentDefinition(manifest.value, componentName)
|
|
247
|
+
const hasDefaultSlot = definition?.slots?.includes('default') ?? false
|
|
248
|
+
|
|
249
|
+
lastAttrs = { componentName, props: propsJson, hasExpressions, children }
|
|
166
250
|
|
|
167
|
-
|
|
251
|
+
const updateNodeAttrs = (update: Record<string, unknown>) => {
|
|
252
|
+
const pos = typeof getPos === 'function' ? getPos() : null
|
|
253
|
+
if (pos != null) {
|
|
254
|
+
const currentNode = view.state.doc.nodeAt(pos)
|
|
255
|
+
if (currentNode) {
|
|
256
|
+
const tr = view.state.tr.setNodeMarkup(pos, undefined, { ...currentNode.attrs, ...update })
|
|
257
|
+
view.dispatch(tr)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
168
261
|
|
|
169
262
|
render(
|
|
170
263
|
<MdxBlockCard
|
|
171
264
|
componentName={componentName}
|
|
172
265
|
props={props}
|
|
173
266
|
hasExpressions={hasExpressions}
|
|
174
|
-
|
|
175
|
-
const pos = typeof getPos === 'function' ? getPos() : null
|
|
176
|
-
if (pos != null) {
|
|
177
|
-
openMdxPropsEditor(pos, componentName, props, cursorPos)
|
|
178
|
-
}
|
|
179
|
-
}}
|
|
267
|
+
slotContent={children}
|
|
180
268
|
onRemove={() => {
|
|
181
269
|
const pos = typeof getPos === 'function' ? getPos() : null
|
|
182
270
|
if (pos != null) {
|
|
@@ -187,6 +275,12 @@ export const mdxComponentView = $view(mdxComponentNode, () => {
|
|
|
187
275
|
}
|
|
188
276
|
}
|
|
189
277
|
}}
|
|
278
|
+
onSlotContentChange={hasDefaultSlot
|
|
279
|
+
? (newContent: string) => updateNodeAttrs({ children: newContent })
|
|
280
|
+
: undefined}
|
|
281
|
+
onPropsChange={!hasExpressions
|
|
282
|
+
? (newProps: Record<string, string>) => updateNodeAttrs({ props: JSON.stringify(newProps) })
|
|
283
|
+
: undefined}
|
|
190
284
|
/>,
|
|
191
285
|
container,
|
|
192
286
|
)
|
|
@@ -197,11 +291,13 @@ export const mdxComponentView = $view(mdxComponentNode, () => {
|
|
|
197
291
|
return {
|
|
198
292
|
dom: container,
|
|
199
293
|
stopEvent: (event: Event) => {
|
|
294
|
+
const target = event.target as HTMLElement
|
|
295
|
+
// Allow all events on inline editors (textarea, input, contenteditable)
|
|
296
|
+
if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT') return true
|
|
297
|
+
if (target.isContentEditable || target.closest('[contenteditable]')) return true
|
|
298
|
+
if (target.closest('[data-mdx-action="children"]') || target.closest('[data-mdx-action="props"]')) return true
|
|
200
299
|
if (event.type === 'mousedown' || event.type === 'click') {
|
|
201
|
-
|
|
202
|
-
if (target.closest('button') || target.closest('[data-mdx-action]')) {
|
|
203
|
-
return true
|
|
204
|
-
}
|
|
300
|
+
if (target.closest('button') || target.closest('[data-mdx-action]')) return true
|
|
205
301
|
}
|
|
206
302
|
return false
|
|
207
303
|
},
|
|
@@ -214,6 +310,7 @@ export const mdxComponentView = $view(mdxComponentNode, () => {
|
|
|
214
310
|
&& attrs.componentName === lastAttrs.componentName
|
|
215
311
|
&& attrs.props === lastAttrs.props
|
|
216
312
|
&& attrs.hasExpressions === lastAttrs.hasExpressions
|
|
313
|
+
&& attrs.children === lastAttrs.children
|
|
217
314
|
) {
|
|
218
315
|
return true
|
|
219
316
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Editor } from '@milkdown/core'
|
|
2
|
+
import { editorViewCtx } from '@milkdown/core'
|
|
3
|
+
import type { EditorView } from '@milkdown/prose/view'
|
|
4
|
+
|
|
5
|
+
export interface ActiveFormats {
|
|
6
|
+
bold: boolean
|
|
7
|
+
italic: boolean
|
|
8
|
+
strikethrough: boolean
|
|
9
|
+
link: boolean
|
|
10
|
+
linkHref: string | null
|
|
11
|
+
bulletList: boolean
|
|
12
|
+
orderedList: boolean
|
|
13
|
+
blockquote: boolean
|
|
14
|
+
heading: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const defaultActiveFormats: ActiveFormats = {
|
|
18
|
+
bold: false,
|
|
19
|
+
italic: false,
|
|
20
|
+
strikethrough: false,
|
|
21
|
+
link: false,
|
|
22
|
+
linkHref: null,
|
|
23
|
+
bulletList: false,
|
|
24
|
+
orderedList: false,
|
|
25
|
+
blockquote: false,
|
|
26
|
+
heading: null,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect active inline/block formats at the current selection in a ProseMirror view.
|
|
31
|
+
*/
|
|
32
|
+
export function getActiveFormats(view: EditorView): ActiveFormats {
|
|
33
|
+
const { state } = view
|
|
34
|
+
const { $from, from, to } = state.selection
|
|
35
|
+
|
|
36
|
+
// Check marks (inline formatting)
|
|
37
|
+
let bold = false
|
|
38
|
+
let italic = false
|
|
39
|
+
let strikethrough = false
|
|
40
|
+
let link = false
|
|
41
|
+
let linkHref: string | null = null
|
|
42
|
+
|
|
43
|
+
const marks = state.storedMarks || $from.marks()
|
|
44
|
+
for (const mark of marks) {
|
|
45
|
+
if (mark.type.name === 'strong') bold = true
|
|
46
|
+
if (mark.type.name === 'emphasis') italic = true
|
|
47
|
+
if (mark.type.name === 'strikethrough') strikethrough = true
|
|
48
|
+
if (mark.type.name === 'link') {
|
|
49
|
+
link = true
|
|
50
|
+
linkHref = mark.attrs.href as string
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Also check marks in the selection range
|
|
55
|
+
if (from !== to) {
|
|
56
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
57
|
+
if (node.marks) {
|
|
58
|
+
for (const mark of node.marks) {
|
|
59
|
+
if (mark.type.name === 'strong') bold = true
|
|
60
|
+
if (mark.type.name === 'emphasis') italic = true
|
|
61
|
+
if (mark.type.name === 'strikethrough') strikethrough = true
|
|
62
|
+
if (mark.type.name === 'link') {
|
|
63
|
+
link = true
|
|
64
|
+
linkHref = mark.attrs.href as string
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check block types (lists, blockquote, heading)
|
|
72
|
+
let bulletList = false
|
|
73
|
+
let orderedList = false
|
|
74
|
+
let blockquote = false
|
|
75
|
+
let heading: number | null = null
|
|
76
|
+
|
|
77
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
78
|
+
const node = $from.node(depth)
|
|
79
|
+
if (node.type.name === 'bullet_list') bulletList = true
|
|
80
|
+
if (node.type.name === 'ordered_list') orderedList = true
|
|
81
|
+
if (node.type.name === 'blockquote') blockquote = true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ($from.parent.type.name === 'heading') {
|
|
85
|
+
heading = $from.parent.attrs.level as number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { bold, italic, strikethrough, link, linkHref, bulletList, orderedList, blockquote, heading }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check whether the current selection is inside a list of the given type.
|
|
93
|
+
*/
|
|
94
|
+
export function isInListType(view: EditorView, listType: string): boolean {
|
|
95
|
+
const { $from } = view.state.selection
|
|
96
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
97
|
+
if ($from.node(depth).type.name === listType) return true
|
|
98
|
+
}
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Toggle a heading level at the current selection. If the selection is already
|
|
104
|
+
* a heading of the given level, convert it back to a paragraph.
|
|
105
|
+
*/
|
|
106
|
+
export function toggleHeading(view: EditorView, level: number): void {
|
|
107
|
+
const { state } = view
|
|
108
|
+
const headingType = state.schema.nodes.heading
|
|
109
|
+
const paragraphType = state.schema.nodes.paragraph
|
|
110
|
+
if (!headingType) return
|
|
111
|
+
|
|
112
|
+
const { $from } = state.selection
|
|
113
|
+
const isCurrentHeading = $from.parent.type.name === 'heading' && $from.parent.attrs.level === level
|
|
114
|
+
const targetType = isCurrentHeading ? paragraphType : headingType
|
|
115
|
+
const attrs = isCurrentHeading ? undefined : { level }
|
|
116
|
+
if (!targetType) return
|
|
117
|
+
|
|
118
|
+
const blockFrom = $from.before($from.depth)
|
|
119
|
+
const blockTo = state.selection.$to.after(state.selection.$to.depth)
|
|
120
|
+
view.dispatch(state.tr.setBlockType(blockFrom, blockTo, targetType, attrs))
|
|
121
|
+
view.focus()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatsEqual(a: ActiveFormats, b: ActiveFormats): boolean {
|
|
125
|
+
return a.bold === b.bold
|
|
126
|
+
&& a.italic === b.italic
|
|
127
|
+
&& a.strikethrough === b.strikethrough
|
|
128
|
+
&& a.link === b.link
|
|
129
|
+
&& a.linkHref === b.linkHref
|
|
130
|
+
&& a.bulletList === b.bulletList
|
|
131
|
+
&& a.orderedList === b.orderedList
|
|
132
|
+
&& a.blockquote === b.blockquote
|
|
133
|
+
&& a.heading === b.heading
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Intercept dispatch on the editor view to track active formats via rAF
|
|
138
|
+
* debouncing. Fires the callback only when formats actually change.
|
|
139
|
+
* Returns a cleanup function that cancels the pending rAF.
|
|
140
|
+
*/
|
|
141
|
+
export function setupFormatTracking(editor: Editor, callback: (formats: ActiveFormats) => void): () => void {
|
|
142
|
+
let formatRaf = 0
|
|
143
|
+
let lastFormats: ActiveFormats = defaultActiveFormats
|
|
144
|
+
|
|
145
|
+
const update = () => {
|
|
146
|
+
try {
|
|
147
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
148
|
+
const formats = getActiveFormats(view)
|
|
149
|
+
if (!formatsEqual(formats, lastFormats)) {
|
|
150
|
+
lastFormats = formats
|
|
151
|
+
callback(formats)
|
|
152
|
+
}
|
|
153
|
+
} catch { /* ignore */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
158
|
+
const origDispatch = view.dispatch.bind(view)
|
|
159
|
+
view.dispatch = (tr) => {
|
|
160
|
+
origDispatch(tr)
|
|
161
|
+
if (tr.selectionSet || tr.docChanged) {
|
|
162
|
+
cancelAnimationFrame(formatRaf)
|
|
163
|
+
formatRaf = requestAnimationFrame(update)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
|
|
168
|
+
// Fire initial check
|
|
169
|
+
update()
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
cancelAnimationFrame(formatRaf)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -109,8 +109,6 @@ export function buildPageNavigatedMessage(manifest: CmsManifest, pathname: strin
|
|
|
109
109
|
export function buildEditorState(opts: {
|
|
110
110
|
isEditing: boolean
|
|
111
111
|
dirtyCount: CmsEditorState['dirtyCount']
|
|
112
|
-
deploymentStatus: CmsEditorState['deployment']['status']
|
|
113
|
-
lastDeployedAt: string | null
|
|
114
112
|
canUndo: boolean
|
|
115
113
|
canRedo: boolean
|
|
116
114
|
}): CmsEditorState {
|
|
@@ -118,10 +116,6 @@ export function buildEditorState(opts: {
|
|
|
118
116
|
isEditing: opts.isEditing,
|
|
119
117
|
hasChanges: opts.dirtyCount.total > 0,
|
|
120
118
|
dirtyCount: opts.dirtyCount,
|
|
121
|
-
deployment: {
|
|
122
|
-
status: opts.deploymentStatus,
|
|
123
|
-
lastDeployedAt: opts.lastDeployedAt,
|
|
124
|
-
},
|
|
125
119
|
canUndo: opts.canUndo,
|
|
126
120
|
canRedo: opts.canRedo,
|
|
127
121
|
}
|