wangEditor 5
官网:https://www.wangeditor.com
demo 示例:https://www.wangeditor.com/demo/
快速开始:https://www.wangeditor.com/v5/getting-started.html
内容处理:https://www.wangeditor.com/v5/content.html
上传图片:https://www.wangeditor.com/v5/menu-config.html#%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87
上传视频:https://www.wangeditor.com/v5/menu-config.html#%E4%B8%8A%E4%BC%A0%E8%A7%86%E9%A2%91
1、 创建空白编辑器
引入 CSS 定义样式
<link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet">
<style>
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100; /* 按需定义 */
}
#toolbar-container { border-bottom: 1px solid #ccc; }
#editor-container { height: 500px; }
</style>
定义 HTML 结构
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
引入 JS 创建编辑器
<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
<script>
const { createEditor, createToolbar } = window.wangEditor
const editorConfig = {
placeholder: 'Type here...',
onChange(editor) {
const html = editor.getHtml()
console.log('editor content', html)
// 也可以同步到 <textarea>
}
}
const editor = createEditor({
selector: '#editor-container',
html: '<p><br></p>',
config: editorConfig,
mode: 'default', // or 'simple'
})
const toolbarConfig = {}
const toolbar = createToolbar({
editor,
selector: '#toolbar-container',
config: toolbarConfig,
mode: 'default', // or 'simple'
})
</script>
- mode: 'default' 默认模式 - 集成了 wangEditor 所有功能
- mode: 'simple' 简洁模式 - 仅有部分常见功能,但更加简洁易用
2、 内容处理
获取 HTML 和 Text
使用 editor.getHtml()
获取 HTML 内容,可参考 demo。使用 editor.getText()
获取纯文本内容。
推荐使用 HTML 格式存储数据。
设置内容
设置 HTML
【注意】这里的 HTML 内容必须是 wangEditor 生成的(即 editor.getHtml()
返回的) HTML 格式,不可以自己随意写。HTML 格式非常灵活,wangEditor 无法兼容所有的 HTML 格式。
例如,wangEditor 可以识别 <strong>hello</strong>
为加粗,但无法识别 <span style="font-weight: bold;">hello</span>
等其他加粗方式。
创建时设置 HTML
const editor = createEditor({
html: '<p>hello <strong>world</strong></p>', // 从 editor.getHtml() 获取的 html 内容
// 其他属性...
})
动态设置 HTML
参考 demo
editor.setHtml('<p>hello <strong>world</strong></p>')
3、 工具栏配置
使用toolbar.getConfig().toolbarKeys获取所有工具栏的配置信息
const toolbarConfig = {}
// 自定义工具栏
toolbarConfig.toolbarKeys = [
'color',
'bgColor',
'clearStyle',
'justifyLeft',
'justifyRight',
'justifyCenter',
// 菜单组,包含多个菜单
{
key: 'group-image',
title: '图片',
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
menuKeys: ["insertImage", "uploadImage"]
},
{
key: 'group-video',
title: '视频',
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
menuKeys: ["insertVideo", "uploadVideo"]
},
'undo',
'redo',
]
4、上传图片
上传图片的配置比较复杂,拆分为几个部分来讲解。可参考这个 demo。
editorConfig.MENU_CONF['uploadImage'] = {
// 上传图片的配置
}
服务端地址
必填,否则上传图片会报错。
editorConfig.MENU_CONF['uploadImage'] = {
server: '/api/upload',
}
【特别注意】服务端 response body 格式要求如下:
上传成功的返回格式:
{
"errno": 0, // 注意:值是数字,不能是字符串
"data": {
"url": "xxx", // 图片 src ,必须
"alt": "yyy", // 图片描述文字,非必须
"href": "zzz" // 图片的链接,非必须
}
}
上传失败的返回格式:
{
"errno": 1, // 只要不等于 0 就行
"message": "失败信息"
}
如果你的服务端 response body 无法按照上述格式,可以使用下文的
customInsert
自定义功能
如果用于 Typescript ,则要定义插入函数的类型。
type InsertFnType = (url: string, alt: string, href: string) => void
自定义插入
如果你的服务端 response body 无法按照上文规定的格式,则无法插入图片,提示失败。
但你可以使用 customInsert
来自定义插入图片。
editorConfig.MENU_CONF['uploadImage'] = {
// 自定义插入图片
customInsert(res: any, insertFn: InsertFnType) { // TS 语法
// customInsert(res, insertFn) { // JS 语法
// res 即服务端的返回结果
// 从 res 中找到 url alt href ,然后插图图片
insertFn(url, alt, href)
},
}
基本配置
editorConfig.MENU_CONF['uploadImage'] = {
// form-data fieldName ,默认值 'wangeditor-uploaded-image'
fieldName: 'your-custom-name',
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 1 * 1024 * 1024, // 1M
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: {
token: 'xxx',
otherKey: 'yyy'
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: {
Accept: 'text/x-json',
otherKey: 'xxx'
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
}
回调函数
editorConfig.MENU_CONF['uploadImage'] = {
// 上传之前触发
onBeforeUpload(file: File) { // TS 语法
// onBeforeUpload(file) { // JS 语法
// file 选中的文件,格式如 { key: file }
return file
// 可以 return
// 1\. return file 或者 new 一个 file ,接下来将上传
// 2\. return false ,不上传这个 file
},
// 上传进度的回调函数
onProgress(progress: number) { // TS 语法
// onProgress(progress) { // JS 语法
// progress 是 0-100 的数字
console.log('progress', progress)
},
// 单个文件上传成功之后
onSuccess(file: File, res: any) { // TS 语法
// onSuccess(file, res) { // JS 语法
console.log(`${file.name} 上传成功`, res)
},
// 单个文件上传失败
onFailed(file: File, res: any) { // TS 语法
// onFailed(file, res) { // JS 语法
console.log(`${file.name} 上传失败`, res)
},
// 上传错误,或者触发 timeout 超时
onError(file: File, err: any, res: any) { // TS 语法
// onError(file, err, res) { // JS 语法
console.log(`${file.name} 上传出错`, err, res)
},
}
5、上传视频
上传视频的配置比较复杂,拆分为几个部分来讲解。可参考这个 demo。
editorConfig.MENU_CONF['uploadVideo'] = {
// 上传视频的配置
}
服务端地址
必填,否则上传视频会报错。
editorConfig.MENU_CONF['uploadVideo'] = {
server: '/api/upload',
}
【特别注意】服务端 response body 格式要求如下:
上传成功的返回格式:
{
"errno": 0, // 注意:值是数字,不能是字符串
"data": {
"url": "xxx", // 视频 src ,必须
"poster": "xxx.png" // 视频封面图片 url ,可选
}
}
// 注意:@wangeditor/editor 版本 >= 5.1.8 才支持 video poster
上传失败的返回格式:
{
"errno": 1, // 只要不等于 0 就行
"message": "失败信息"
}
如果你的服务端 response body 无法按照上述格式,可以使用下文的
customInsert
基本配置
editorConfig.MENU_CONF['uploadVideo'] = {
// form-data fieldName ,默认值 'wangeditor-uploaded-video'
fieldName: 'your-custom-name',
// 单个文件的最大体积限制,默认为 10M
maxFileSize: 5 * 1024 * 1024, // 5M
// 最多可上传几个文件,默认为 5
maxNumberOfFiles: 3,
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['video/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: {
token: 'xxx',
otherKey: 'yyy'
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: {
Accept: 'text/x-json',
otherKey: 'xxx'
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 30 秒
timeout: 15 * 1000, // 15 秒
// 视频不支持 base64 格式插入
}
回调函数
editorConfig.MENU_CONF['uploadVideo'] = {
// 上传之前触发
onBeforeUpload(file: File) { // TS 语法
// onBeforeUpload(file) { // JS 语法
// file 选中的文件,格式如 { key: file }
return file
// 可以 return
// 1\. return file 或者 new 一个 file ,接下来将上传
// 2\. return false ,不上传这个 file
},
// 上传进度的回调函数
onProgress(progress: number) { // TS 语法
// onProgress(progress) { // JS 语法
// progress 是 0-100 的数字
console.log('progress', progress)
},
// 单个文件上传成功之后
onSuccess(file: File, res: any) { // TS 语法
// onSuccess(file, res) { // JS 语法
console.log(`${file.name} 上传成功`, res)
},
// 单个文件上传失败
onFailed(file: File, res: any) { // TS 语法
// onFailed(file, res) { // JS 语法
console.log(`${file.name} 上传失败`, res)
},
// 上传错误,或者触发 timeout 超时
onError(file: File, err: any, res: any) { // TS 语法
// onError(file, err, res) { // JS 语法
console.log(`${file.name} 上传出错`, err, res)
},
}
自定义功能
如果用于 Typescript ,则要定义插入函数的类型。
type InsertFnType = (url: string, poster: string = '') => void
自定义插入
如果你的服务端 response body 无法按照上文规定的格式,则无法插入视频,提示失败。
但你可以使用 customInsert
来自定义插入视频。
editorConfig.MENU_CONF['uploadVideo'] = {
// 自定义插入视频
customInsert(res: any, insertFn: InsertFnType) { // TS 语法
// customInsert(res, insertFn) { // JS 语法
// res 即服务端的返回结果
// 从 res 中找到 url poster ,然后插入视频
insertFn(url, poster)
},
}
示例代码:
ajax-create.blade.php
<style>
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100; /* 按需定义 */
}
#toolbar-container { border-bottom: 1px solid #ccc; }
#editor-container { height: 500px; }
</style>
<form action="{{route('order.note.ajax.create')}}" method="post">
@csrf
<input type="hidden" name="order_id" value="{{$orderId}}"/>
<div class="mb-3">
<textarea style="display:none;" class="form-control" name="content" rows="5"></textarea>
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
<div class="invalid-feedback"></div>
</div>
<button type="button" class="btn btn-primary" onclick="ajaxSubmitForm(this)">保存</button>
<div class="js-ajax-msg"></div>
</form>
<script>
$(document).ready(function(){
// 自定义校验图片
function customCheckImageFn(src, alt, url) {
if (!src) {
return
}
if (src.indexOf('http') !== 0) {
return '图片网址必须以 http/https 开头'
}
return true
// 返回值有三种选择:
// 1. 返回 true ,说明检查通过,编辑器将正常插入图片
// 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串)
// 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入。但不会提示任何信息
}
// 转换图片链接
function customParseImageSrc(src) {
if (src.indexOf('http') !== 0) {
return `http://${src}`
}
return src
}
const { createEditor, createToolbar } = window.wangEditor
const editorConfig = {
MENU_CONF: {},
placeholder: 'Type here...',
onChange(editor) {
const html = editor.getHtml()
console.log('editor content', html)
// 同步到 <textarea>
if('<p><br></p>' !== html){
$("textarea[name=content]").text(html)
}
}
}
// 配置上传图片
editorConfig.MENU_CONF['uploadImage'] = {
server: '{{ route("api.upload.file") }}',
fieldName: 'image',
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 1 * 1024 * 1024, // 1M
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: {
_token: "{{ csrf_token() }}"
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
// 上传之前触发
onBeforeUpload(file) {
// file 选中的文件,格式如 { key: file }
return file
// 可以 return
// 1. return file 或者 new 一个 file ,接下来将上传
// 2. return false ,不上传这个 file
},
// 上传进度的回调函数
onProgress(progress) {
// progress 是 0-100 的数字
console.log('progress', progress)
},
// 单个文件上传成功之后
onSuccess(file, res) {
console.log(`${file.name} 上传成功`, res)
},
// 单个文件上传失败
onFailed(file, res) {
console.log(`${file.name} 上传失败`, res)
},
// 上传错误,或者触发 timeout 超时
onError(file, err, res) {
// onError(file, err, res) {
console.log(`${file.name} 上传出错`, err, res)
},
}
// 配置上传视频
editorConfig.MENU_CONF['uploadVideo'] = {
server: '{{ route("api.upload.file") }}',
fieldName: 'video',
// 单个文件的最大体积限制,默认为 10M
maxFileSize: 5 * 1024 * 1024, // 5M
// 最多可上传几个文件,默认为 5
maxNumberOfFiles: 3,
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['video/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: {
_token: "{{ csrf_token() }}"
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 30 秒
timeout: 15 * 1000, // 15 秒
// 视频不支持 base64 格式插入
// 上传之前触发
onBeforeUpload(file) {
// file 选中的文件,格式如 { key: file }
return file
// 可以 return
// 1. return file 或者 new 一个 file ,接下来将上传
// 2. return false ,不上传这个 file
},
// 上传进度的回调函数
onProgress(progress) {
// progress 是 0-100 的数字
console.log('progress', progress)
},
// 单个文件上传成功之后
onSuccess(file, res) {
console.log(`${file.name} 上传成功`, res)
},
// 单个文件上传失败
onFailed(file, res) {
console.log(`${file.name} 上传失败`, res)
},
// 上传错误,或者触发 timeout 超时
onError(file, err, res) {
console.log(`${file.name} 上传出错`, err, res)
},
}
// 插入图片
editorConfig.MENU_CONF['insertImage'] = {
onInsertedImage(imageNode) {
if (imageNode == null) return
const { src, alt, url, href } = imageNode
console.log('inserted image', src, alt, url, href)
},
checkImage: customCheckImageFn, // 也支持 async 函数
parseImageSrc: customParseImageSrc, // 也支持 async 函数
}
// 编辑图片
editorConfig.MENU_CONF['editImage'] = {
onUpdatedImage(imageNode) {
if (imageNode == null) return
const { src, alt, url } = imageNode
console.log('updated image', src, alt, url)
},
checkImage: customCheckImageFn, // 也支持 async 函数
parseImageSrc: customParseImageSrc, // 也支持 async 函数
}
// 自定义校验视频
function customCheckVideoFn(src){
if (!src) {
return
}
if (src.indexOf('http') !== 0) {
return '视频地址必须以 http/https 开头'
}
return true
// 返回值有三种选择:
// 1. 返回 true ,说明检查通过,编辑器将正常插入视频
// 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串)
// 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入。但不会提示任何信息
}
// 自定义转换视频
function customParseVideoSrc(src) {
if (src.includes('.bilibili.com')) {
// 转换 bilibili url 为 iframe (仅作为示例,不保证代码正确和完整)
const arr = location.pathname.split('/')
const vid = arr[arr.length - 1]
return `<iframe src="//player.bilibili.com/player.html?bvid=${vid}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`
}
return src
}
editorConfig.MENU_CONF['insertVideo'] = {
onInsertedVideo(videoNode) {
if (videoNode == null) return
const { src } = videoNode
console.log('inserted video', src)
},
checkVideo: customCheckVideoFn, // 也支持 async 函数
parseVideoSrc: customParseVideoSrc, // 也支持 async 函数
}
const editor = createEditor({
selector: '#editor-container',
// html: '<p>hello <strong>world</strong></p>',
config: editorConfig,
mode: 'default', // or 'simple'
})
const toolbarConfig = {}
// 自定义工具栏
toolbarConfig.toolbarKeys = [
'color',
'bgColor',
'clearStyle',
'justifyLeft',
'justifyRight',
'justifyCenter',
// 菜单组,包含多个菜单
{
key: 'group-image',
title: '图片',
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
menuKeys: ["insertImage", "uploadImage"]
},
{
key: 'group-video',
title: '视频',
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
menuKeys: ["insertVideo", "uploadVideo"]
},
'undo',
'redo',
]
const toolbar = createToolbar({
editor,
selector: '#toolbar-container',
config: toolbarConfig,
mode: 'default', // 'simple' or 'default'
})
});
</script>
UploadController.php
<?php
namespace App\Http\Controllers\WangEditor;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
public function handle(Request $request)
{
try{
$resData = [
'errno'=>0,
'data'=>[]
];
// 图片上传
if($file = $request->file('image')) {
// wangeditor/image/20220819/OZqFtvH6vAu52ZMFLeq3w5bfPgeHPhGyqYVUeQFr.png
$imageFile = $file->store('wangeditor/image/'.date('Ymd'));
$resData = [
'errno'=>0,
'data'=>
[
'url'=> Storage::url($imageFile), // 图片 src ,必须
'alt'=> '', // 图片描述文字,非必须
'href'=> '' // 图片的链接,非必须
]
];
}
// 视频上传
if ($file = $request->file('video')){
// wangeditor/video/20220819/GOVnjEqXH1aI7hhuAskf05QR8t62vKKXPKThYDfH.mp4
$videoFile = $file->store('wangeditor/video/'.date('Ymd'));
$resData = [
'errno'=>0,
'data'=>
[
'url'=> Storage::url($videoFile), // 视频 src ,必须
'poster'=>'' // 视频封面图片 url ,可选
]
];
}
return response()->json($resData);
}catch(\Exception $e){
return response()->json([
'errno'=>1,
'message'=> $e->getMessage()
]);
}
}
}
web.php
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Route;
Route::middleware('auth.user')->group(function () {
// wangeditor 图片、视频上传
Route::post('api-upload-file',[WangEditor\UploadController::class,'handle'])->name('api.upload.file');
});