UNPKG

quill-react-commercial

Version:
222 lines (204 loc) 8.71 kB
// Copy/paste or drag images into the rich-text editor. /* 原改编自 'quill-image-drop-module',主要Bug是 1. Quill已支持Drop成Base64图片,当本Module运行时Drop会插入两次base64图片 2. paste到table中还是Base64 3. 无法上传到服务器 */ import { getI18nText } from '../i18n'; import { throttle } from '../utils'; // 新的Module原理:在text-change时将所有编辑器中有base64的图片都上传,增加uploading状态 // https://github.com/quilljs/quill/issues/1089#issuecomment-614313509 export class ImageDrop { /** * Instantiate the module given a quill instance and any options * @param {Quill} quill * @param {Object} options */ constructor(quill, options = {}) { this.quill = quill; // save the quill reference this.options = options; this.editorContainer = quill.root.parentNode; this.imageHandler = options.imageHandler; // 添加图片上传方法 this.uploadedImgsList = options.uploadedImgsList; // if (this.uploadTimer) clearTimeout(this.uploadTimer); this.quill.on('text-change', (delta, oldDelta, source) => { if (this.imageHandler && this.imageHandler.imgUploadApi) { throttle(() => { const imgs = quill.container.querySelectorAll( 'img[src^="data:"]:not(img[data-status=uploading]):not(img[data-status=fail])', ); if (imgs && imgs.length > 0) { imgs.forEach((img) => { this.uploadBase64Img(img); }); } })(); } // // 当删除图片,该图片的失败tooltip应该删除,此tooltip是在ImageDrop Module中添加 // for (let i = 0; i < delta.ops.length; i++) { // if (delta.ops[i].hasOwnProperty('delete') && source === 'user') { // // 判断所有 delete 的 img url // const deleted = ImageDrop.getImgUrls(this.quill.getContents().diff(oldDelta)); // // getImgUrls(this.quill.getContents()) 能拿到所有现存 img url // if (deleted && deleted.length > 0) { // this.onDelete(); // } // } // } this.onDelete(); // 图片删除、图片后新增换行时去除格式 }); // this.b64ToUrl = this.b64ToUrl.bind(this); } uploadBase64Img(img) { console.log('upload img'); const base64Str = img.getAttribute('src'); if (typeof base64Str === 'string' && /data:image\/.*;base64,/.test(base64Str)) { const words = getI18nText(['imgStatusUploading', 'imgStatusFail'], this.options.i18n); img.setAttribute('data-status', 'uploading'); img.parentNode.classList.add('img-container'); // 只能给图片的父元素添加 after 伪元素,img无法添加 img.parentNode.setAttribute('data-after', words[0]); // const statusDiv = document.createElement('div'); // statusDiv.classList.add('img-status', 'uploading'); // statusDiv.innerHTML = '<p>图片上传中...</p>'; // statusDiv.style.cssText = `left:${img.offsetLeft}px;top:${img.offsetTop}px;display:block;`; // this.editorContainer.append(statusDiv); const { uploadSuccCB, uploadFailCB } = this.imageHandler; this.b64ToUrl(base64Str) .then((url) => { img.setAttribute('src', url); img.setAttribute('data-status', 'success'); img.parentNode.setAttribute('data-after', ''); this.uploadedImgsList.push(url); if (uploadSuccCB) uploadSuccCB(url); }) .catch((error) => { console.log(error); img.setAttribute('data-status', 'fail'); img.parentNode.setAttribute('data-after', words[1]); if (uploadFailCB) uploadFailCB(error); img.parentNode.onclick = () => this.uploadBase64Img(img); }); } } onDelete() { const allContainers = this.quill.container.querySelectorAll('.img-container[data-after]'); if (allContainers && allContainers.length > 0) { allContainers.forEach((container) => { const imgs = container.querySelectorAll('img[src^="data:"]'); if (!imgs || imgs.length === 0) { container.removeAttribute('data-after'); container.removeAttribute('class'); } }); } // const imgTooltip = this.quill.root.parentNode.querySelectorAll('div.upload-fail'); // if (imgTooltip.length > 0) { // const base64Img = this.quill.container.querySelectorAll('img[src^="data:"]'); // let classList = []; // base64Img.forEach((img) => { // classList = classList.concat(img.className.split(' ')); // }); // imgTooltip.forEach((tooltip) => { // const match = tooltip.className.split(' ').filter((css) => /i-\w/.test(css)); // if (!classList.includes(match[0])) this.quill.root.parentNode.removeChild(tooltip); // }); // } } b64ToUrl(base64) { return new Promise((resolve, reject) => { // Split the base64 string in data and contentType const block = base64.split(';'); // Get the content type of the image const contentType = block[0].split(':')[1]; // get the real base64 content of the file const realData = block[1].split(',')[1]; // Convert it to a blob to upload const blob = ImageDrop.b64toBlob(realData, contentType); const { imgUploadApi } = this.imageHandler; ImageDrop.uploadImg( blob, imgUploadApi, (url) => { resolve(url); }, (error) => { reject(error); }, ); }); } // 获取 delta 中所有图片 URL:https://github.com/quilljs/quill/issues/2041#issuecomment-492488030 static getImgUrls(delta) { return delta.ops.filter((i) => i.insert && i.insert.image).map((i) => i.insert.image); } /** * Convert a base64 string in a Blob according to the data and contentType. * * @param b64Data {String} Pure base64 string without contentType * @param contentType {String} the content type of the file i.e (image/jpeg - image/png - text/plain) * @param sliceSize {Int} SliceSize to process the byteCharacters * @see http://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript * @return Blob */ static b64toBlob(b64Data, contentType, sliceSize) { contentType = contentType || ''; sliceSize = sliceSize || 512; const byteCharacters = atob(b64Data); const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { const slice = byteCharacters.slice(offset, offset + sliceSize); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } return new Blob(byteArrays, { type: contentType }); } static uploadImg = (blob, imgUploadApi, uploadSuccCB, uploadFailCB) => { try { const formData = new FormData(); formData.append('file', blob, blob.name || `default.${blob.type.split('/')[1]}`); imgUploadApi(formData) .then((url) => { uploadSuccCB(url); }) .catch((error) => { console.log('upload img error: ', error); uploadFailCB(error); }); } catch (e) { console.log('uploadImg: ', e); uploadFailCB(e); } }; // showFailTooltip(img, id) { // if (img) { // const base64Str = img.getAttribute('src'); // if (typeof base64Str === 'string' && /data:image\/.*;base64,/.test(base64Str)) { // const rect = img.getBoundingClientRect(); // const tooltip = this.editorContainer.querySelector(`div.i-${id}`); // const tooltip = img.parent.querySelector('.img-status'); // img.parent.style.position = 'relative'; // const containerRect = img.parent.getBoundingClientRect(); // if (tooltip) { // tooltip.style.cssText = `${tooltip.style.cssText}left:${ // rect.left - containerRect.left - 1 + this.editorContainer.scrollLeft // }px;top:${rect.top - containerRect.top + this.editorContainer.scrollTop}px;max-height:${ // img.height // }px;max-width:${img.width}px;display: block;`; // // setTimeout(() => { // // tooltip.style.display = 'none'; // // }, 2000); // } // } else { // // 已经上传成功的图片删除失败tooltip // img.parentNode.removeChild( // // this.quill.root.parentNode.querySelector(`div.i-${id}`), // img.parent.querySelector('.img-status'), // ); // } // } // } }