UNPKG

@cartamd/plugin-attachment

Version:

This plugin adds support for attachments. Install it using:

181 lines (180 loc) 6.78 kB
import { get, writable } from 'svelte/store'; import DropOverlay from './DropOverlay.svelte'; import AttachIcon from './icons/AttachIcon.svelte'; import LoadingOverlay from './LoadingOverlay.svelte'; const ImageMimeTypes = [ 'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif' ]; /** * Carta attachment plugin. */ export const attachment = (options) => { let carta; const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes; const draggingOverTextArea = writable(false); const draggingOverOverlay = writable(false); const uploadingFiles = writable([]); const draggingOver = writable(false); draggingOverTextArea.subscribe((v) => draggingOver.set(v || get(draggingOverOverlay))); draggingOverOverlay.subscribe((v) => draggingOver.set(v || get(draggingOverTextArea))); draggingOver.subscribe((v) => { if (!carta) return; // Add custom class to editor if (!carta.element) return; if (v) carta.element.classList.add('dragging-attachment'); else carta.element.classList.remove('dragging-attachment'); }); async function handleFile(file) { if (!allowedMimeTypes.includes(file.type)) return; if (!carta?.input) return; const input = carta.input; const textarea = input.textarea; let pos = input.textarea.selectionStart; const loadingStr = `[Uploading ${file.name}](loading)`; const isImage = ImageMimeTypes.includes(file.type); if (isImage) { // assume images are being inserted as blocks const line = carta.input.getLine(); pos = line.end; if (line.value) { input.insertAt(pos, '\n\n'); pos += 2; } input.insertAt(pos, loadingStr + '\n'); pos += loadingStr.length + 1; } else { // non image attachments are inline (could make multiple into comma separated list or bullets) carta.input.insertAt(pos, loadingStr + ' '); pos += loadingStr.length + 1; } carta.input.update(); textarea.setSelectionRange(pos, pos); uploadingFiles.update((files) => [...files, file]); const path = await options.upload(file); uploadingFiles.update((files) => files.filter((f) => f !== file)); // Remove loading string const value = carta.input.textarea.value; const loadingStrIndex = value.indexOf(loadingStr); if (loadingStrIndex !== -1) carta.input.removeAt(loadingStrIndex, loadingStr.length); carta.input.update(); textarea.setSelectionRange(loadingStrIndex, loadingStrIndex); if (!path) return; const str = isImage ? `![${file.name}](${path})` : `[${file.name}](${path})`; carta.input.insertAt(loadingStrIndex, str); carta.input.update(); // update cursor position to account for the string replacement if (input.textarea.selectionStart < loadingStrIndex) { // caret is before the loading string, no change required pos = input.textarea.selectionStart; } else if (input.textarea.selectionStart >= loadingStrIndex + str.length) { // caret is after the loading string, adjust position by the difference pos = input.textarea.selectionStart - loadingStr.length + str.length; } else if (input.textarea.selectionStart >= loadingStrIndex) { // caret is within the loading string, position it just after pos = loadingStrIndex + str.length + 1; } textarea.setSelectionRange(pos, pos); carta.input.history.saveState(textarea.value, textarea.selectionStart); return; } function handleDrop(e) { e.preventDefault(); draggingOverOverlay.set(false); draggingOverTextArea.set(false); const files = e.dataTransfer?.files; if (!files) return; // TODO: // Files may not be processed in order, so move cursor to position of last once completed // but account for user potentially moving cursor / editing while uploads are in progress. for (const file of files) handleFile(file); } function handlePaste(e) { const items = e.clipboardData?.items; if (!items) return; const itemsArray = Array.from(items); for (const item of itemsArray) { if (item.kind === 'file') { const file = item.getAsFile(); if (!file) continue; e.preventDefault(); handleFile(file); } } } return { onLoad: ({ carta: c }) => { carta = c; }, listeners: [ ['drop', handleDrop, false], ['dragenter', () => draggingOverTextArea.set(true)], ['dragleave', () => draggingOverTextArea.set(false)], ['dragover', (e) => e.preventDefault()], ['paste', handlePaste, false] ], components: [ { component: DropOverlay, props: { draggingOverTextArea, draggingOverOverlay, handleDrop, dropOverlay: options.dropOverlay }, parent: 'input' }, { component: LoadingOverlay, props: { uploadingFiles, loadingOverlay: options.loadingOverlay }, parent: 'input' } ], icons: options.disableIcon ? [] : [ { component: AttachIcon, action() { // Tricky way to open file selection dialog const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.accept = allowedMimeTypes.join(', '); input.onchange = (e) => { const files = e.target?.files; if (!files) return; for (const file of files) handleFile(file); }; input.click(); }, id: 'attach', label: 'Attach file' } ] }; };