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(async ({path, url}: { path: string; url: string }) => {
await 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;
}
/** Clear every staged attachment preview and reset the backing form input. */
public reset() {
this._attachmentContainer.replaceChildren();
const attachments = this._options.attachmentInput;
if (attachments) {
attachments.value = '';
}
}
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): Promise {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('Content-Type', file.type);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`[Quill Attachment Module] Failed to upload file. Status: ${xhr.status}`));
}
};
xhr.onerror = () => {
reject(new Error(`[Quill Attachment Module] Error occurred while uploading file.`));
};
xhr.send(file);
});
}
}