1 动机
尽管tinymce已经有link插件,但是不能完全满足客户的需求,因此采取自行编写插件的方式来设计。 首先,明确自己的需求。
- 实现图片、视频、文件的上传,上传至七牛云
- 有预览区,对于视频、图片可以设置宽高
- 根据返回URL,以及不同的文件类型,以HTML的形式插入到editor
- 对于大文件、视频,实现分片上传,支持断点续传
本文主要是实现1(除上传至七牛云)2以及3,并且实现插件打包
2 实现过程
2.1 预备工作
新建一个文件夹,根据Yeoman生成器的步骤新建一个项目并且npm start运行(别忘了npm install),我创建的插件名称为media-7-n。
接下来的修改都会在plugin.ts中进行,该文件的初始内容如下
import { Editor, TinyMCE } from 'tinymce';
declare const tinymce: TinyMCE;
const setup = (editor: Editor, url: string): void => {
//注册按钮
editor.ui.registry.addButton('media-7-n', {
text: 'media-7-n button',
//初始的行为
onAction: () => {
editor.setContent('<p>content added from media-7-n</p>');
}
});
};
export default (): void => {
tinymce.PluginManager.add('media-7-n', setup);
};
2.2 插件UI设置
根据需求,我们的界面需要五个部分,文件上传按钮,宽度设置、高度设置、注释填写、预览区。根据tinymce提供的文档,设计了如下五个区域。并且设计弹窗的标题以及按钮。
在plugin.ts中修改
editor.ui.registry.addButton('media-7-n', {
text: '文件上传',
icon: 'link',
onAction: () => {
const dialogConfig: any = {
title: '文件上传',
body: {
type: 'panel',
items: [
{
type: 'urlinput',
name: 'source',
filetype: 'media',
label: 'Source'
},
{
type: 'input',
name: 'width',
label: '宽度'
},
{
type: 'input',
name: 'height',
label: '高度'
},
{
type: 'input', // component type
name: 'note', // identifier
label: '注释 (图片和视频)', // text for the iframe's title attribute
},
{
type: 'iframe', // component type
name: 'iframe', // identifier
label: '预览', // text for the iframe's title attribute
sandboxed: true,
transparent: true
},
],
},
buttons: [
{
type: 'cancel',
text: '取消',
},
{
type: 'submit',
text: '确定',
primary: true
}
],
}
}
})
注意:如果使用了urlinput,一定要在tinymce的初始化函数中加入file_picker_callback函数,才会出现文件上传按钮的
2.3 触发事件onChange
注册一个onChange事件,用于文件上传后,依据不同的文件类型更新到预览区。使用api.setData更新到iframe区域。
// 文件后缀识别
const fileType = {
image:['jpg','jpeg','png','tif','svg'],
video:['mp4',"rmvb", "rm", "asf", "divx", "mpg", "mpeg", "wmv", "mkv", "vob"]
}
const uploadFile = (api: any): void => {
const data = api.getData();
// 获取文件
const file = data?.source;
// 获取文件名后缀 如jpg
const fileExt = file?.meta?.title?.split('.')[1]
// 不同文件行为实现不同的预览
if(fileType.image.indexOf(fileExt)!==-1){
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
`<img style="width:100%" src=${file?.value}></img>`+
'</body>' +
'</html>'
})
}
else if(fileType.video.indexOf(fileExt)!==-1){
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
`<video style="width:100%" src=${file?.value}></video>`+
'</body>' +
'</html>'
})
}else{
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
'<div style="display:flex;width:calc(100% - 20px);height:100px;padding:10px;background:#f5f5f5;border-radius:15px;">'+
`<div style="width:90px;height:90px;margin-right:10px;background:#eeeeff;border-radius:13px;"><svg style="width:100%;height:100%; t="1683031868915" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2669" width="200" height="200"><path d="M912 208H427.872l-50.368-94.176A63.936 63.936 0 0 0 321.056 80H112c-35.296 0-64 28.704-64 64v736c0 35.296 28.704 64 64 64h800c35.296 0 64-28.704 64-64v-608c0-35.296-28.704-64-64-64z m-800-64h209.056l68.448 128H912v97.984c-0.416 0-0.8-0.128-1.216-0.128H113.248c-0.416 0-0.8 0.128-1.248 0.128V144z m0 736v-96l1.248-350.144 798.752 1.216V784h0.064v96H112z" fill="#020202" p-id="2670"></path></svg></div><div style="width:60%">${file?.value}</div>`+
'</div>'+
'</body>' +
'</html>'
})
}
}
2.4 提交事件
注册一个提交事件onSubmit,主要是用editor.insertContent在editor中插入html。(代码中的editor变量的定义详见完整代码)
onSubmit(api:any) {
const data = api.getData();
// 获取文件
const file = data?.source;
// 获取文件名后缀 如jpg
const fileExt = file?.meta?.title?.split('.')[1]
// 插入到文档中
if(fileType.image.indexOf(fileExt)!==-1){
editor.insertContent(`<div><img style="width:${data.width?data.width+'px':'375px'};height:${data.height?data.height+'px':''}" src=${file?.value}></img><p style="text-align:center; margin-top: -10px;">${data.note}</p></div>`)
}
else if(fileType.video.indexOf(fileExt)!==-1){
editor.insertContent(`<div><video style="width:${data.width?data.width+'px':'375px'};height:${data.height?data.height+'px':''}" src=${file?.value}></video><p style="text-align:center;margin-top: -10px;">${data.note}</p></div>`)
}else{
editor.insertContent(`<div style="display:flex;width:calc(100% - 20px);height:100px;padding:10px;background:#f5f5f5;border-radius:15px;"><div style="width:90px;height:90px;margin-right:10px;background:#eeeeff;border-radius:13px;"><svg style="width:100%;height:100%; t="1683031868915" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2669" width="200" height="200"><path d="M912 208H427.872l-50.368-94.176A63.936 63.936 0 0 0 321.056 80H112c-35.296 0-64 28.704-64 64v736c0 35.296 28.704 64 64 64h800c35.296 0 64-28.704 64-64v-608c0-35.296-28.704-64-64-64z m-800-64h209.056l68.448 128H912v97.984c-0.416 0-0.8-0.128-1.216-0.128H113.248c-0.416 0-0.8 0.128-1.248 0.128V144z m0 736v-96l1.248-350.144 798.752 1.216V784h0.064v96H112z" fill="#020202" p-id="2670"></path></svg></div><div style="width:60%">${file?.value}</div></div>`)
}
api.close();
}
2.5 完整代码
在demo.ts中也要修改代码用于测试,主要是定义了file_picker_callback,用于描述文件上传后的行为。
这个函数中主要是处理文件上传,这里只是作为一个测试函数,并未将文件上传到服务器,在实际使用过程中需要上传文件到服务器。
file_picker_callback函数会在点击上传文件的图标后被触发。这个函数主要的流程为:新增一个input元素->定义type为文件->绑定一个文件上传的事件onchange->点击input元素。最后的点击input元素会打开文件上传的界面。可以在onchange事件中对文件上传的进行按需处理。
以下是demo.ts的代码
import { TinyMCE } from 'tinymce';
import Plugin from '../../main/ts/Plugin';
declare let tinymce: TinyMCE;
Plugin();
const fileUpload = (cb)=>{
// 必须定义input,并且点击才会弹出文件上传的框
const input = document.createElement('input');
input.setAttribute('type', 'file');
// 文件上传的事件
input.onchange = function () {
// 获取上传的文件
console.log(this,typeof(this))
const file = (this as HTMLInputElement).files[0];
const reader = new FileReader();
// 读取文件
reader.onload = function () {
const id = 'blobid' + (new Date()).getTime();
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
const base64 = (reader?.result as string).split(',')[1];
const blobInfo = blobCache.create(id, file, base64);
// tinymce要求的blobInfo
blobCache.add(blobInfo);
// cb是回调函数,传回名称和title
cb(blobInfo.blobUri(), { title: file.name,file:file });
};
// readAsDataURL 选择图片文件后即使预览图片,这里用不到
reader.readAsDataURL(file);
};
input.click();
}
tinymce.init({
selector: 'textarea.tinymce',
plugins: 'code media-7-n',
toolbar: 'media-7-n',
file_picker_types: 'file image media',
width:'375px',
height:'900px',
file_picker_callback: fileUpload
});
以下是plugin.ts的完整代码
import { Editor, TinyMCE } from 'tinymce';
declare const tinymce: TinyMCE;
// 文件后缀识别
const fileType = {
image:['jpg','jpeg','png','tif','svg'],
video:['mp4',"rmvb", "rm", "asf", "divx", "mpg", "mpeg", "wmv", "mkv", "vob"]
}
// eslint-disable-next-line
const uploadFile = (api: any): void => {
const data = api.getData();
// 获取文件
const file = data?.source;
// 获取文件名后缀 如jpg
const fileExt = (file?.meta?.title as string).split('.')[1]
// 不同文件行为实现不同的预览
if(fileType.image.indexOf(fileExt)!==-1){
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
`<img style="width:100%" src=${file?.value}></img>`+
'</body>' +
'</html>'
})
}
else if(fileType.video.indexOf(fileExt)!==-1){
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
`<video style="width:100%" src=${file?.value}></video>`+
'</body>' +
'</html>'
})
}else{
api.setData({
iframe: '<!DOCTYPE html>' +
'<html>' +
'<head></head>' +
'<body>'+
'<div style="display:flex;width:calc(100% - 20px);height:100px;padding:10px;background:#f5f5f5;border-radius:15px;">'+
`<div style="width:90px;height:90px;margin-right:10px;background:#eeeeff;border-radius:13px;"><svg style="width:100%;height:100%; t="1683031868915" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2669" width="200" height="200"><path d="M912 208H427.872l-50.368-94.176A63.936 63.936 0 0 0 321.056 80H112c-35.296 0-64 28.704-64 64v736c0 35.296 28.704 64 64 64h800c35.296 0 64-28.704 64-64v-608c0-35.296-28.704-64-64-64z m-800-64h209.056l68.448 128H912v97.984c-0.416 0-0.8-0.128-1.216-0.128H113.248c-0.416 0-0.8 0.128-1.248 0.128V144z m0 736v-96l1.248-350.144 798.752 1.216V784h0.064v96H112z" fill="#020202" p-id="2670"></path></svg></div><div style="width:60%">${file?.value}</div>`+
'</div>'+
'</body>' +
'</html>'
})
}
}
const setup = (editor: Editor): void => {
editor.ui.registry.addButton('media-7-n', {
text: '文件上传',
icon: 'link',
onAction: () => {
// 插件的样式
// eslint-disable-next-line
const dialogConfig: any = {
title: '文件上传',
body: {
type: 'panel',
items: [
{
type: 'urlinput',
name: 'source',
filetype: 'media',
label: 'Source'
},
{
type: 'input',
name: 'width',
label: '宽度'
},
{
type: 'input',
name: 'height',
label: '高度'
},
{
type: 'input', // component type
name: 'note', // identifier
label: '注释 (图片和视频)', // text for the iframe's title attribute
},
{
type: 'iframe', // component type
name: 'iframe', // identifier
label: '预览', // text for the iframe's title attribute
sandboxed: true,
transparent: true
},
],
},
buttons: [
{
type: 'cancel',
text: '取消',
},
{
type: 'submit',
text: '确定',
primary: true
}
],
// eslint-disable-next-line
onChange(api:any) {
uploadFile(api);
},
// eslint-disable-next-line
onSubmit(api:any) {
const data = api.getData();
// 获取文件
const file = data?.source;
// 获取文件名后缀 如jpg
const fileExt = file?.meta?.title?.split('.')[1]
// 插入到文档中
if(fileType.image.indexOf(fileExt)!==-1){
editor.insertContent(`<div><img style="width:${data.width?data.width+'px':'375px'};height:${data.height?data.height+'px':''}" src=${file?.value}></img><p style="text-align:center; margin-top: -10px;">${data.note}</p></div>`)
}
else if(fileType.video.indexOf(fileExt)!==-1){
editor.insertContent(`<div><video style="width:${data.width?data.width+'px':'375px'};height:${data.height?data.height+'px':''}" src=${file?.value}></video><p style="text-align:center;margin-top: -10px;">${data.note}</p></div>`)
}else{
editor.insertContent(`<div style="display:flex;width:calc(100% - 20px);height:100px;padding:10px;background:#f5f5f5;border-radius:15px;"><div style="width:90px;height:90px;margin-right:10px;background:#eeeeff;border-radius:13px;"><svg style="width:100%;height:100%; t="1683031868915" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2669" width="200" height="200"><path d="M912 208H427.872l-50.368-94.176A63.936 63.936 0 0 0 321.056 80H112c-35.296 0-64 28.704-64 64v736c0 35.296 28.704 64 64 64h800c35.296 0 64-28.704 64-64v-608c0-35.296-28.704-64-64-64z m-800-64h209.056l68.448 128H912v97.984c-0.416 0-0.8-0.128-1.216-0.128H113.248c-0.416 0-0.8 0.128-1.248 0.128V144z m0 736v-96l1.248-350.144 798.752 1.216V784h0.064v96H112z" fill="#020202" p-id="2670"></path></svg></div><div style="width:60%">${file?.value}</div></div>`)
}
api.close();
}
};
editor.windowManager.open(dialogConfig);
},
});
};
export default (): void => {
tinymce.PluginManager.add('media-7-n', setup);
};
最后yarn build打包即可