Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export interface EmbedNodeProvider {
autoEmbedUrl?: (src: string) => boolean
}

export type EmbedNodeView = (editor: Editor, root: HTMLDivElement) => EmbedNodeViewHandle | undefined
export type EmbedNodeView = (
editor: Editor,
root: HTMLDivElement,
getPos: () => number
) => EmbedNodeViewHandle | undefined
export type EmbedNodeProviderConstructor<T> = (options: T) => EmbedNodeProvider

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

addNodeView () {
return ({ node, HTMLAttributes, editor }) => {
return ({ node, HTMLAttributes, editor, getPos }) => {
const providerPromise = matchUrl(this.options.providers, node.attrs.src)

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

setLoadingState(editor.view, root, true)
const pos = typeof getPos === 'function' ? getPos() : 0
setLoadingState(editor.view, pos, true)
root.setAttribute('block-editor-blur', 'true')

let handle: EmbedNodeViewHandle | undefined

void providerPromise
.then((view) => {
view = view ?? StubEmbedNodeView
handle = view(editor, root)
handle = view(editor, root, getPos)
if (handle !== undefined) {
root.classList.add(`embed-${handle.name}`)
}
})
.finally(() => {
setLoadingState(editor.view, root, false)
const pos = typeof getPos === 'function' ? getPos() : 0
setLoadingState(editor.view, pos, false)
})

return {
Expand Down Expand Up @@ -500,7 +506,7 @@ export function replacePreviewContent (
return tr
}

const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLElement) => {
const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLDivElement, getPos: () => number) => {
const hint = document.createElement('p')
const hintIcon = hint.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
const hintSpan = hint.appendChild(document.createElement('span'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ export const DriveEmbedProvider: EmbedNodeProviderConstructor<DriveEmbedOptions>

if (previewType === undefined) return

return (editor: Editor, root: HTMLDivElement) => {
return (editor: Editor, root: HTMLDivElement, getPos: () => number) => {
const setLoading = (loading: boolean): void => {
setLoadingState(editor.view, root, loading)
const pos = typeof getPos === 'function' ? getPos() : 0
setLoadingState(editor.view, pos, loading)
}
const renderer = new SvelteRenderer(FilePreview as any, {
element: root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const YoutubeEmbedProvider: EmbedNodeProviderConstructor<YoutubeEmbedUrlO
const url = getEmbedUrlFromYoutubeUrl(src, options)
if (url === undefined) return

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

const iframe = document.createElement('iframe')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
addNodeView () {
const imageSrcCache = new Map<string, { src: string, srcset: string }>()

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

setLoadingState(view, container, true)
const pos = typeof getPos === 'function' ? getPos() : 0
setLoadingState(view, pos, true)
const setImageProps = (src: string | null, srcset: string | null): void => {
if (src != null) imgElement.src = src
if (srcset != null) imgElement.srcset = srcset
void imgElement.decode().finally(() => {
setLoadingState(view, container, false)
const pos = typeof getPos === 'function' ? getPos() : 0
setLoadingState(view, pos, false)
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export interface SelectionCursorContext {

export const ToolbarExtension = Extension.create<ToolbarOptions>({
addProseMirrorPlugins () {
return [ToolbarControlPlugin(this.editor, this.options)]
return [LoadingStatePlugin(), ToolbarControlPlugin(this.editor, this.options)]
}
})

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

export const loadingStatePluginKey = new PluginKey('loadingState')

export interface LoadingStatePluginState {
loadingNodes: Set<number>
}

export interface LoadingStateTxMeta {
setLoading?: { pos: number, loading: boolean }
}

export function LoadingStatePlugin (): Plugin<LoadingStatePluginState> {
return new Plugin<LoadingStatePluginState>({
key: loadingStatePluginKey,

state: {
init: () => ({
loadingNodes: new Set<number>()
}),

apply (tr, prevState) {
const meta = tr.getMeta(loadingStatePluginKey) as LoadingStateTxMeta | undefined
let loadingNodes = prevState.loadingNodes

// Handle loading state changes
if (meta?.setLoading !== undefined) {
loadingNodes = new Set(loadingNodes)
if (meta.setLoading.loading) {
loadingNodes.add(meta.setLoading.pos)
} else {
loadingNodes.delete(meta.setLoading.pos)
}
}

// Map positions through document changes
if (tr.docChanged && loadingNodes.size > 0) {
const mapped = new Set<number>()
loadingNodes.forEach((pos) => {
try {
const mappedPos = tr.mapping.map(pos, -1)
// Verify position is still valid
if (mappedPos >= 0 && mappedPos < tr.doc.content.size) {
mapped.add(mappedPos)
}
} catch (e) {
// Position no longer valid, skip it
console.debug('LoadingStatePlugin: position no longer valid', pos)
}
})
loadingNodes = mapped
}

return { loadingNodes }
}
}
})
}

export function getLoadingState (state: EditorState): LoadingStatePluginState | undefined {
return loadingStatePluginKey.getState(state) as LoadingStatePluginState | undefined
}

export const toolbarPluginKey = new PluginKey('dynamicToolbar')

export interface ToolbarControlPluginState {
Expand Down Expand Up @@ -326,6 +387,7 @@ export function ToolbarControlPlugin (editor: Editor, options: ToolbarOptions):
editor.on('transaction', ({ editor, transaction: tr }) => {
const meta = tr.getMeta(toolbarPluginKey) as ToolbarControlTxMeta
const loadingState = tr.getMeta('loadingState') as boolean | undefined

if (meta?.cursor !== undefined && !eqCursors(currCursor, meta.cursor)) {
prevCursor = currCursor
currCursor = meta.cursor !== null ? { ...meta.cursor } : null
Expand Down Expand Up @@ -506,17 +568,22 @@ function updateCursorFromMouseEvent (view: EditorView, event: MouseEvent): void
}

function scanForLoadingState (view: EditorView, range: Range): boolean {
let isLoading = false
view.state.doc.nodesBetween(range.from, range.to, (node, pos) => {
const element = view.nodeDOM(pos)
if (!(element instanceof HTMLElement)) return
if (range.from > range.to || range.from < 0) {
return false
}

if (element.dataset.loading === 'true') {
isLoading = true
return false
const loadingState = getLoadingState(view.state)
if (loadingState === undefined || loadingState.loadingNodes.size === 0) {
return false
}

for (const pos of loadingState.loadingNodes) {
if (pos >= range.from && pos <= range.to) {
return true
}
})
return isLoading
}

return false
}

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

export function setLoadingState (view: EditorView, element: HTMLElement, loading: boolean): void {
if (loading) {
element.setAttribute('data-loading', 'true')
} else {
element.removeAttribute('data-loading')
requestAnimationFrame(() => {
view.dispatch(view.state.tr.setMeta('loadingState', loading))
})
export function setLoadingState (view: EditorView, pos: number, loading: boolean): void {
const meta: LoadingStateTxMeta = {
setLoading: { pos, loading }
}

view.dispatch(view.state.tr.setMeta(loadingStatePluginKey, meta).setMeta('loadingState', loading))
}

export const GeneralToolbarProvider: ToolbarProvider<any> = {
Expand Down
Loading