H5 编辑器 Tinymce之解决图片上传/粘贴
TinyMCE 5是一款功能强大且灵活的富文本编辑器,可以嵌入Web应用程序中.
1.在HTML代码中<head>
标签中引入下边的代码块
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script>
*
如果使用webpack包管理工具
npm install tinymce
您也可以通过 tinyMCE 自定义自己所需要的pugins,以满足自己需求的同时,可以减少包的重量
2.在<body>
中加入如下HTML代码
<body>
<h1>TinyMCE Quick Start Guide</h1>
<form method="post">
<textarea id="mytextarea">Hello, World!</textarea>
</form>
</body>
3.新增<script>
标签,调用tinymce的init
方法
<script>
tinymce.init({
selector: '#mytextarea'
});
</script>
当你引入tinyMCE插件时,全局自动注入
tinymce
对象,你可以在任何位置去调用它的方法,您可以使用console.log(tinymce)
去查看它的所有方法
此时您的页面已初步构建完成,打开浏览器,可以看到如下的功能页面
下面我们就开始完善适合需求的各项功能组件
window.tinymce.init({
selector: `#mytextarea`, //选择器的标识
language: "zh", //需要配置的语言
//language_url: "./languages/zh_CN.js", //语言配置也可以自己引入自己本地的语言包
width:'300px',//设置编辑器高度
height: '300px',//设置编辑器的高度
font_formats: //显示的自定义字体 以 xx = xx 形式显示
"宋体=SimSun;微软雅黑=Microsoft Yahei;华文黑体=STHeiti;华文楷体=STKaiti;华文仿宋=华文仿宋;思源黑体=Source Han Sans CN;思源宋体=Source Han Serif SC;华文细黑=STXihei;黑体=SimHei;方正粗圆简体=方正粗圆简体;Andale Mono=andale mono,times;",
fontsize_formats: "8pt 10pt 12pt 14pt 18pt 24pt 36pt",//字体的大小
contextmenu://
"link image imagetools inserttable | cell row column deletetable | headings",
content_css: [ //自定义的content样式表
"//fonts.googleapis.com/css?family=Lato:300,300i,400,400i",
"//www.tinymce.com/css/codepen.min.css"
],
//skin: "oxide-dark", //皮肤
//statusbar: false, //底部状态栏
body_class: "panel-body",//给iframe body标签添加class名
object_resizing: false,
toolbar: [ //工具栏
'undo redo |styleselect| bold italic forecolor backcolor | fontselect |fontsizeselect | alignleft aligncenter alignright alignjustify |' +
'ht| bullist numlist outdent indent | removeformat|' +
'',
'link image media code codesample hr charmap preview anchor pagebreak insertdatetime me blocks' +
'dia emoticons fullscreen save'
],
menubar: "file edit insert view format table",//顶部操作状态栏
plugins: ['advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools media insertdatetime link lists nonbreaking noneditable pagebreak powerpaste preview print save searchreplace spellchecker tabfocus table textpattern visualblocks visualchars quickbars charmap'],//所需要的组件数组
quickbars_selection_toolbar: //选择文本时的快捷栏
"bold italic underline forecolor backcolor | fontselect |fontsizeselect | formatselect | quicklink blockquote",
quickbars_image_toolbar: //选中图片时的快捷栏
"alignleft aligncenter alignright quicklink | imageoptions",
//external_plugins: { //引入本地的组件包 原包许多组件需要收费,我们自行引入,但需要有资源
// powerpaste: "/public/tinymce/plugins/powerpaste/plugin.min.js"
// },
formats: { //自定义的
alignleft: { //居左自动嵌套div元素,并添加display:flex样式
block: "div",
styles: { display: "flex", justifyContent: "flex-start" },
classes: "left"
},
aligncenter: {//居中
block: "div",
styles: {
display: "flex",
justifyContent: "center"
},
classes: "center" //添加class名
},
alignright: {//居右
block: "div",
styles: { display: "flex", justifyContent: "flex-end" },
classes: "right"
}
},
quickbars_insert_toolbar: "quickimage quicktable media", //点击空白区域的快捷工具栏
autosave_restore_when_empty: false, //浏览器崩溃自动保存本地
end_container_on_empty_block: true,//是否在末尾添加空div
powerpaste_word_import: "merge", //复制粘贴的文字样式处理 参数可以是propmt, merge, clean,效果自行切换对比
powerpaste_html_import: "merge", //复制粘贴的html样式处理 propmt, merge, clean
powerpaste_allow_local_images: true, //复制粘贴图书是否允许本地图片
paste_data_images: true,
paste_preprocess: (pluginApi, data) => {
//console.log(data);
// Apply custom filtering by mutating data.content
// For example:
const content = data.content;
const newContent = this.yourCustomFilter(content);
data.content = newContent;
},
code_dialog_height: 450,// code 模态框的高度
code_dialog_width: 1000,//code 模态框的宽度
charmap_append: [],
// advlist_bullet_styles: "circle",//列表样式
// advlist_number_styles: "default",//数字列表样式
imagetools_cors_hosts: ["www.tinymce.com", "codepen.io"],
image_advtab: false,//是否显示图片高级选项
// image_uploadtab: false,//是否显示上传图片按钮
default_link_target: "_blank",//点击链接是否跳转新页面
link_title: false,//链接标题
media_live_embeds: true,
//想要哪一个图标提供本地文件选择功能,参数可为media(媒体)、image(图片)、file(文件)
file_picker_types: "media",//上传文件类型
media_alt_source: false,
//media_poster: false,//媒体的poster
//media_filter_html: false,
// file_picker_callback: function(callback, value, meta) {
// //当点击meidia图标上传时,判断meta.filetype == 'media'有必要,因为file_picker_callback是media(媒体)、image(图片)、file(文件)的共同入口
// console.log(meta);
// if (meta.filetype == "media") {
// //创建一个隐藏的type=file的文件选择input
// let input = document.createElement("input");
// input.setAttribute("type", "file");
// input.onchange = function() {
// let file = this.files[0]; //只选取第一个文件。如果要选取全部,后面注意做修改
// console.log(file);
// const url = "";
// let xhr, formData;
// xhr = new XMLHttpRequest();
// xhr.open("POST", self.apiUrl);
// xhr.withCredentials = self.credentials;
// xhr.upload.onprogress = function(e) {
// // 进度(e.loaded / e.total * 100)
// let percent = (e.loaded / e.total) * 100;
// if (percent < 100) {
// tinymce.activeEditor.setProgressState(true); //是否显示loading状态:1(显示)0(隐藏)
// } else {
// tinymce.activeEditor.setProgressState(false);
// }
// };
// xhr.onerror = function() {
// //根据自己的需要添加代码
// tinymce.activeEditor.setProgressState(false);
// return;
// };
// xhr.onload = function() {
// let json;
// if (xhr.status < 200 || xhr.status >= 300) {
// console.log("HTTP 错误: " + xhr.status);
// return;
// }
// json = JSON.parse(xhr.responseText);
// //假设接口返回JSON数据为{status: 0, msg: "上传成功", data: {location: "/localImgs/1546434503854.mp4"}}
// if (json.status == 0) {
// //接口返回的文件保存地址
// let mediaLocation = json.data.location;
// //cb()回调函数,将mediaLocation显示在弹框输入框中
// callback(mediaLocation, { title: file.name });
// } else {
// console.log(json.msg);
// return;
// }
// };
// formData = new FormData();
// //假设接口接收参数为file,值为选中的文件
// formData.append("file", file);
// //正式使用将下面被注释的内容恢复
// xhr.send(formData);
// };
// //触发点击
// input.click();
// }
// },
media_url_resolver: function(data, resolve) {//上传视频的自定义html代码块,必须调用resole返回代码块
try {
let videoUri = encodeURI(data.url);
let embedHtml = `<p><video controls="controls" width="100%" height="auto"> <source src="${videoUri}" type="video/mp4" /></video></p> <p style="text-align: left;"> </p>`;
resolve({ html: embedHtml });
} catch (e) {
resolve({ html: "" });
}
},
save_enablewhendirty: true,
nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin
init_instance_callback: editor => {//初始化执行代码
if (_this.value) {
editor.setContent(_this.value);
}
_this.hasInit = true;
//检测编辑器动作
editor.on("NodeChange Change KeyUp SetContent", () => {
if (this.value !== "") {
this.hasChange = true;
}
this.$emit("input", editor.getContent());
});
},
save_onsavecallback: () => { //点击保存按钮处理函数
let content = window.tinymce.get(this.id).getContent();
// 匹配并替换 任意html元素中 url 路径
if (this.newImgUrl.length) {
content = content.replace(
/<img [^>]*src=['"]([^'"]+)[^>]*>/gi,
(mactch, capture) => {
let current = "";
console.log(this.newImgUrl);
for (let i = 0; i < this.newImgUrl.length; i++) {
console.log(
capture.replace(/(&)/gi, "&") ==
this.newImgUrl[i].originUrl
);
if (
capture.replace(/(&)/gi, "&") ==
this.newImgUrl[i].originUrl
) {
current = this.newImgUrl[i].url;
break;
}
}
// this.newImgUrl.forEach(item => {
// console.log(
// item.originUrl == capture.replace(/(&)/gi, "&")
// );
// if (capture.replace(/(&)/gi, "&") == item.originUrl) {
// current = item.url;
// }
// });
current = current ? current : capture;
return mactch.replace(
/src=[\'\"]?([^\'\"]*)[\'\"]?/i,
"src=" + current
);
}
);
} // 匹配并替换 img中src图片路径
},
setup(editor) {
editor.on("FullscreenStateChanged", e => {
_this.fullscreen = e.state;
});
},
images_upload_handler(blobInfo, success, failure, progress) { //图片上传处理逻辑
//此处添加自己的上传图片逻辑代码
},
urlconverter_callback(url, node, on_save, name) {//自动检测不是自己服务器库的图片
//设置白名单
const assignUrl = [
"blob:http://localhost",
"data:image/gif;base64"
];
let curl = url;
let isInnerUrl = false; //默认不是内部链接
try {
assignUrl.forEach(item => {
if (url.indexOf(item) > -1) {
isInnerUrl = true;
throw new Error("EndIterate");
}
});
} catch (e) {
if (e.message != "EndIterate") throw e;
}
if (node == "img" && !isInnerUrl) {
let json;
getBase64(url).then(base64 => {
const url = ``;
request({
url,
file: data2blob(base64, mimes["png"]),
filename: "file" + "." + "png"
}).then(res => {
if (res.data.code == 0) {
_this.newImgUrl.push({
originUrl: curl,
url: res.data.data.url
});
//return res.data.data.url;
//deferred.resolve(res.data.url);
} else {
failure("Invalid JSON: " + res.data.data.msg);
}
});
});
}
return url;
}
});
因为复制粘贴组件powerpaste是收费项目,但又是一个完整编辑器不可或缺的功能,所以这里给出它的下载地址 https://download.csdn.net/download/wddwwwq1/12579334,请自行下载。
在这里我们重点讲解关于powerpaste
的功能构建
urlconverter_callback(url, node, on_save, name) {
//设置白名单
const assignUrl = [
"blob:http://localhost",
"data:image/gif;base64",
"http://192.168.2.221",
"http://192.168.2.222",
"http://192.168.2.223",
"http://192.168.2.224",
];
let curl = url;
let isInnerUrl = false; //默认不是内部链接
try {
assignUrl.forEach(item => {
if (url.indexOf(item) > -1) {
isInnerUrl = true;
throw new Error("EndIterate");
}
});
} catch (e) {
if (e.message != "EndIterate") throw e;
}
if (node == "img" && !isInnerUrl) {
let json;
getBase64(url).then(base64 => {
const url =
process.env.NODE_ENV !== "production"
? ""
: "";
request({
url,
file: data2blob(base64, mimes["png"]),
filename: "file" + "." + "png"
}).then(res => {
if (res.data.code == 0) {
_this.newImgUrl.push({
originUrl: curl,
url: res.data.data.url
});
//return res.data.data.url;
//deferred.resolve(res.data.url);
} else {
failure("Invalid JSON: " + res.data.data.msg);
}
});
});
}
return url;
}
上述代码是复制粘贴功能的核心,因为我们复制的图片不一定是自己服务器的图片,所以我们需要把其他服务器的http图片,保存在我们的服务器上。
-
当编辑器文本中包含有图片首先我们给出一个数组
assignUrl
,列出您不想让编辑器检测的域名,然后使用回调函数的url
去检查是否跟数组白名单的域名相匹配,如果包含,则表示是自己服务器的图片,不需要再上传服务器。 -
urlconverter_callback
回调函数有四个参数(url, node, on_save, name)
,其中url
表示检测的链接地址,node
则表示此链接所在的标签名字。例如<a href="http://www.baidu.com"></a>
和<img src="https://timgsa.baidu.com/timg.jpg" />
-
判断
node == img && !isInnerUrl
,如果为true
,则表明我们需要手动上传图片到我们自己的服务器,首先把http格式的图片转换为base64
//图片转换为base64
export default function(img) {
function getBase64Image(img, width, height) { //width、height调用时传入具体像素值,控制大小 ,不传则默认图像大小
var canvas = document.createElement("canvas");
canvas.width = width ? width : img.width;
canvas.height = height ? height : img.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
var dataURL = canvas.toDataURL();
return dataURL;
}
var image = new Image();
image.crossOrigin = '';
image.src = img;
//var deferred = $.Deferred();
var deferred = new Deferred();
if (img) {
image.onload = function() {
deferred.resolve(getBase64Image(image)); //将base64传给done上传处理
}
//问题要让onload完成后再return sessionStorage['imgTest']
} else {}
return deferred.promise;
}
function Deferred() {
var self = this;
self.promise = new Promise(function(resolve, reject) {
self._resolve = resolve;
self._reject = reject;
});
}
Deferred.prototype.resolve = function(data) {
this._resolve(data);
}
Deferred.prototype.reject = function(data) {
//this._reject.call(this.promise,data);
this._reject(data);
}
这里使用
canvas
画图工具转换图片,最后使用canvas.toDataURL()
在转换为base64的图片格式说明:如不了解
canvas
,请自行学习
- 因为上传图片是异步,并且上传需要时间,等待服务器返回图片地址时,我们已经把文本显示在编辑器之中,所以在我们
return url
的时候,还没有得到返回结果,由于tinymce
不支持异步处理函数,我尝试用async await
处理也没有实际作用,所以我们可以在点击保存文本的时候去手动替换需要替换的图片地址,在上传图片返回结果时我们把图片地址保存在一个数组之中_this.newImgUrl.push({ originUrl: curl, url: res.data.data.url });
,以便在保存的时候操作回填。
save_onsavecallback: () => {
let content = window.tinymce.get(this.id).getContent();
// 匹配并替换 任意html元素中 url 路径
if (this.newImgUrl.length) {
content = content.replace(
/<img [^>]*src=['"]([^'"]+)[^>]*>/gi,
(mactch, capture) => {
let current = "";
for (let i = 0; i < this.newImgUrl.length; i++) {
if (
capture.replace(/(&)/gi, "&") ==
this.newImgUrl[i].originUrl
) {
current = this.newImgUrl[i].url;
break;
}
}
current = current ? current : capture;
return mactch.replace(
/src=[\'\"]?([^\'\"]*)[\'\"]?/i,
"src=" + current
);
}
);
} // 匹配并替换 img中src图片路径
//...此处上传文本省略
},
因为我使用的VUE构建,代码块里的
this
请自行注意理解