Skip to content

Commit 0ee9891

Browse files
authored
fix: optimize editor toolbar loading state check (#10174)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
1 parent 621a61d commit 0ee9891

File tree

5 files changed

+103
-30
lines changed

5 files changed

+103
-30
lines changed

plugins/text-editor-resources/src/components/extension/embed/embed.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export interface EmbedNodeProvider {
4848
autoEmbedUrl?: (src: string) => boolean
4949
}
5050

51-
export type EmbedNodeView = (editor: Editor, root: HTMLDivElement) => EmbedNodeViewHandle | undefined
51+
export type EmbedNodeView = (
52+
editor: Editor,
53+
root: HTMLDivElement,
54+
getPos: () => number
55+
) => EmbedNodeViewHandle | undefined
5256
export type EmbedNodeProviderConstructor<T> = (options: T) => EmbedNodeProvider
5357

5458
export type EmbedCursor = ToolbarCursor<EmbedCursorProps>
@@ -101,29 +105,31 @@ export const EmbedNode = BaseEmbedNode.extend<EmbedNodeOptions>({
101105
},
102106

103107
addNodeView () {
104-
return ({ node, HTMLAttributes, editor }) => {
108+
return ({ node, HTMLAttributes, editor, getPos }) => {
105109
const providerPromise = matchUrl(this.options.providers, node.attrs.src)
106110

107111
const root = document.createElement('div')
108112
root.setAttribute('data-type', this.name)
109113
root.setAttribute('data-embed-src', node.attrs.src)
110114
root.classList.add('embed-node')
111115

112-
setLoadingState(editor.view, root, true)
116+
const pos = typeof getPos === 'function' ? getPos() : 0
117+
setLoadingState(editor.view, pos, true)
113118
root.setAttribute('block-editor-blur', 'true')
114119

115120
let handle: EmbedNodeViewHandle | undefined
116121

117122
void providerPromise
118123
.then((view) => {
119124
view = view ?? StubEmbedNodeView
120-
handle = view(editor, root)
125+
handle = view(editor, root, getPos)
121126
if (handle !== undefined) {
122127
root.classList.add(`embed-${handle.name}`)
123128
}
124129
})
125130
.finally(() => {
126-
setLoadingState(editor.view, root, false)
131+
const pos = typeof getPos === 'function' ? getPos() : 0
132+
setLoadingState(editor.view, pos, false)
127133
})
128134

129135
return {
@@ -500,7 +506,7 @@ export function replacePreviewContent (
500506
return tr
501507
}
502508

503-
const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLElement) => {
509+
const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLDivElement, getPos: () => number) => {
504510
const hint = document.createElement('p')
505511
const hintIcon = hint.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
506512
const hintSpan = hint.appendChild(document.createElement('span'))

plugins/text-editor-resources/src/components/extension/embed/providers/drive.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ export const DriveEmbedProvider: EmbedNodeProviderConstructor<DriveEmbedOptions>
5959

6060
if (previewType === undefined) return
6161

62-
return (editor: Editor, root: HTMLDivElement) => {
62+
return (editor: Editor, root: HTMLDivElement, getPos: () => number) => {
6363
const setLoading = (loading: boolean): void => {
64-
setLoadingState(editor.view, root, loading)
64+
const pos = typeof getPos === 'function' ? getPos() : 0
65+
setLoadingState(editor.view, pos, loading)
6566
}
6667
const renderer = new SvelteRenderer(FilePreview as any, {
6768
element: root,

plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const YoutubeEmbedProvider: EmbedNodeProviderConstructor<YoutubeEmbedUrlO
2121
const url = getEmbedUrlFromYoutubeUrl(src, options)
2222
if (url === undefined) return
2323

24-
return (editor: Editor, root: HTMLDivElement) => {
24+
return (editor: Editor, root: HTMLDivElement, getPos: () => number) => {
2525
root.setAttribute('data-block-toolbar-mouse-lock', 'true')
2626

2727
const iframe = document.createElement('iframe')

plugins/text-editor-resources/src/components/extension/imageExt.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
151151
addNodeView () {
152152
const imageSrcCache = new Map<string, { src: string, srcset: string }>()
153153

154-
return ({ view, node, HTMLAttributes }) => {
154+
return ({ view, node, HTMLAttributes, getPos }) => {
155155
const container = document.createElement('div')
156156
const imgElement = document.createElement('img')
157157
container.append(imgElement)
@@ -161,12 +161,14 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
161161
'data-align': node.attrs.align
162162
}
163163

164-
setLoadingState(view, container, true)
164+
const pos = typeof getPos === 'function' ? getPos() : 0
165+
setLoadingState(view, pos, true)
165166
const setImageProps = (src: string | null, srcset: string | null): void => {
166167
if (src != null) imgElement.src = src
167168
if (srcset != null) imgElement.srcset = srcset
168169
void imgElement.decode().finally(() => {
169-
setLoadingState(view, container, false)
170+
const pos = typeof getPos === 'function' ? getPos() : 0
171+
setLoadingState(view, pos, false)
170172
})
171173
}
172174

plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export interface SelectionCursorContext {
8888

8989
export const ToolbarExtension = Extension.create<ToolbarOptions>({
9090
addProseMirrorPlugins () {
91-
return [ToolbarControlPlugin(this.editor, this.options)]
91+
return [LoadingStatePlugin(), ToolbarControlPlugin(this.editor, this.options)]
9292
}
9393
})
9494

@@ -98,6 +98,67 @@ export interface ToolbarControlTxMeta {
9898
providers?: Array<ToolbarProvider<any>>
9999
}
100100

101+
export const loadingStatePluginKey = new PluginKey('loadingState')
102+
103+
export interface LoadingStatePluginState {
104+
loadingNodes: Set<number>
105+
}
106+
107+
export interface LoadingStateTxMeta {
108+
setLoading?: { pos: number, loading: boolean }
109+
}
110+
111+
export function LoadingStatePlugin (): Plugin<LoadingStatePluginState> {
112+
return new Plugin<LoadingStatePluginState>({
113+
key: loadingStatePluginKey,
114+
115+
state: {
116+
init: () => ({
117+
loadingNodes: new Set<number>()
118+
}),
119+
120+
apply (tr, prevState) {
121+
const meta = tr.getMeta(loadingStatePluginKey) as LoadingStateTxMeta | undefined
122+
let loadingNodes = prevState.loadingNodes
123+
124+
// Handle loading state changes
125+
if (meta?.setLoading !== undefined) {
126+
loadingNodes = new Set(loadingNodes)
127+
if (meta.setLoading.loading) {
128+
loadingNodes.add(meta.setLoading.pos)
129+
} else {
130+
loadingNodes.delete(meta.setLoading.pos)
131+
}
132+
}
133+
134+
// Map positions through document changes
135+
if (tr.docChanged && loadingNodes.size > 0) {
136+
const mapped = new Set<number>()
137+
loadingNodes.forEach((pos) => {
138+
try {
139+
const mappedPos = tr.mapping.map(pos, -1)
140+
// Verify position is still valid
141+
if (mappedPos >= 0 && mappedPos < tr.doc.content.size) {
142+
mapped.add(mappedPos)
143+
}
144+
} catch (e) {
145+
// Position no longer valid, skip it
146+
console.debug('LoadingStatePlugin: position no longer valid', pos)
147+
}
148+
})
149+
loadingNodes = mapped
150+
}
151+
152+
return { loadingNodes }
153+
}
154+
}
155+
})
156+
}
157+
158+
export function getLoadingState (state: EditorState): LoadingStatePluginState | undefined {
159+
return loadingStatePluginKey.getState(state) as LoadingStatePluginState | undefined
160+
}
161+
101162
export const toolbarPluginKey = new PluginKey('dynamicToolbar')
102163

103164
export interface ToolbarControlPluginState {
@@ -326,6 +387,7 @@ export function ToolbarControlPlugin (editor: Editor, options: ToolbarOptions):
326387
editor.on('transaction', ({ editor, transaction: tr }) => {
327388
const meta = tr.getMeta(toolbarPluginKey) as ToolbarControlTxMeta
328389
const loadingState = tr.getMeta('loadingState') as boolean | undefined
390+
329391
if (meta?.cursor !== undefined && !eqCursors(currCursor, meta.cursor)) {
330392
prevCursor = currCursor
331393
currCursor = meta.cursor !== null ? { ...meta.cursor } : null
@@ -506,17 +568,22 @@ function updateCursorFromMouseEvent (view: EditorView, event: MouseEvent): void
506568
}
507569

508570
function scanForLoadingState (view: EditorView, range: Range): boolean {
509-
let isLoading = false
510-
view.state.doc.nodesBetween(range.from, range.to, (node, pos) => {
511-
const element = view.nodeDOM(pos)
512-
if (!(element instanceof HTMLElement)) return
571+
if (range.from > range.to || range.from < 0) {
572+
return false
573+
}
513574

514-
if (element.dataset.loading === 'true') {
515-
isLoading = true
516-
return false
575+
const loadingState = getLoadingState(view.state)
576+
if (loadingState === undefined || loadingState.loadingNodes.size === 0) {
577+
return false
578+
}
579+
580+
for (const pos of loadingState.loadingNodes) {
581+
if (pos >= range.from && pos <= range.to) {
582+
return true
517583
}
518-
})
519-
return isLoading
584+
}
585+
586+
return false
520587
}
521588

522589
function scanForAnchor (view: EditorView, range: Range): HTMLElement | undefined {
@@ -653,15 +720,12 @@ function minmax (value = 0, min = 0, max = 0): number {
653720
return Math.min(Math.max(value, min), max)
654721
}
655722

656-
export function setLoadingState (view: EditorView, element: HTMLElement, loading: boolean): void {
657-
if (loading) {
658-
element.setAttribute('data-loading', 'true')
659-
} else {
660-
element.removeAttribute('data-loading')
661-
requestAnimationFrame(() => {
662-
view.dispatch(view.state.tr.setMeta('loadingState', loading))
663-
})
723+
export function setLoadingState (view: EditorView, pos: number, loading: boolean): void {
724+
const meta: LoadingStateTxMeta = {
725+
setLoading: { pos, loading }
664726
}
727+
728+
view.dispatch(view.state.tr.setMeta(loadingStatePluginKey, meta).setMeta('loadingState', loading))
665729
}
666730

667731
export const GeneralToolbarProvider: ToolbarProvider<any> = {

0 commit comments

Comments
 (0)