|
|
|
@ -56,6 +56,22 @@ function handleUploadError(error: unknown, options: ImageUploadOptions): void { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function findPlaceholderPos(doc: ProseMirrorNode, blobUrl: string): number { |
|
|
|
let found = -1; |
|
|
|
doc.descendants((node: ProseMirrorNode, offset: number) => { |
|
|
|
if (found !== -1) return false; |
|
|
|
if ( |
|
|
|
node.type.name === 'image' && |
|
|
|
node.attrs.src === blobUrl && |
|
|
|
node.attrs['data-uploading'] === 'true' |
|
|
|
) { |
|
|
|
found = offset; |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
return found; |
|
|
|
} |
|
|
|
|
|
|
|
interface UploadContext { |
|
|
|
blobUrl: string; |
|
|
|
pos: number; |
|
|
|
@ -85,63 +101,33 @@ function createUploadProcess( |
|
|
|
}) |
|
|
|
.run(); |
|
|
|
|
|
|
|
// Find the node we just inserted to track its position
|
|
|
|
let nodePos = insertPos; |
|
|
|
const { doc } = editor.state; |
|
|
|
doc.descendants((node: ProseMirrorNode, offset: number) => { |
|
|
|
if ( |
|
|
|
node.type.name === 'image' && |
|
|
|
node.attrs.src === blobUrl && |
|
|
|
node.attrs['data-uploading'] === 'true' |
|
|
|
) { |
|
|
|
nodePos = offset; |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
const nodePos = findPlaceholderPos(editor.state.doc, blobUrl); |
|
|
|
|
|
|
|
const uploadContext: UploadContext = { blobUrl, pos: nodePos }; |
|
|
|
|
|
|
|
options |
|
|
|
.upload(file, (percent: number) => { |
|
|
|
// Update progress attribute on the placeholder image
|
|
|
|
const currentState = editor.state; |
|
|
|
let currentPos = -1; |
|
|
|
currentState.doc.descendants((node: ProseMirrorNode, offset: number) => { |
|
|
|
if ( |
|
|
|
node.type.name === 'image' && |
|
|
|
node.attrs.src === blobUrl && |
|
|
|
node.attrs['data-uploading'] === 'true' |
|
|
|
) { |
|
|
|
currentPos = offset; |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
if (editor.isDestroyed) return; |
|
|
|
|
|
|
|
const currentPos = findPlaceholderPos(editor.state.doc, blobUrl); |
|
|
|
if (currentPos === -1) return; |
|
|
|
|
|
|
|
const node = currentState.doc.nodeAt(currentPos); |
|
|
|
const node = editor.state.doc.nodeAt(currentPos); |
|
|
|
if (!node) return; |
|
|
|
|
|
|
|
const transaction = currentState.tr.setNodeMarkup(currentPos, undefined, { |
|
|
|
const transaction = editor.state.tr.setNodeMarkup(currentPos, undefined, { |
|
|
|
...node.attrs, |
|
|
|
'data-upload-progress': percent, |
|
|
|
}); |
|
|
|
editor.view.dispatch(transaction); |
|
|
|
}) |
|
|
|
.then((url: string) => { |
|
|
|
// Replace blob URL with real URL and remove uploading attributes
|
|
|
|
const currentState = editor.state; |
|
|
|
let currentPos = -1; |
|
|
|
currentState.doc.descendants((node: ProseMirrorNode, offset: number) => { |
|
|
|
if ( |
|
|
|
node.type.name === 'image' && |
|
|
|
node.attrs.src === blobUrl && |
|
|
|
node.attrs['data-uploading'] === 'true' |
|
|
|
) { |
|
|
|
currentPos = offset; |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
if (editor.isDestroyed) { |
|
|
|
URL.revokeObjectURL(blobUrl); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const currentPos = findPlaceholderPos(editor.state.doc, blobUrl); |
|
|
|
|
|
|
|
if (currentPos === -1) { |
|
|
|
blobUrlTracker?.delete(blobUrl); |
|
|
|
@ -149,14 +135,14 @@ function createUploadProcess( |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const node = currentState.doc.nodeAt(currentPos); |
|
|
|
const node = editor.state.doc.nodeAt(currentPos); |
|
|
|
if (!node) { |
|
|
|
blobUrlTracker?.delete(blobUrl); |
|
|
|
URL.revokeObjectURL(blobUrl); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const transaction = currentState.tr.setNodeMarkup(currentPos, undefined, { |
|
|
|
const transaction = editor.state.tr.setNodeMarkup(currentPos, undefined, { |
|
|
|
...node.attrs, |
|
|
|
'data-upload-progress': null, |
|
|
|
'data-uploading': null, |
|
|
|
@ -167,24 +153,17 @@ function createUploadProcess( |
|
|
|
URL.revokeObjectURL(blobUrl); |
|
|
|
}) |
|
|
|
.catch((error: unknown) => { |
|
|
|
// Remove placeholder image on failure
|
|
|
|
const currentState = editor.state; |
|
|
|
let currentPos = -1; |
|
|
|
currentState.doc.descendants((node: ProseMirrorNode, offset: number) => { |
|
|
|
if ( |
|
|
|
node.type.name === 'image' && |
|
|
|
node.attrs.src === blobUrl && |
|
|
|
node.attrs['data-uploading'] === 'true' |
|
|
|
) { |
|
|
|
currentPos = offset; |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
if (editor.isDestroyed) { |
|
|
|
URL.revokeObjectURL(blobUrl); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const currentPos = findPlaceholderPos(editor.state.doc, blobUrl); |
|
|
|
|
|
|
|
if (currentPos !== -1) { |
|
|
|
const transaction = currentState.tr.delete( |
|
|
|
const transaction = editor.state.tr.delete( |
|
|
|
currentPos, |
|
|
|
currentPos + (currentState.doc.nodeAt(currentPos)?.nodeSize ?? 1), |
|
|
|
currentPos + (editor.state.doc.nodeAt(currentPos)?.nodeSize ?? 1), |
|
|
|
); |
|
|
|
editor.view.dispatch(transaction); |
|
|
|
} |
|
|
|
@ -208,23 +187,15 @@ function createCustomImage( |
|
|
|
'data-upload-progress': { |
|
|
|
default: null, |
|
|
|
parseHTML: (element) => element.dataset.uploadProgress, |
|
|
|
renderHTML: (attributes) => { |
|
|
|
if ( |
|
|
|
attributes['data-upload-progress'] === null || |
|
|
|
attributes['data-upload-progress'] === undefined |
|
|
|
) |
|
|
|
return {}; |
|
|
|
return { |
|
|
|
'data-upload-progress': attributes['data-upload-progress'], |
|
|
|
}; |
|
|
|
renderHTML: () => { |
|
|
|
return {}; |
|
|
|
}, |
|
|
|
}, |
|
|
|
'data-uploading': { |
|
|
|
default: null, |
|
|
|
parseHTML: (element) => element.dataset.uploading, |
|
|
|
renderHTML: (attributes) => { |
|
|
|
if (!attributes['data-uploading']) return {}; |
|
|
|
return { 'data-uploading': attributes['data-uploading'] }; |
|
|
|
renderHTML: () => { |
|
|
|
return {}; |
|
|
|
}, |
|
|
|
}, |
|
|
|
}; |
|
|
|
@ -325,6 +296,7 @@ function createCustomImage( |
|
|
|
|
|
|
|
document.body.append(input); |
|
|
|
input.click(); |
|
|
|
return true; |
|
|
|
}, |
|
|
|
}; |
|
|
|
}, |
|
|
|
@ -339,18 +311,29 @@ function createCustomImage( |
|
|
|
handleDrop: (view: EditorView, event: DragEvent) => { |
|
|
|
if (!event.dataTransfer?.files.length) return false; |
|
|
|
|
|
|
|
const file = event.dataTransfer.files[0]; |
|
|
|
if (!file || !file.type.startsWith('image/')) return false; |
|
|
|
const imageFiles = [...event.dataTransfer.files].filter((f) => |
|
|
|
f.type.startsWith('image/'), |
|
|
|
); |
|
|
|
if (imageFiles.length === 0) return false; |
|
|
|
|
|
|
|
event.preventDefault(); |
|
|
|
|
|
|
|
// Only support single image upload
|
|
|
|
const file = imageFiles[0]; |
|
|
|
if (!file) return false; |
|
|
|
if (imageFiles.length > 1) { |
|
|
|
handleUploadError( |
|
|
|
new Error($t('ui.tiptap.upload.onlySingleImage')), |
|
|
|
imageUpload, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const error = validateFile(file, imageUpload); |
|
|
|
if (error) { |
|
|
|
handleUploadError(new Error(error), imageUpload); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// Calculate drop position
|
|
|
|
const coordinates = view.posAtCoords({ |
|
|
|
left: event.clientX, |
|
|
|
top: event.clientY, |
|
|
|
@ -376,18 +359,27 @@ function createCustomImage( |
|
|
|
const items = event.clipboardData?.items; |
|
|
|
if (!items) return false; |
|
|
|
|
|
|
|
let imageFile: File | undefined; |
|
|
|
const imageFiles: File[] = []; |
|
|
|
for (const item of items) { |
|
|
|
if (item.type.startsWith('image/')) { |
|
|
|
imageFile = item.getAsFile() ?? undefined; |
|
|
|
break; |
|
|
|
const file = item.getAsFile(); |
|
|
|
if (file) imageFiles.push(file); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!imageFile) return false; |
|
|
|
if (imageFiles.length === 0) return false; |
|
|
|
|
|
|
|
event.preventDefault(); |
|
|
|
|
|
|
|
const imageFile = imageFiles[0]; |
|
|
|
if (!imageFile) return false; |
|
|
|
if (imageFiles.length > 1) { |
|
|
|
handleUploadError( |
|
|
|
new Error($t('ui.tiptap.upload.onlySingleImage')), |
|
|
|
imageUpload, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const error = validateFile(imageFile, imageUpload); |
|
|
|
if (error) { |
|
|
|
handleUploadError(new Error(error), imageUpload); |
|
|
|
|