import type Quill from 'quill'; import type Toolbar from "../toolbar/toolbar"; interface AttachmentOptions { upload: (file: File) => Promise<{ path: any; url: any; filename: any }>; onFileUploaded?: (node: HTMLElement, {url}: { url: string }) => void; attachmentInput?: HTMLInputElement; } const generateId = () => { const name = 'attachment'; const id = new Date().getTime(); return `${name}_${id}`; }; export default class Attachment { private _quill: Quill; private _options: AttachmentOptions; private _fileHolder: HTMLInputElement | null; constructor(quill: Quill, options: AttachmentOptions) { this._quill = quill; this._options = options; if (typeof (this._options.upload) !== "function") { console.warn("[Quill Attachment Module] No upload function provided"); } if (typeof (this._options.onFileUploaded) !== "function") { console.warn("[Quill Attachment Module] No file uploaded function provided"); } (this._quill .getModule('toolbar') as Toolbar) .addHandler('attachment', this._selectLocalImage.bind(this)); this._createAttachmentContainer(); } private _selectLocalImage() { this._fileHolder = document.createElement('input'); this._fileHolder.setAttribute('type', 'file'); this._fileHolder.setAttribute('accept', '*/*'); this._fileHolder.onchange = this._fileChanged.bind(this); this._fileHolder.click(); } private _fileChanged() { if (!this._fileHolder?.files || this._fileHolder.files.length < 0) { return; } const file = this._fileHolder.files[0]; this.addAttachment(file); } public addAttachment(file: File, dataUrl?: string) { const attachmentId = generateId(); const insertWithDataUrl = (base64: string) => { this._insertAttachment({ dataUrl: base64, file, id: attachmentId }); }; if (dataUrl) { insertWithDataUrl(dataUrl); } else { const fileReader = new FileReader(); fileReader.addEventListener('load', () => { const base64Content = fileReader.result as string; insertWithDataUrl(base64Content); }, false); fileReader.readAsDataURL(file); } if (typeof this._options.upload === 'function') { this._options.upload(file).then(({ path, url }: { path: string; url: string }) => { this._uploadAttachment(file, url); this._updateAttachment(attachmentId, url, path); }).catch((err: { message: string }) => { console.warn(err.message); }); } } private _attachmentContainer = document.createElement('div'); private _createAttachmentContainer() { const attachmentContainer = this._attachmentContainer; attachmentContainer.id = 'attachment-container'; attachmentContainer.style.display = 'flex'; attachmentContainer.style.flexWrap = 'wrap'; attachmentContainer.style.gap = '10px'; attachmentContainer.style.position = 'absolute'; attachmentContainer.style.bottom = '0'; attachmentContainer.style.left = '0'; attachmentContainer.style.right = '0'; attachmentContainer.style.padding = '10px'; this._quill.container.appendChild(attachmentContainer); } private _insertAttachment({dataUrl, file, id}: { dataUrl: string; file: File; id: string }) { this._attachmentContainer.appendChild(this._createAttachment(dataUrl, file, id)); } private _updateAttachment(id: string, url: string, filename: string) { const element = this._quill.container.querySelector(`#${id}`); if (!element) return; element.setAttribute('href', url); const attachmentName = element.querySelector('.attachment-name'); if (attachmentName) { if (filename) { attachmentName.textContent = filename; } else { attachmentName.textContent = 'Error uploading file'; } } if (this._options.onFileUploaded) { this._options.onFileUploaded(element as HTMLElement, {url}); } // add the url to the hidden input const attachments = this._options.attachmentInput; if (attachments && filename) { // value should be an array of attachment names const value = attachments.value; const data: string[] = value ? JSON.parse(value) as string[] : []; data.push(filename); attachments.value = JSON.stringify(data); } } private _createAttachment(dataUrl: string, file: File, id: string) { const attachment = document.createElement('a'); attachment.setAttribute('href', dataUrl); attachment.setAttribute('id', id); attachment.setAttribute('download', file.name); attachment.style.padding = '10px'; attachment.style.backgroundColor = 'white'; attachment.style.border = '1px solid #ccc'; attachment.style.borderRadius = '5px'; attachment.style.display = 'flex'; attachment.style.alignItems = 'center'; attachment.style.gap = '5px'; const p = document.createElement('p'); p.classList.add('attachment-name'); p.textContent = 'Uploading...'; attachment.appendChild(p); // add a remove button const removeButton = document.createElement('button'); removeButton.innerHTML = ''; removeButton.onclick = (e) => { e.preventDefault(); e.stopImmediatePropagation(); this._removeAttachment(id); }; attachment.appendChild(removeButton); return attachment; } private _removeAttachment(id: string) { const attachment = this._quill.container.querySelector(`#${id}`); if (attachment) { attachment.remove(); } // remove the attachment from the hidden input const attachments = this._options.attachmentInput; if (attachments) { // value should be an array of attachment names const value = attachments.value; const attachmentName = attachment?.querySelector('.attachment-name'); const data: string[] = value ? JSON.parse(value) as string[] : []; const index = data.indexOf(attachmentName?.textContent ? attachmentName.textContent : ''); if (index > -1) { data.splice(index, 1); attachments.value = JSON.stringify(data); } } } private _uploadAttachment(file: File, url: string) { const xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('Content-Type', file.type); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { console.log(`[Quill Attachment Module] File uploaded successfully to ${url}`); } else { console.error(`[Quill Attachment Module] Failed to upload file. Status: ${xhr.status}`); } }; xhr.onerror = () => { console.error(`[Quill Attachment Module] Error occurred while uploading file.`); }; xhr.send(file); } }