This commit is contained in:
c
2025-03-12 20:11:03 +08:00
commit 70b7c82c97
1178 changed files with 148185 additions and 0 deletions
@@ -0,0 +1,31 @@
export default function CustomClipboard (Quill) {
const Delta = Quill.import('delta')
const Clipboard = Quill.import('modules/clipboard')
class CustomClipboard extends Clipboard {
onPaste(e) {
const top = window.pageYOffset;
const left = window.pageXOffset;
if (e.defaultPrevented || !this.quill.isEnabled()) return;
let range = this.quill.getSelection();
let delta = new Delta().retain(range.index);
this.container.style.top = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0).toString() + 'px';
this.container.focus();
setTimeout(() => {
this.quill.selection.update(Quill.sources.SILENT);
delta = delta.concat(this.convert()).delete(range.length);
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);
let bounds = this.quill.getBounds(delta.length() - range.length, Quill.sources.SILENT);
this.quill.scrollingContainer.scrollTop = bounds.top;
// scroll window to previous position after paste
window.scrollTo({top, left});
}, 1);
}
}
return {
'modules/clipboard': CustomClipboard
}
}
@@ -0,0 +1,199 @@
import './image-uploading.css'
export default function (Quill) {
// const Delta = Quill.import('delta')
class ImageExtend {
constructor(quill, config) {
this.quill = quill
this.config = config
// 通过监听内容变化判断是否上传图片
quill.on(Quill.events.TEXT_CHANGE, (delta) => {
const imageElements = quill.container.querySelectorAll('img[data-local]:not(.uploading)')
imageElements.forEach(element => {
if (element.classList.contains('uploaded')) {
element.removeAttribute('data-local')
element.classList.remove('uploaded')
if(element.classList.length <= 0) {
element.removeAttribute('class')
}
} else {
this.uploader(element)
}
})
})
// 插入粘贴的文件图片
quill.root.addEventListener('paste', (e) => {
const files = Array.from(e.clipboardData.files || []);
if (files.length > 0) {
e.preventDefault();
setTimeout(() => {
this.insertFileImage(files[0])
}, 0)
}
}, false)
// 插入粘贴的网络图片
quill.clipboard.addMatcher('IMG', function (node, delta) {
if (!delta.ops[0].attributes) {
delta.ops[0].attributes = {}
}
delta.ops[0].attributes['data-local'] = node.src
delta.ops[0].attributes = Object.keys(delta.ops[0].attributes).reduce((attributes, key) => {
if (['data-local', 'alt'].includes(key)) {
attributes[key] = delta.ops[0].attributes[key]
}
return attributes
}, {})
return delta
});
}
async uploader(element) {
// 标记元素正在上传
element.classList.add('uploading')
// 检查是否配置了上传方法
if (typeof this.config.uploadHandler !== 'function') {
console.warn('没有配置图片上传方法,移除图片:' + element.src)
Quill.find(element).deleteAt(0)
return
}
const imageFile = await this.convertImageFile(element.src)
// 构建上传元素
const uploadNode = this.getUploadNode(element.src)
// 获取位置
const parent = this.quill.root.parentNode
setTimeout(() => {
const elementRect = element.getBoundingClientRect()
const containerRect = parent.getBoundingClientRect()
uploadNode.style.left = `${elementRect.left - containerRect.left - 1 + parent.scrollLeft}px`
uploadNode.style.top = `${elementRect.top - containerRect.top + parent.scrollTop}px`
uploadNode.style.width = `${elementRect.width}px`
uploadNode.style.height = `${elementRect.height}px`
// 渲染到element上
parent.appendChild(uploadNode)
}, 100)
// 上传图片 展示进度
Promise.resolve({
then: (resolve, reject) => {
try {
const handler = this.config.uploadHandler(imageFile, element)
resolve(handler)
} catch (e) {
reject(e)
}
}
}).then((url) => {
const customData = (element.getAttribute('data-custom') || '').split('&').filter(item => item).reduce((res, item) => {
const [key, value] = item.split('=')
res[key] = value
return res
}, {})
customData.source = url
element.src = url
element.removeAttribute('data-local')
element.classList.remove('uploading')
element.setAttribute('data-custom', Object.keys(customData).map(key => `${key}=${customData[key]}`).join('&'))
parent.removeChild(uploadNode)
}).catch((e) => {
parent.removeChild(uploadNode)
console.error(e)
})
}
async convertImageFile(imageUrl) {
const result = {
blob: imageUrl,
ext: 'jpg',
size: 0,
}
// if (!/^blob:/.test(imageUrl)) {
const image = await fetch(imageUrl)
const blob = await image.blob()
const mimeType = this.getBlobImageMimeType(blob, imageUrl)
const ext = mimeType.replace('image/', '')
const filename = `image.${ext}`
const file = new File([blob], filename, {type: mimeType})
result.blob = this.file2blob(file)
result.ext = ext
result.size = file.size
// }
return result
}
getBlobImageMimeType(blob, imageUrl) {
if (blob.type) {
return blob.type
}
if (imageUrl.match(/data:(image\/\w+);base64/)) {
return imageUrl.match(/data:(image\/\w+);base64/)[1].toLowerCase()
}
return 'image/jpeg'
}
getUploadNode(imageUrl) {
const div = document.createElement('div')
const img = document.createElement('img')
img.src = imageUrl
img.setAttribute('οndragstart', 'return false')
div.appendChild(img)
div.classList.add('image-uploading')
return div
}
file2blob(file) {
let url
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file);
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file);
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file);
}
return url
}
insertFileImage(file) {
const range = this.quill.getSelection(true)
const url = this.file2blob(file)
if (range.index > 0 && this.quill.getText(range.index - 1, 1) !== '\n') {
this.quill.insertText(range.index, '\n', Quill.sources.SILENT)
range.index += 1
}
this.quill.insertEmbed(range.index, 'image', url, Quill.sources.SILENT)
this.quill.formatText(range.index, 1, 'data-local', url, Quill.sources.SILENT)
this.quill.insertText(range.index + 1, '\n', Quill.sources.SILENT)
this.quill.setSelection(range.index + 2, Quill.sources.SILENT)
this.quill.scrollIntoView()
}
}
return {
'modules/ImageExtend': ImageExtend
}
}
@@ -0,0 +1,38 @@
.image-uploading {
position: absolute;
display: inline-block;
background: #fff;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.image-uploading img {
filter: blur(5px);
opacity: 1;
user-select: none;
-webkit-user-select: none;
}
.image-uploading::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 30px;
height: 30px;
margin-top: -15px;
margin-left: -15px;
border-radius: 50%;
border: 3px solid #ccc;
border-top-color: #1e986c;
z-index: 1;
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,14 @@
import ImageExtend from './image-extend'
import CustomClipboard from "./clipboard"
import MediaVideo from "./media-video"
export function register (Quill) {
const modules = {
ImageExtend,
CustomClipboard,
MediaVideo
}
const options = {}
Object.values(modules).forEach(value => Object.assign(options, value(Quill)))
Quill.register(options, true)
}
@@ -0,0 +1,26 @@
export default function (Quill) {
class MediaVideoModule {
constructor(quill, config) {
this.quill = quill
this.config = config
// 刪除视频
this.quill.root.addEventListener('click', this.handleClick, false);
}
handleClick (e) {
if (e.target && e.target.classList.contains('ql-media-video-remove')) {
const node = e.target.parentNode
const mediaVideo = Quill.find(node)
if (mediaVideo) {
mediaVideo.deleteAt(0)
}
}
}
}
return {
'modules/MediaVideo': MediaVideoModule
}
}