Augular中接入tinymce富文本
本文主要介绍了如何在augular中接入tinymce富文本编辑器,并且实现图片和音频自定义上传的逻辑。
一、参考文档
- 中文文档:tinymce.ax-z.cn/
- 英文文档:www.tiny.cloud/
- github:github.com/tinymce/
二、富文本编辑器的接入
1、tiny插件的安装
执行命令:npm install tinymce@5.2.0 @tinymce/tinymce-angular@3.4.0
注意:
- 要根据augular的版本来选择第二个插件的版本,一般来说augular8以下的用@tinymce/tinymce-angular@3.0.0版本的,augular9以上用4.0版本的,augular4版本以下的不支持使用该插件,请注意升级
- 如果是安装了3.0版本之后,在package.json中要把对应的插件版本固定下来,要不然的话在其他环境装包的时候可能会出现自动升级的情况,从而导致错误的出现
2、插件的引入
(1)、 在项目的components目录下添加tiny-editor文件夹
在下面创建tiny-editor.component.html、tiny-editor.component.scss、tiny-editor.component.ts文件
(2)、在tiny-editor.component.html文件中添加tiny-editor的引用,代码如下:
<div class="tiny-editor">
<editor id="editorID" [init]="editorConfig" [(ngModel)]="editorContent"></editor>
</div>
其中,editorConfig中存储的是tiny的配置信息,也可以不传,不传的话tiny默认提供了几个功能可以使用,editContent中存储的是富文本编辑器的内容,是字符串的形式
(3)、完善tiny-editor.component.ts文件内容
根据上面,我们发现了两个参数,所以其实我们要做的很简单,就是把editorConfig的内容丰富好就行了,代码如下,以下会在代码中进行介绍:
// 编辑器配置
editorConfig = {
base_url: '/tinymce', // 设置编辑器的目录,指定一些自定义的文件的目录
height: 500, // 编辑器的高度
menubar: false, //
// 需要用到的插件,可以根据自己的需要进行选择,对应关系可以前往官网查看
plugins: 'print preview searchreplace autolink fullscreen image media imagetools link code codesample table charmap hr pagebreak nonbreaking anchor advlist lists textpattern help autosave', // 当前版本这四个插件用不了bdmap indent2em formatpainter axupimgs,想要用需要单独去下载相关包
// 菜单栏配置,这里设置决定了你需要向用户提供哪些功能的icon,具体功能的实现还是得plugins中实现
toolbar: 'code undo redo restoredraft | cut copy | forecolor backcolor bold italic underline strikethrough link | alignleft aligncenter alignright alignjustify | bullist numlist blockquote subscript superscript removeformat | formatselect fontselect fontsizeselect | table image media charmap hr fullscreen',
// 设置语言,另外默认的是英文包,需要自己卸载中文语言包放在项目目录下,后续会展开说明
language: 'zh_CN',
language_url: '/tinymce/lang/zh_CN.js',
toolbar_mode : 'wrap', // 当菜单栏的总宽度超过编辑器的总宽度时,会折叠按钮,要想全部展示,设置为wrap换行展示,设置为scrolling在同一行滑动展示
branding: false, // 是否展示右下角tiny支持字样
autosave_ask_before_unload: false, // 不关闭的话,每次上传了图片之后再刷新都会提示是否重新加载页面
// 自定义字体大小选择内容
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px 56px 72px',
// 自定义字体的中英文难对照关系,默认英文
font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
media_live_embeds: true, // 网上说开启该设置,视频上传后会自动播放,目测暂时不生效
file_picker_types: 'media', // 指定文件上传类型 参数可为media(媒体)、image(图片)、file(文件)
/*
* @desc 自定义图片上传逻辑,否则的话就要服务端配合去tiny进行一些注册
* @param blobInfo 文件信息,blobInfo.blob()文件流,blobInfo.filename()文件名称
* @param succFun 成功回调,需要将成功的url放在这里,会回显到成功的框内
* @param failFun 失败回调,上传失败后的提示
*/
images_upload_handler: (blobInfo, succFun, failFun) => {
// 这里去实现自己公司的图片上传的功能即可
this.post('xxx', {chunk: blobInfo.blob()})
.then(res => {
succFun(res.url)
})
.catch(err => {
failFun(err)
})
},
//自定义文件选择器的回调内容
file_picker_callback: (callback, value, meta) => {
if (meta.filetype === 'media') {
let input = document.createElement('input');
input.setAttribute('type', 'file');
let _this = this;
input.onchange = function () {
// @ts-ignore
let file = this.files[0];
// 上传视频方法
_this.uploadMv(file);
// 因为只有这个callback才能够将他回显到页面上,所以要在这里callback
if(_this.uploaded) {
callback(_this.mvUrl);
} else {
setTimeout(() => {
callback(_this.mvUrl);
}, 1000)
}
}
// 触发点击
input.click()
// callback('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4');
} else {
this.commonMethodService.toast('请选择正确的音频文件');
}
},
// tiny上传完音频返回的是一张封面图,而不是可播放的音频,要想可播放的话,需要加以下的方法,来渲染一个可以播放音频的video出来
media_url_resolver: function (data, resolve) {
try {
console.log('data', data)
let videoUri = encodeURI(data.url);
let embedHtml = `<p>
<span
class="mce-object-video"
data-mce-selected="1"
data-mce-object="video"
data-mce-p-width="100%"
data-mce-p-height="auto"
data-mce-p-controls="controls"
data-mce-p-controlslist="nodownload"
data-mce-p-allowfullscreen="true"
data-mce-p-src=${videoUri} >
<video src=${data.url} width="100%" height="auto" controls="controls" controlslist="nodownload">
</video>
</span>
</p>`
resolve({ html: embedHtml });
} catch (e) {
resolve({ html: "" });
}
}
};
3、其他配置信息修改
(1)、修改augular.json
主要是在assets下指定将node_modules里面插件的内容部署到我们指定的文件目录下,从而让timy编辑器能够找到,完整代码如下
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"web-template": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce/skins",
"output": "/tinymce/skins/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce/themes",
"output": "/tinymce/themes/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce/plugins",
"output": "/tinymce/plugins/"
},
{
"glob": "**/*",
"input": "src/tinymce/lang",
"output": "/tinymce/lang/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce/icons",
"output": "/tinymce/icons/"
},
{ "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" }
],
"styles": [
"node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
"node_modules/@jdb-ui/core/publish/src/jdb-plg-ui.min.css",
"src/styles.scss",
"src/jdb-ui.scss"
],
"scripts": [
"src/assets/laydate.js",
"node_modules/tinymce/tinymce.min.js"
]
},
"configurations": {
"serve": {
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.serve.ts"
}]
},
"debug": {
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.test.ts"
}]
},
"test": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.test.ts"
}]
},
"huidu": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.huidu.ts"
}]
},
"betaProd": {
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}],
"optimization": true,
"outputHashing": "all",
"sourceMap": true,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
},
"production": {
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "web-template:build"
},
"configurations": {
"serve": {
"browserTarget": "web-template:build:serve",
"proxyConfig": "proxy.serve.js"
},
"debug": {
"browserTarget": "web-template:build:debug",
"proxyConfig": "proxy.debug.json"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "web-template:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
"node_modules/@jdb-ui/core/publish/src/jdb-plg-ui.min.css",
"src/styles.scss"
],
"scripts": [
"src/assets/laydate.js"
],
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"web-template-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "web-template:serve"
},
"configurations": {
"production": {
"devServerTarget": "web-template:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "web-template",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
}
}
}
三、常见问题解决
1、富文本编辑器报icons文件找不到了或者说warning提示你说要先注册
此时你F12打开你的netWork查看的话,你会发现你的浏览器加载的tiny.min.js文件的话,是走的cdn加载,也就是从网络上获取的文件。
从网络上获取的文件,他本身是不会有icons相关内容的,所以他才会给你报错说,有个icons.js文件找不到了,然后那个注册的提示也是同理,因为你走的是cdn在线的一个文件,而且此时其实文件加载也是比较慢的
解决方法:
在augular.json中增加scripts配置,配置内容为,即为指定从node_modules中加载tiny.min.js文件,就可以了,详细配置位置等参考上满augular.json中配置
"scripts": [
"node_modules/tinymce/tinymce.min.js"
]
注意: 1、关于提示没有在tiny注册这个东西还有一个解决方法就是去tiny那边注册一下,然后把你的域名添加到白名单就好了
2、为什么要配置本地加载也有一个好处,虽然说cdn是就近加速,但是其实还是会受到网络状态的影响,会导致富文本的加载速度和体验都不会很好,所以换成本地加载的话也能有效的解决这一个问题
2、关于图片上传
tiny提供了自定义图片上传的参数images_upload_handler,在这里可以拦截自定义图片上传的逻辑
/*
* @param blobInfo -- 图片的文件流,提供了很多方法可以获得文件的名字、内容等信息
* @param succFun 成功的回调,将上传成功的图片url放在回调中即可
* @param failFun 失败回调
*/
images_upload_handler: (blobInfo, succFun, failFun) => {
this.post('xxxx', params)
.then((res) => {
succFun(res.data && res.data.url);
})
.catch((err) => {
failFunn(err);
})
}
3、关于音频上传
tiny提供了自定义文件上传的参数file_picker_callback,在这里可以拦截自定义文件上传的逻辑,这里面包括了media(媒体),image(图片),file(文件)
file_picker_callback: (callback, value, meta) => {
if (meta.filetype === 'media') {
// 创建一个input框,来触发文件选择的逻辑
let input = document.createElement('input');
input.setAttribute('type', 'file');
let _this = this;
input.onchange = function () {
// @ts-ignore
let file = this.files[0];
// 自定义音频上传逻辑,业务自己实现
_this.uploadMv(file);
if(_this.uploaded) {
// 上传成功之后,将url回调回去
callback(_this.mvUrl);
} else {
setTimeout(() => {
callback(_this.mvUrl);
}, 1000)
}
}
// 触发点击
input.click()
// callback('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4');
} else {
this.commonMethodService.toast('请选择正确的音频文件');
}
},
4、音频上传之后,只能展示封面,无法播放视频
核心思路就是我们构建一个可以播放音频的html的片段,插入到富文本中,利用tiny提供的media_url_resolver方法
// tiny上传完音频返回的是一张封面图,而不是可播放的音频,要想可播放的话,需要加以下的方法,来渲染一个可以播放音频的video出来
media_url_resolver: function (data, resolve) {
try {
console.log('data', data)
let videoUri = encodeURI(data.url);
let embedHtml = `<p>
<span
class="mce-object-video"
data-mce-selected="1"
data-mce-object="video"
data-mce-p-width="100%"
data-mce-p-height="auto"
data-mce-p-controls="controls"
data-mce-p-controlslist="nodownload"
data-mce-p-allowfullscreen="true"
data-mce-p-src=${videoUri} >
<video src=${data.url} width="100%" height="auto" controls="controls" controlslist="nodownload">
</video>
</span>
</p>`
resolve({ html: embedHtml });
} catch (e) {
resolve({ html: "" });
}
}