Merge pull request #2724 from wangeditor-team/feature-uploadVideo
上传视频功能
This commit is contained in:
commit
f4525bc72f
|
@ -0,0 +1,128 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>wangEditor example</title>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/xgplayer@2.9.6/browser/index.js" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
wangEditor demo
|
||||||
|
</p>
|
||||||
|
<div id="div1">
|
||||||
|
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
|
||||||
|
<p>测试视频</p>
|
||||||
|
<p>
|
||||||
|
<video src="/server/upload-files/测试1-w9b.mp4" controls="controls" style="max-width:100%"></video>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../dist/wangEditor.js"></script>
|
||||||
|
<script>
|
||||||
|
// 改为使用var声明,才能在window对象上获取到编辑器实例,方便e2e测试
|
||||||
|
var E = window.wangEditor
|
||||||
|
var editor = new E('#div1')
|
||||||
|
|
||||||
|
// 上传地址
|
||||||
|
editor.config.uploadVideoServer = '/api/upload-video';
|
||||||
|
|
||||||
|
// 自定义插入视频
|
||||||
|
// editor.config.customInsertVideo = (url) => {
|
||||||
|
// console.log('自定义插入视频---->', url);
|
||||||
|
// // 演示西瓜视频
|
||||||
|
// editor.cmd.do('insertHTML', `<p>
|
||||||
|
// <p contenteditable = false id="mse" style="max-width:100%"></p>
|
||||||
|
// </p>`)
|
||||||
|
// let player = new Player({
|
||||||
|
// id: 'mse',
|
||||||
|
// url: url
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 自定义上传
|
||||||
|
// editor.config.customUploadVideo = (files, insertVideoFn) => {
|
||||||
|
// console.log('自定义上传---->', files);
|
||||||
|
// insertVideoFn('/server/upload-files/测试1-w9b.mp4');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 显示“插入视频”
|
||||||
|
editor.config.showLinkVideo = true;
|
||||||
|
|
||||||
|
// accept
|
||||||
|
editor.config.uploadVideoAccept = ['mp4'];
|
||||||
|
|
||||||
|
// 上传视频的最大体积,默认 1024M
|
||||||
|
editor.config.uploadVideoMaxSize = 1 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
// 自定义上传视频的名称
|
||||||
|
editor.config.uploadVideoName = '';
|
||||||
|
|
||||||
|
// 上传视频自定义参数
|
||||||
|
editor.config.uploadVideoParams = {
|
||||||
|
name: 'v2',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义参数拼接到 url 中
|
||||||
|
editor.config.uploadVideoParamsWithUrl = true;
|
||||||
|
|
||||||
|
// 上传视频自定义 header
|
||||||
|
editor.config.uploadVideoHeaders = {};
|
||||||
|
|
||||||
|
// 钩子函数
|
||||||
|
editor.config.uploadVideoHooks = {
|
||||||
|
// 上传视频之前
|
||||||
|
before: function (xhr) {
|
||||||
|
console.log(xhr)
|
||||||
|
|
||||||
|
// 可阻止视频上传
|
||||||
|
// return {
|
||||||
|
// prevent: true,
|
||||||
|
// msg: '需要提示给用户的错误信息'
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
// 视频上传并返回了结果,视频插入已成功
|
||||||
|
success: function (xhr) {
|
||||||
|
console.log('success', xhr)
|
||||||
|
},
|
||||||
|
// 视频上传并返回了结果,但视频插入时出错了
|
||||||
|
fail: function (xhr, editor, resData) {
|
||||||
|
console.log('fail', resData)
|
||||||
|
},
|
||||||
|
// 上传视频出错,一般为 http 请求的错误
|
||||||
|
error: function (xhr, editor, resData) {
|
||||||
|
console.log('error', xhr, resData)
|
||||||
|
},
|
||||||
|
// 上传视频超时
|
||||||
|
timeout: function (xhr) {
|
||||||
|
console.log('timeout')
|
||||||
|
},
|
||||||
|
// 视频上传并返回了结果,想要自己把视频插入到编辑器中
|
||||||
|
// 例如服务器端返回的不是 { errno: 0, data: {url: ....} } 这种格式,可使用 customInsert
|
||||||
|
customInsert: function (insertVideoFn, result) {
|
||||||
|
// result 即服务端返回的接口
|
||||||
|
console.log('customInsert', result)
|
||||||
|
|
||||||
|
// insertVideoFn 可把视频插入到编辑器,传入视频 src ,执行函数即可
|
||||||
|
insertVideoFn(result.data.url)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传视频超时时间 ms 默认5分钟
|
||||||
|
editor.config.uploadVideoTimeout = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
// 跨域带 cookie
|
||||||
|
editor.config.withVideoCredentials = false;
|
||||||
|
|
||||||
|
// 自定义alert
|
||||||
|
editor.config.customAlert = (s) => {
|
||||||
|
console.log('customAlert: ' + s)
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.create()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* @description 保存上传的文件
|
||||||
|
* @author wangfupeng
|
||||||
|
*/
|
||||||
|
|
||||||
|
const os = require('os')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const formidable = require('formidable')
|
||||||
|
const { objForEach } = require('../util')
|
||||||
|
const FILE_FOLDER = 'upload-files'
|
||||||
|
const isWindows = os.type().toLowerCase().indexOf('windows') >= 0
|
||||||
|
const TMP_FOLDER = 'upload-files-tmp'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取随机数
|
||||||
|
*/
|
||||||
|
function getRandom() {
|
||||||
|
return Math.random().toString(36).slice(-3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给文件名加后缀,如 a.png 转换为 a-123123.png
|
||||||
|
* @param {string} fileName 文件名
|
||||||
|
*/
|
||||||
|
function genRandomFileName(fileName = '') {
|
||||||
|
// 如 fileName === 'a.123.png'
|
||||||
|
|
||||||
|
const r = getRandom()
|
||||||
|
if (!fileName) return r
|
||||||
|
|
||||||
|
const length = fileName.length // 9
|
||||||
|
const pointLastIndexOf = fileName.lastIndexOf('.') // 5
|
||||||
|
if (pointLastIndexOf < 0) return `${fileName}-${r}`
|
||||||
|
|
||||||
|
const fileNameWithOutExt = fileName.slice(0, pointLastIndexOf) // "a.123"
|
||||||
|
const ext = fileName.slice(pointLastIndexOf + 1, length) // "png"
|
||||||
|
return `${fileNameWithOutExt}-${r}.${ext}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存上传的文件
|
||||||
|
* @param {Object} req request
|
||||||
|
*/
|
||||||
|
function saveFiles(req) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const videoLinks = []
|
||||||
|
const form = formidable({ multiples: true })
|
||||||
|
|
||||||
|
// windows 系统,处理 rename 报错
|
||||||
|
if (isWindows) {
|
||||||
|
const tmpPath = path.resolve(__dirname, '..', '..', TMP_FOLDER) // 在根目录下
|
||||||
|
if (!fs.existsSync(tmpPath)) {
|
||||||
|
fs.mkdirSync(tmpPath)
|
||||||
|
}
|
||||||
|
form.uploadDir = TMP_FOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
form.parse(req, function (err, fields, files) {
|
||||||
|
if (err) {
|
||||||
|
reject('formidable, form.parse err', err.stack)
|
||||||
|
}
|
||||||
|
// 存储视频的文件夹
|
||||||
|
const storePath = path.resolve(__dirname, '..', FILE_FOLDER)
|
||||||
|
if (!fs.existsSync(storePath)) {
|
||||||
|
fs.mkdirSync(storePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有上传来的视频
|
||||||
|
objForEach(files, (name, file) => {
|
||||||
|
console.log('name...', name)
|
||||||
|
|
||||||
|
// 视频临时位置
|
||||||
|
const tempFilePath = file.path
|
||||||
|
// 视频名称和路径
|
||||||
|
const fileName = genRandomFileName(name) // 为文件名增加一个随机数,防止同名文件覆盖
|
||||||
|
console.log('fileName...', fileName)
|
||||||
|
const fullFileName = path.join(storePath, fileName)
|
||||||
|
console.log('fullFileName...', fullFileName)
|
||||||
|
// 将临时文件保存为正式文件
|
||||||
|
fs.renameSync(tempFilePath, fullFileName)
|
||||||
|
// 存储链接
|
||||||
|
const url = `/server/${FILE_FOLDER}/` + fileName
|
||||||
|
videoLinks.push(url)
|
||||||
|
})
|
||||||
|
console.log('videoLinks...', videoLinks)
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
resolve({
|
||||||
|
errno: 0,
|
||||||
|
data: {
|
||||||
|
url: videoLinks[0]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = saveFiles
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
const router = require('koa-router')()
|
const router = require('koa-router')()
|
||||||
const saveFiles = require('./controller/save-file')
|
const saveFiles = require('./controller/save-file')
|
||||||
|
const saveVideoFile = require('./controller/saveVideo-file')
|
||||||
|
|
||||||
router.prefix('/api')
|
router.prefix('/api')
|
||||||
|
|
||||||
|
@ -19,4 +20,10 @@ router.post('/upload-img', async function (ctx, next) {
|
||||||
ctx.body = data
|
ctx.body = data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 上传视频
|
||||||
|
router.post('/upload-video', async function (ctx, next) {
|
||||||
|
const data = await saveVideoFile(ctx.req)
|
||||||
|
ctx.body = data
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -141,8 +141,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 上传图片的 panel 定制样式 */
|
/* 上传图片、上传视频的 panel 定制样式 */
|
||||||
.w-e-up-img-container {
|
.w-e-up-img-container, .w-e-up-video-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.w-e-up-btn {
|
.w-e-up-btn {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import imageConfig, { UploadImageHooksType } from './image'
|
||||||
import textConfig from './text'
|
import textConfig from './text'
|
||||||
import langConfig from './lang'
|
import langConfig from './lang'
|
||||||
import historyConfig from './history'
|
import historyConfig from './history'
|
||||||
import videoConfig from './video'
|
import videoConfig, { UploadVideoHooksType } from './video'
|
||||||
|
|
||||||
// 字典类型
|
// 字典类型
|
||||||
export type DicType = {
|
export type DicType = {
|
||||||
|
@ -76,6 +76,20 @@ export type ConfigType = {
|
||||||
|
|
||||||
onlineVideoCheck: Function
|
onlineVideoCheck: Function
|
||||||
onlineVideoCallback: Function
|
onlineVideoCallback: Function
|
||||||
|
|
||||||
|
showLinkVideo: Boolean
|
||||||
|
uploadVideoAccept: string[]
|
||||||
|
uploadVideoServer: string
|
||||||
|
uploadVideoMaxSize: number
|
||||||
|
uploadVideoName: string
|
||||||
|
uploadVideoParams: DicType
|
||||||
|
uploadVideoParamsWithUrl: boolean
|
||||||
|
uploadVideoHeaders: DicType
|
||||||
|
uploadVideoHooks: UploadVideoHooksType
|
||||||
|
uploadVideoTimeout: number
|
||||||
|
withVideoCredentials: boolean
|
||||||
|
customUploadVideo: Function | null
|
||||||
|
customInsertVideo: Function | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Resource = {
|
export type Resource = {
|
||||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
插入视频: '插入视频',
|
插入视频: '插入视频',
|
||||||
|
上传视频: '上传视频',
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
行: '行',
|
行: '行',
|
||||||
|
@ -126,6 +127,15 @@ export default {
|
||||||
请替换为支持的图片类型: '请替换为支持的图片类型',
|
请替换为支持的图片类型: '请替换为支持的图片类型',
|
||||||
您插入的网络图片无法识别: '您插入的网络图片无法识别',
|
您插入的网络图片无法识别: '您插入的网络图片无法识别',
|
||||||
您刚才插入的图片链接未通过编辑器校验: '您刚才插入的图片链接未通过编辑器校验',
|
您刚才插入的图片链接未通过编辑器校验: '您刚才插入的图片链接未通过编辑器校验',
|
||||||
|
插入视频错误: '插入视频错误',
|
||||||
|
视频链接: '视频链接',
|
||||||
|
不是视频: '不是视频',
|
||||||
|
视频验证未通过: '视频验证未通过',
|
||||||
|
个视频: '个视频',
|
||||||
|
上传视频超时: '上传视频超时',
|
||||||
|
上传视频错误: '上传视频错误',
|
||||||
|
上传视频失败: '上传视频失败',
|
||||||
|
上传视频返回结果错误: '上传视频返回结果错误',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -214,6 +224,7 @@ export default {
|
||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
插入视频: 'insert video',
|
插入视频: 'insert video',
|
||||||
|
上传视频: 'upload local video',
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
行: 'rows',
|
行: 'rows',
|
||||||
|
@ -255,6 +266,15 @@ export default {
|
||||||
您插入的网络图片无法识别: 'the network picture you inserted is not recognized',
|
您插入的网络图片无法识别: 'the network picture you inserted is not recognized',
|
||||||
您刚才插入的图片链接未通过编辑器校验:
|
您刚才插入的图片链接未通过编辑器校验:
|
||||||
'the image link you just inserted did not pass the editor verification',
|
'the image link you just inserted did not pass the editor verification',
|
||||||
|
插入视频错误: 'insert video error',
|
||||||
|
视频链接: 'video link',
|
||||||
|
不是视频: 'is not video',
|
||||||
|
视频验证未通过: 'video validate failed',
|
||||||
|
个视频: 'videos',
|
||||||
|
上传视频超时: 'upload video timeout',
|
||||||
|
上传视频错误: 'upload video error',
|
||||||
|
上传视频失败: 'upload video failed',
|
||||||
|
上传视频返回结果错误: 'upload video return results error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,26 @@
|
||||||
* @author hutianhao
|
* @author hutianhao
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Editor from '../editor/index'
|
||||||
import { EMPTY_FN } from '../utils/const'
|
import { EMPTY_FN } from '../utils/const'
|
||||||
|
import { ResType } from '../menus/video/upload-video'
|
||||||
|
|
||||||
|
export type UploadVideoHooksType = {
|
||||||
|
before?: (
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
editor: Editor,
|
||||||
|
files: File[]
|
||||||
|
) => { prevent: boolean; msg: string } | void
|
||||||
|
success?: (xhr: XMLHttpRequest, editor: Editor, result: ResType) => void
|
||||||
|
fail?: (xhr: XMLHttpRequest, editor: Editor, err: ResType | string) => void
|
||||||
|
error?: (xhr: XMLHttpRequest, editor: Editor) => void
|
||||||
|
timeout?: (xhr: XMLHttpRequest, editor: Editor) => void
|
||||||
|
customInsert?: (
|
||||||
|
inserVideo: (this: Editor, src: string) => void,
|
||||||
|
result: ResType,
|
||||||
|
editor: Editor
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 插入网络视频前的回调函数
|
// 插入网络视频前的回调函数
|
||||||
|
@ -13,4 +32,46 @@ export default {
|
||||||
|
|
||||||
// 插入网络视频成功之后的回调函数
|
// 插入网络视频成功之后的回调函数
|
||||||
onlineVideoCallback: EMPTY_FN,
|
onlineVideoCallback: EMPTY_FN,
|
||||||
|
|
||||||
|
// 显示“插入视频”
|
||||||
|
showLinkVideo: true,
|
||||||
|
|
||||||
|
// accept
|
||||||
|
uploadVideoAccept: ['mp4'],
|
||||||
|
|
||||||
|
// 服务端地址
|
||||||
|
uploadVideoServer: '',
|
||||||
|
|
||||||
|
// 上传视频的最大体积,默认 1024M
|
||||||
|
uploadVideoMaxSize: 1 * 1024 * 1024 * 1024,
|
||||||
|
|
||||||
|
// 一次最多上传多少个视频
|
||||||
|
// uploadVideoMaxLength: 2,
|
||||||
|
|
||||||
|
// 自定义上传视频的名称
|
||||||
|
uploadVideoName: '',
|
||||||
|
|
||||||
|
// 上传视频自定义参数
|
||||||
|
uploadVideoParams: {},
|
||||||
|
|
||||||
|
// 自定义参数拼接到 url 中
|
||||||
|
uploadVideoParamsWithUrl: false,
|
||||||
|
|
||||||
|
// 上传视频自定义 header
|
||||||
|
uploadVideoHeaders: {},
|
||||||
|
|
||||||
|
// 钩子函数
|
||||||
|
uploadVideoHooks: {},
|
||||||
|
|
||||||
|
// 上传视频超时时间 ms 默认2个小时
|
||||||
|
uploadVideoTimeout: 1000 * 60 * 60 * 2,
|
||||||
|
|
||||||
|
// 跨域带 cookie
|
||||||
|
withVideoCredentials: false,
|
||||||
|
|
||||||
|
// 自定义上传
|
||||||
|
customUploadVideo: null,
|
||||||
|
|
||||||
|
// 自定义插入视频
|
||||||
|
customInsertVideo: null,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @description 绑定视频的事件
|
||||||
|
* @author lichunlin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Editor from '../../../editor/index'
|
||||||
|
import bindTooltipVideo from './tooltip-event'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
* @param editor 编辑器实例
|
||||||
|
*/
|
||||||
|
function bindEvent(editor: Editor): void {
|
||||||
|
//Tooltip
|
||||||
|
bindTooltipVideo(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bindEvent
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* @description tooltip 事件
|
||||||
|
* @author lichunlin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import $, { DomElement } from '../../../utils/dom-core'
|
||||||
|
import Tooltip, { TooltipConfType } from '../../menu-constructors/Tooltip'
|
||||||
|
import Editor from '../../../editor/index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Tooltip 的显示隐藏函数
|
||||||
|
*/
|
||||||
|
export function createShowHideFn(editor: Editor) {
|
||||||
|
let tooltip: Tooltip | null
|
||||||
|
const t = (text: string, prefix: string = ''): string => {
|
||||||
|
return editor.i18next.t(prefix + text)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 显示 tooltip
|
||||||
|
* @param $node 链接元素
|
||||||
|
*/
|
||||||
|
function showVideoTooltip($node: DomElement) {
|
||||||
|
const conf: TooltipConfType = [
|
||||||
|
{
|
||||||
|
$elem: $("<span class='w-e-icon-trash-o'></span>"),
|
||||||
|
onClick: (editor: Editor, $node: DomElement) => {
|
||||||
|
// 选中video元素
|
||||||
|
editor.selection.createRangeByElem($node)
|
||||||
|
editor.selection.restoreSelection()
|
||||||
|
editor.cmd.do('delete')
|
||||||
|
// 返回 true,表示执行完之后,隐藏 tooltip。否则不隐藏。
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$elem: $('<span>100%</span>'),
|
||||||
|
onClick: (editor: Editor, $node: DomElement) => {
|
||||||
|
$node.attr('width', '100%')
|
||||||
|
$node.removeAttr('height')
|
||||||
|
// 返回 true,表示执行完之后,隐藏 tooltip。否则不隐藏。
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$elem: $('<span>50%</span>'),
|
||||||
|
onClick: (editor: Editor, $node: DomElement) => {
|
||||||
|
$node.attr('width', '50%')
|
||||||
|
$node.removeAttr('height')
|
||||||
|
// 返回 true,表示执行完之后,隐藏 tooltip。否则不隐藏。
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$elem: $('<span>30%</span>'),
|
||||||
|
onClick: (editor: Editor, $node: DomElement) => {
|
||||||
|
$node.attr('width', '30%')
|
||||||
|
$node.removeAttr('height')
|
||||||
|
// 返回 true,表示执行完之后,隐藏 tooltip。否则不隐藏。
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$elem: $(`<span>${t('重置')}</span>`),
|
||||||
|
onClick: (editor: Editor, $node: DomElement) => {
|
||||||
|
$node.removeAttr('width')
|
||||||
|
$node.removeAttr('height')
|
||||||
|
|
||||||
|
// 返回 true,表示执行完之后,隐藏 tooltip。否则不隐藏。
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
tooltip = new Tooltip(editor, $node, conf)
|
||||||
|
tooltip.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏 tooltip
|
||||||
|
*/
|
||||||
|
function hideVideoTooltip() {
|
||||||
|
// 移除 tooltip
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.remove()
|
||||||
|
tooltip = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showVideoTooltip,
|
||||||
|
hideVideoTooltip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 tooltip 事件
|
||||||
|
* @param editor 编辑器实例
|
||||||
|
*/
|
||||||
|
export default function bindTooltipEvent(editor: Editor) {
|
||||||
|
const { showVideoTooltip, hideVideoTooltip } = createShowHideFn(editor)
|
||||||
|
|
||||||
|
// 点击视频元素是,显示 tooltip
|
||||||
|
editor.txt.eventHooks.videoClickEvents.push(showVideoTooltip)
|
||||||
|
|
||||||
|
// 点击其他地方,或者滚动时,隐藏 tooltip
|
||||||
|
editor.txt.eventHooks.clickEvents.push(hideVideoTooltip)
|
||||||
|
editor.txt.eventHooks.keyupEvents.push(hideVideoTooltip)
|
||||||
|
editor.txt.eventHooks.toolbarClickEvents.push(hideVideoTooltip)
|
||||||
|
editor.txt.eventHooks.menuClickEvents.push(hideVideoTooltip)
|
||||||
|
editor.txt.eventHooks.textScrollEvents.push(hideVideoTooltip)
|
||||||
|
|
||||||
|
// change 时隐藏
|
||||||
|
editor.txt.eventHooks.changeEvents.push(hideVideoTooltip)
|
||||||
|
}
|
|
@ -4,16 +4,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Editor from '../../editor/index'
|
import Editor from '../../editor/index'
|
||||||
import { PanelConf } from '../menu-constructors/Panel'
|
import { PanelConf, PanelTabConf } from '../menu-constructors/Panel'
|
||||||
import { getRandom } from '../../utils/util'
|
import { getRandom } from '../../utils/util'
|
||||||
import $ from '../../utils/dom-core'
|
import $ from '../../utils/dom-core'
|
||||||
|
import UploadVideo from './upload-video'
|
||||||
import { videoRegex } from '../../utils/const'
|
import { videoRegex } from '../../utils/const'
|
||||||
|
|
||||||
export default function (editor: Editor, video: string): PanelConf {
|
export default function (editor: Editor, video: string): PanelConf {
|
||||||
|
const config = editor.config
|
||||||
|
const uploadVideo = new UploadVideo(editor)
|
||||||
|
|
||||||
// panel 中需要用到的id
|
// panel 中需要用到的id
|
||||||
const inputIFrameId = getRandom('input-iframe')
|
const inputIFrameId = getRandom('input-iframe')
|
||||||
const btnOkId = getRandom('btn-ok')
|
const btnOkId = getRandom('btn-ok')
|
||||||
const i18nPrefix = 'menus.panelMenus.video.'
|
const i18nPrefix = 'menus.panelMenus.video.'
|
||||||
|
const inputUploadId = getRandom('input-upload')
|
||||||
|
const btnStartId = getRandom('btn-local-ok')
|
||||||
const t = (text: string, prefix: string = i18nPrefix): string => {
|
const t = (text: string, prefix: string = i18nPrefix): string => {
|
||||||
return editor.i18next.t(prefix + text)
|
return editor.i18next.t(prefix + text)
|
||||||
}
|
}
|
||||||
|
@ -64,54 +70,118 @@ export default function (editor: Editor, video: string): PanelConf {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const conf = {
|
// tabs配置
|
||||||
|
// const fileMultipleAttr = config.uploadVideoMaxLength === 1 ? '' : 'multiple="multiple"'
|
||||||
|
const tabsConf: PanelTabConf[] = [
|
||||||
|
{
|
||||||
|
// tab 的标题
|
||||||
|
title: editor.i18next.t('menus.panelMenus.video.上传视频'),
|
||||||
|
tpl: `<div class="w-e-up-video-container">
|
||||||
|
<div id="${btnStartId}" class="w-e-up-btn">
|
||||||
|
<i class="w-e-icon-upload2"></i>
|
||||||
|
</div>
|
||||||
|
<div style="display:none;">
|
||||||
|
<input id="${inputUploadId}" type="file" accept="video/*"/>
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
events: [
|
||||||
|
// 触发选择视频
|
||||||
|
{
|
||||||
|
selector: '#' + btnStartId,
|
||||||
|
type: 'click',
|
||||||
|
fn: () => {
|
||||||
|
const $file = $('#' + inputUploadId)
|
||||||
|
const fileElem = $file.elems[0]
|
||||||
|
if (fileElem) {
|
||||||
|
fileElem.click()
|
||||||
|
} else {
|
||||||
|
// 返回 true 可关闭 panel
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 选择视频完毕
|
||||||
|
{
|
||||||
|
selector: '#' + inputUploadId,
|
||||||
|
type: 'change',
|
||||||
|
fn: () => {
|
||||||
|
const $file = $('#' + inputUploadId)
|
||||||
|
const fileElem = $file.elems[0]
|
||||||
|
if (!fileElem) {
|
||||||
|
// 返回 true 可关闭 panel
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的 file 对象列表
|
||||||
|
const fileList = (fileElem as any).files
|
||||||
|
if (fileList.length) {
|
||||||
|
uploadVideo.uploadVideo(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 true 可关闭 panel
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// tab 的标题
|
||||||
|
title: editor.i18next.t('menus.panelMenus.video.插入视频'),
|
||||||
|
// 模板
|
||||||
|
tpl: `<div>
|
||||||
|
<input
|
||||||
|
id="${inputIFrameId}"
|
||||||
|
type="text"
|
||||||
|
class="block"
|
||||||
|
placeholder="${editor.i18next.t('如')}:<iframe src=... ></iframe>"/>
|
||||||
|
</td>
|
||||||
|
<div class="w-e-button-container">
|
||||||
|
<button type="button" id="${btnOkId}" class="right">
|
||||||
|
${editor.i18next.t('插入')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
// 事件绑定
|
||||||
|
events: [
|
||||||
|
// 插入视频
|
||||||
|
{
|
||||||
|
selector: '#' + btnOkId,
|
||||||
|
type: 'click',
|
||||||
|
fn: () => {
|
||||||
|
// 执行插入视频
|
||||||
|
const $video = $('#' + inputIFrameId)
|
||||||
|
let video = $video.val().trim()
|
||||||
|
|
||||||
|
// 视频为空,则不插入
|
||||||
|
if (!video) return
|
||||||
|
// 对当前用户插入的内容进行判断,插入为空,或者返回false,都停止插入
|
||||||
|
if (!checkOnlineVideo(video)) return
|
||||||
|
|
||||||
|
insertVideo(video)
|
||||||
|
|
||||||
|
// 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, // tab end
|
||||||
|
]
|
||||||
|
|
||||||
|
const conf: PanelConf = {
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
|
||||||
// panel 中可包含多个 tab
|
// panel 中可包含多个 tab
|
||||||
tabs: [
|
tabs: [], // tabs end
|
||||||
{
|
}
|
||||||
// tab 的标题
|
|
||||||
title: editor.i18next.t('menus.panelMenus.video.插入视频'),
|
|
||||||
// 模板
|
|
||||||
tpl: `<div>
|
|
||||||
<input
|
|
||||||
id="${inputIFrameId}"
|
|
||||||
type="text"
|
|
||||||
class="block"
|
|
||||||
placeholder="${editor.i18next.t('如')}:<iframe src=... ></iframe>"/>
|
|
||||||
</td>
|
|
||||||
<div class="w-e-button-container">
|
|
||||||
<button type="button" id="${btnOkId}" class="right">
|
|
||||||
${editor.i18next.t('插入')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
// 事件绑定
|
|
||||||
events: [
|
|
||||||
// 插入视频
|
|
||||||
{
|
|
||||||
selector: '#' + btnOkId,
|
|
||||||
type: 'click',
|
|
||||||
fn: () => {
|
|
||||||
// 执行插入视频
|
|
||||||
const $video = $('#' + inputIFrameId)
|
|
||||||
let video = $video.val().trim()
|
|
||||||
|
|
||||||
// 视频为空,则不插入
|
// 显示“上传视频”
|
||||||
if (!video) return
|
if (window.FileReader && (config.uploadVideoServer || config.customUploadVideo)) {
|
||||||
// 对当前用户插入的内容进行判断,插入为空,或者返回false,都停止插入
|
conf.tabs.push(tabsConf[0])
|
||||||
if (!checkOnlineVideo(video)) return
|
}
|
||||||
|
// 显示“插入视频”
|
||||||
insertVideo(video)
|
if (config.showLinkVideo) {
|
||||||
|
conf.tabs.push(tabsConf[1])
|
||||||
// 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, // tab end
|
|
||||||
], // tabs end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return conf
|
return conf
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Editor from '../../editor/index'
|
||||||
import PanelMenu from '../menu-constructors/PanelMenu'
|
import PanelMenu from '../menu-constructors/PanelMenu'
|
||||||
import { MenuActive } from '../menu-constructors/Menu'
|
import { MenuActive } from '../menu-constructors/Menu'
|
||||||
import createPanelConf from './create-panel-conf'
|
import createPanelConf from './create-panel-conf'
|
||||||
|
import bindEvent from './bind-event/index'
|
||||||
|
|
||||||
class Video extends PanelMenu implements MenuActive {
|
class Video extends PanelMenu implements MenuActive {
|
||||||
constructor(editor: Editor) {
|
constructor(editor: Editor) {
|
||||||
|
@ -18,6 +19,9 @@ class Video extends PanelMenu implements MenuActive {
|
||||||
</div>`
|
</div>`
|
||||||
)
|
)
|
||||||
super($elem, editor)
|
super($elem, editor)
|
||||||
|
|
||||||
|
// 绑定事件 tootip
|
||||||
|
bindEvent(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
/**
|
||||||
|
* @description 上传视频
|
||||||
|
* @author lichunlin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Editor from '../../editor/index'
|
||||||
|
import { arrForEach, forEach } from '../../utils/util'
|
||||||
|
import post from '../../editor/upload/upload-core'
|
||||||
|
import Progress from '../../editor/upload/progress'
|
||||||
|
|
||||||
|
type ResData = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台返回的格式
|
||||||
|
export type ResType = {
|
||||||
|
errno: number | string
|
||||||
|
data: ResData
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadVideo {
|
||||||
|
private editor: Editor
|
||||||
|
|
||||||
|
constructor(editor: Editor) {
|
||||||
|
this.editor = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传视频
|
||||||
|
* @param files 文件列表
|
||||||
|
*/
|
||||||
|
public uploadVideo(files: FileList | File[]): void {
|
||||||
|
if (!files.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = this.editor
|
||||||
|
const config = editor.config
|
||||||
|
|
||||||
|
// ------------------------------ i18next ------------------------------
|
||||||
|
|
||||||
|
const i18nPrefix = 'validate.'
|
||||||
|
const t = (text: string): string => {
|
||||||
|
return editor.i18next.t(i18nPrefix + text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------ 获取配置信息 ------------------------------
|
||||||
|
// 服务端地址
|
||||||
|
let uploadVideoServer = config.uploadVideoServer
|
||||||
|
// 上传视频的最大体积,默认 1024M
|
||||||
|
const maxSize = config.uploadVideoMaxSize
|
||||||
|
const uploadVideoMaxSize = maxSize / 1024 / 1024
|
||||||
|
// 一次最多上传多少个视频
|
||||||
|
// const uploadVideoMaxLength = config.uploadVideoMaxLength
|
||||||
|
// 自定义上传视频的名称
|
||||||
|
const uploadVideoName = config.uploadVideoName
|
||||||
|
// 上传视频自定义参数
|
||||||
|
const uploadVideoParams = config.uploadVideoParams
|
||||||
|
// 自定义参数拼接到 url 中
|
||||||
|
const uploadVideoParamsWithUrl = config.uploadVideoParamsWithUrl
|
||||||
|
// 上传视频自定义 header
|
||||||
|
const uploadVideoHeaders = config.uploadVideoHeaders
|
||||||
|
// 钩子函数
|
||||||
|
const uploadVideoHooks = config.uploadVideoHooks
|
||||||
|
// 上传视频超时时间 ms 默认2个小时
|
||||||
|
const uploadVideoTimeout = config.uploadVideoTimeout
|
||||||
|
// 跨域带 cookie
|
||||||
|
const withVideoCredentials = config.withVideoCredentials
|
||||||
|
// 自定义上传
|
||||||
|
const customUploadVideo = config.customUploadVideo
|
||||||
|
// 格式校验
|
||||||
|
const uploadVideoAccept = config.uploadVideoAccept
|
||||||
|
|
||||||
|
// ------------------------------ 验证文件信息 ------------------------------
|
||||||
|
const resultFiles: File[] = []
|
||||||
|
const errInfos: string[] = []
|
||||||
|
arrForEach(files, file => {
|
||||||
|
const name = file.name
|
||||||
|
const size = file.size / 1024 / 1024
|
||||||
|
|
||||||
|
// chrome 低版本 name === undefined
|
||||||
|
if (!name || !size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(uploadVideoAccept instanceof Array)) {
|
||||||
|
// 格式不是数组
|
||||||
|
errInfos.push(`【${uploadVideoAccept}】${t('uploadVideoAccept 不是Array')}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!uploadVideoAccept.some(
|
||||||
|
item => item === name.split('.')[name.split('.').length - 1]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 后缀名不合法,不是视频
|
||||||
|
errInfos.push(`【${name}】${t('不是视频')}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadVideoMaxSize < size) {
|
||||||
|
// 上传视频过大
|
||||||
|
errInfos.push(`【${name}】${t('大于')} ${uploadVideoMaxSize}M`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//验证通过的加入结果列表
|
||||||
|
resultFiles.push(file)
|
||||||
|
})
|
||||||
|
// 抛出验证信息
|
||||||
|
if (errInfos.length) {
|
||||||
|
config.customAlert(`${t('视频验证未通过')}: \n` + errInfos.join('\n'), 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果过滤后文件列表为空直接返回
|
||||||
|
if (resultFiles.length === 0) {
|
||||||
|
config.customAlert(t('传入的文件不合法'), 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------ 自定义上传 ------------------------------
|
||||||
|
if (customUploadVideo && typeof customUploadVideo === 'function') {
|
||||||
|
customUploadVideo(resultFiles, this.insertVideo.bind(this))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加视频数据
|
||||||
|
const formData = new FormData()
|
||||||
|
resultFiles.forEach((file: File, index: number) => {
|
||||||
|
let name = uploadVideoName || file.name
|
||||||
|
if (resultFiles.length > 1) {
|
||||||
|
// 多个文件时,filename 不能重复
|
||||||
|
name = name + (index + 1)
|
||||||
|
}
|
||||||
|
formData.append(name, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ------------------------------ 上传视频 ------------------------------
|
||||||
|
|
||||||
|
//添加自定义参数 基于有服务端地址的情况下
|
||||||
|
if (uploadVideoServer) {
|
||||||
|
// 添加自定义参数
|
||||||
|
const uploadVideoServerArr = uploadVideoServer.split('#')
|
||||||
|
uploadVideoServer = uploadVideoServerArr[0]
|
||||||
|
const uploadVideoServerHash = uploadVideoServerArr[1] || ''
|
||||||
|
forEach(uploadVideoParams, (key: string, val: string) => {
|
||||||
|
// 因使用者反应,自定义参数不能默认 encode ,由 v3.1.1 版本开始注释掉
|
||||||
|
// val = encodeURIComponent(val)
|
||||||
|
|
||||||
|
// 第一,将参数拼接到 url 中
|
||||||
|
if (uploadVideoParamsWithUrl) {
|
||||||
|
if (uploadVideoServer.indexOf('?') > 0) {
|
||||||
|
uploadVideoServer += '&'
|
||||||
|
} else {
|
||||||
|
uploadVideoServer += '?'
|
||||||
|
}
|
||||||
|
uploadVideoServer = uploadVideoServer + key + '=' + val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二,将参数添加到 formData 中
|
||||||
|
formData.append(key, val)
|
||||||
|
})
|
||||||
|
if (uploadVideoServerHash) {
|
||||||
|
uploadVideoServer += '#' + uploadVideoServerHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
const xhr = post(uploadVideoServer, {
|
||||||
|
timeout: uploadVideoTimeout,
|
||||||
|
formData,
|
||||||
|
headers: uploadVideoHeaders,
|
||||||
|
withCredentials: !!withVideoCredentials,
|
||||||
|
beforeSend: xhr => {
|
||||||
|
if (uploadVideoHooks.before)
|
||||||
|
return uploadVideoHooks.before(xhr, editor, resultFiles)
|
||||||
|
},
|
||||||
|
onTimeout: xhr => {
|
||||||
|
config.customAlert(t('上传视频超时'), 'error')
|
||||||
|
if (uploadVideoHooks.timeout) uploadVideoHooks.timeout(xhr, editor)
|
||||||
|
},
|
||||||
|
onProgress: (percent, e) => {
|
||||||
|
const progressBar = new Progress(editor)
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
percent = e.loaded / e.total
|
||||||
|
progressBar.show(percent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: xhr => {
|
||||||
|
config.customAlert(
|
||||||
|
t('上传视频错误'),
|
||||||
|
'error',
|
||||||
|
`${t('上传视频错误')},${t('服务器返回状态')}: ${xhr.status}`
|
||||||
|
)
|
||||||
|
if (uploadVideoHooks.error) uploadVideoHooks.error(xhr, editor)
|
||||||
|
},
|
||||||
|
onFail: (xhr, resultStr) => {
|
||||||
|
config.customAlert(
|
||||||
|
t('上传视频失败'),
|
||||||
|
'error',
|
||||||
|
t('上传视频返回结果错误') + `,${t('返回结果')}: ` + resultStr
|
||||||
|
)
|
||||||
|
if (uploadVideoHooks.fail) uploadVideoHooks.fail(xhr, editor, resultStr)
|
||||||
|
},
|
||||||
|
onSuccess: (xhr, result: ResType) => {
|
||||||
|
if (uploadVideoHooks.customInsert) {
|
||||||
|
// 自定义插入视频
|
||||||
|
uploadVideoHooks.customInsert(this.insertVideo.bind(this), result, editor)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (result.errno != '0') {
|
||||||
|
// 返回格式不对,应该为 { errno: 0, data: [...] }
|
||||||
|
config.customAlert(
|
||||||
|
t('上传视频失败'),
|
||||||
|
'error',
|
||||||
|
`${t('上传视频返回结果错误')},${t('返回结果')} errno=${result.errno}`
|
||||||
|
)
|
||||||
|
if (uploadVideoHooks.fail) uploadVideoHooks.fail(xhr, editor, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功,插入视频
|
||||||
|
const data = result.data
|
||||||
|
|
||||||
|
this.insertVideo(data.url)
|
||||||
|
|
||||||
|
// 钩子函数
|
||||||
|
if (uploadVideoHooks.success) uploadVideoHooks.success(xhr, editor, result)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (typeof xhr === 'string') {
|
||||||
|
// 上传被阻止
|
||||||
|
config.customAlert(xhr, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 往编辑器区域插入视频
|
||||||
|
* @param url 视频访问地址
|
||||||
|
*/
|
||||||
|
public insertVideo(url: string): void {
|
||||||
|
const editor = this.editor
|
||||||
|
const config = editor.config
|
||||||
|
|
||||||
|
const i18nPrefix = 'validate.'
|
||||||
|
const t = (text: string, prefix: string = i18nPrefix): string => {
|
||||||
|
return editor.i18next.t(prefix + text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断用户是否自定义插入视频
|
||||||
|
if (!config.customInsertVideo) {
|
||||||
|
editor.cmd.do(
|
||||||
|
'insertHTML',
|
||||||
|
`<p><video src="${url}" controls="controls" style="max-width:100%"></video></p><p><br></p>`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
config.customInsertVideo(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载视频
|
||||||
|
let video: any = document.createElement('video')
|
||||||
|
video.onload = () => {
|
||||||
|
video = null
|
||||||
|
}
|
||||||
|
video.onerror = () => {
|
||||||
|
config.customAlert(
|
||||||
|
t('插入视频错误'),
|
||||||
|
'error',
|
||||||
|
`wangEditor: ${t('插入视频错误')},${t('视频链接')} "${url}",${t('下载链接失败')}`
|
||||||
|
)
|
||||||
|
video = null
|
||||||
|
}
|
||||||
|
video.onabort = () => (video = null)
|
||||||
|
video.src = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadVideo
|
|
@ -54,6 +54,8 @@ type TextEventHooks = {
|
||||||
dropListMenuHoverEvents: (() => void)[]
|
dropListMenuHoverEvents: (() => void)[]
|
||||||
/** 点击分割线时 */
|
/** 点击分割线时 */
|
||||||
splitLineEvents: ((e: DomElement) => void)[]
|
splitLineEvents: ((e: DomElement) => void)[]
|
||||||
|
/** 视频点击事件 */
|
||||||
|
videoClickEvents: ((e: DomElement) => void)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Text {
|
class Text {
|
||||||
|
@ -85,6 +87,7 @@ class Text {
|
||||||
menuClickEvents: [],
|
menuClickEvents: [],
|
||||||
dropListMenuHoverEvents: [],
|
dropListMenuHoverEvents: [],
|
||||||
splitLineEvents: [],
|
splitLineEvents: [],
|
||||||
|
videoClickEvents: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +561,27 @@ class Text {
|
||||||
const enterDownEvents = eventHooks.enterDownEvents
|
const enterDownEvents = eventHooks.enterDownEvents
|
||||||
enterDownEvents.forEach(fn => fn(e))
|
enterDownEvents.forEach(fn => fn(e))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 视频 click
|
||||||
|
$textElem.on('click', (e: Event) => {
|
||||||
|
// 存储视频
|
||||||
|
let $video: DomElement | null = null
|
||||||
|
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
const $target = $(target)
|
||||||
|
|
||||||
|
//处理视频点击 简单的video 标签
|
||||||
|
if ($target.getNodeName() === 'VIDEO') {
|
||||||
|
// 当前点击的就是视频
|
||||||
|
e.stopPropagation()
|
||||||
|
$video = $target
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$video) return // 没有点击视频,则返回
|
||||||
|
|
||||||
|
const videoClickEvents = eventHooks.videoClickEvents
|
||||||
|
videoClickEvents.forEach(fn => fn($video as DomElement))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* @description Video menu tooltip-event
|
||||||
|
* @author lichunlin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import createEditor from '../../../helpers/create-editor'
|
||||||
|
import $ from '../../../../src/utils/dom-core'
|
||||||
|
import bindTooltipEvent, * as tooltipEvent from '../../../../src/menus/video/bind-event/tooltip-event'
|
||||||
|
|
||||||
|
describe('Video menu tooltip-event', () => {
|
||||||
|
test('绑定 tooltip-event 事件', () => {
|
||||||
|
const editor = createEditor(document, 'div1')
|
||||||
|
|
||||||
|
bindTooltipEvent(editor)
|
||||||
|
|
||||||
|
expect(editor.txt.eventHooks.videoClickEvents.length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 createShowHideFn 函数返回显示和隐藏tooltip方法', () => {
|
||||||
|
const editor = createEditor(document, 'div2')
|
||||||
|
const fns = tooltipEvent.createShowHideFn(editor)
|
||||||
|
|
||||||
|
expect(fns.showVideoTooltip instanceof Function).toBeTruthy()
|
||||||
|
expect(fns.hideVideoTooltip instanceof Function).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('绑定 tooltip-event 事件,执行视频点击事件会展示tooltip', () => {
|
||||||
|
const editor = createEditor(document, 'div4')
|
||||||
|
|
||||||
|
bindTooltipEvent(editor)
|
||||||
|
|
||||||
|
const videoClickEvents = editor.txt.eventHooks.videoClickEvents
|
||||||
|
|
||||||
|
videoClickEvents.forEach(fn => {
|
||||||
|
fn($(editor.$textElem))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect($('.w-e-tooltip').elems[0]).not.toBeUndefined()
|
||||||
|
expect($('.w-e-tooltip').elems[0].childNodes.length).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('绑定 tooltip-event 事件,执行视频之外的其它点击事件会隐藏tooltip', () => {
|
||||||
|
const editor = createEditor(document, 'div5')
|
||||||
|
|
||||||
|
bindTooltipEvent(editor)
|
||||||
|
|
||||||
|
const videoClickEvents = editor.txt.eventHooks.videoClickEvents
|
||||||
|
const clickEvents = editor.txt.eventHooks.clickEvents
|
||||||
|
|
||||||
|
videoClickEvents.forEach(fn => {
|
||||||
|
fn($('<div></div>'))
|
||||||
|
})
|
||||||
|
|
||||||
|
clickEvents.forEach(fn => {
|
||||||
|
// @ts-ignore
|
||||||
|
fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect($('#div5 .w-e-tooltip').elems[0]).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,455 @@
|
||||||
|
/**
|
||||||
|
* @description upload-video test
|
||||||
|
* @author lichunlin
|
||||||
|
*/
|
||||||
|
import createEditor from '../../../helpers/create-editor'
|
||||||
|
import mockCmdFn from '../../../helpers/command-mock'
|
||||||
|
import mockFile from '../../../helpers/mock-file'
|
||||||
|
import mockXHR from '../../../helpers/mock-xhr'
|
||||||
|
import Editor from '../../../../src/editor'
|
||||||
|
import UploadVideo from '../../../../src/menus/video/upload-video'
|
||||||
|
|
||||||
|
let editor: Editor
|
||||||
|
let id = 1
|
||||||
|
|
||||||
|
const videoUrl =
|
||||||
|
'https://stream7.iqilu.com/10339/upload_transcode/202002/18/20200218114723HDu3hhxqIT.mp4'
|
||||||
|
const uploadVideoServer = 'http://localhost:8881/api/upload-video'
|
||||||
|
|
||||||
|
const defaultRes = {
|
||||||
|
status: 200,
|
||||||
|
res: JSON.stringify({ data: { url: '' }, errno: 0 }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockXHRHttpRequest = (res: any = defaultRes) => {
|
||||||
|
const mockXHRObject = mockXHR(res)
|
||||||
|
|
||||||
|
const mockObject = jest.fn().mockImplementation(() => mockXHRObject)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.XMLHttpRequest = mockObject
|
||||||
|
|
||||||
|
return mockXHRObject
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUploadVideoInstance = (config: any) => {
|
||||||
|
const editor = createEditor(document, `div${id++}`, '', config)
|
||||||
|
const uploadVideo = new UploadVideo(editor)
|
||||||
|
return uploadVideo
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockSupportCommand = () => {
|
||||||
|
mockCmdFn(document)
|
||||||
|
document.queryCommandSupported = jest.fn(() => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deaultFiles = [{ name: '测试.mp4', size: 512, mimeType: 'video/mp4' }]
|
||||||
|
const createMockFilse = (fileList: any[] = deaultFiles) => {
|
||||||
|
const files = fileList.map(file => mockFile(file))
|
||||||
|
return files.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('upload video', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor = createEditor(document, `div${id++}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('能够初始化基本的UploadVideo类', () => {
|
||||||
|
const uploadVideo = new UploadVideo(editor)
|
||||||
|
|
||||||
|
expect(uploadVideo.insertVideo instanceof Function).toBeTruthy()
|
||||||
|
expect(uploadVideo.insertVideo instanceof Function).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 insertVideo 可以网编辑器里插入视频', () => {
|
||||||
|
const uploadVideo = new UploadVideo(editor)
|
||||||
|
|
||||||
|
mockSupportCommand()
|
||||||
|
|
||||||
|
uploadVideo.insertVideo(videoUrl)
|
||||||
|
|
||||||
|
expect(document.execCommand).toBeCalledWith(
|
||||||
|
'insertHTML',
|
||||||
|
false,
|
||||||
|
`<p><video src="${videoUrl}" controls="controls" style="max-width:100%"></video></p><p><br></p>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 insertVideo 可以网编辑器里插入视频,可以监听插入视频回调', () => {
|
||||||
|
const callback = jest.fn()
|
||||||
|
|
||||||
|
const uploadVideo = createUploadVideoInstance({
|
||||||
|
linkVideoCallback: callback,
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSupportCommand()
|
||||||
|
|
||||||
|
uploadVideo.insertVideo(videoUrl)
|
||||||
|
|
||||||
|
expect(document.execCommand).toBeCalledWith(
|
||||||
|
'insertHTML',
|
||||||
|
false,
|
||||||
|
`<p><video src="${videoUrl}" controls="controls" style="max-width:100%"></video></p><p><br></p>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频', done => {
|
||||||
|
expect.assertions(1)
|
||||||
|
|
||||||
|
const jestFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
success: jestFn,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(jestFn).toBeCalled()
|
||||||
|
done()
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果传入的文件为空直接返回', () => {
|
||||||
|
const upload = new UploadVideo(editor)
|
||||||
|
|
||||||
|
const res = upload.uploadVideo([])
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果没有配置customUploadVideo, 则必须配置 uploadVideoServer ', () => {
|
||||||
|
const upload = new UploadVideo(editor)
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const res = upload.uploadVideo(files)
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果文件没有名字或者size为,则会被过滤掉', () => {
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
customAlert: fn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse([{ name: '', size: 0, mimeType: 'video/mp4' }])
|
||||||
|
|
||||||
|
const res = upload.uploadVideo(files)
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
expect(fn).toBeCalledWith('传入的文件不合法', 'warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果文件非视频,则返回并提示错误信息', () => {
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
customAlert: fn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse([{ name: 'test.txt', size: 200, mimeType: 'text/plain' }])
|
||||||
|
|
||||||
|
const res = upload.uploadVideo(files)
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
expect(fn).toBeCalledWith('视频验证未通过: \n【test.txt】不是视频', 'warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果文件体积大小超过配置的大小,则返回并提示错误信息', () => {
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoMaxSize: 5 * 1024 * 1024,
|
||||||
|
customAlert: fn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse([
|
||||||
|
{ name: 'test.mp4', size: 6 * 1024 * 1024, mimeType: 'video/mp4' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const res = upload.uploadVideo(files)
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
expect(fn).toBeCalledWith(`视频验证未通过: \n【test.mp4】大于 5M`, 'warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果配置了 customUploadVideo 选项,则调用customUploadVideo上传', () => {
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
customUploadVideo: fn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const res = upload.uploadVideo(files)
|
||||||
|
|
||||||
|
expect(res).toBeUndefined()
|
||||||
|
expect(fn).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,如果可以配置uploadVideoParamsWithUrl添加query参数', done => {
|
||||||
|
expect.assertions(1)
|
||||||
|
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoParams: {
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
},
|
||||||
|
uploadVideoParamsWithUrl: true,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
success: fn,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(fn).toBeCalled()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频,uploadVideoServer支持hash参数拼接', done => {
|
||||||
|
expect.assertions(1)
|
||||||
|
|
||||||
|
const fn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoParams: {
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
},
|
||||||
|
uploadVideoParamsWithUrl: true,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
success: fn,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse([
|
||||||
|
{ name: 'test1.mp4', size: 2048, mimeType: 'video/mp4' },
|
||||||
|
{ name: 'test2.mp4', size: 2048, mimeType: 'video/mp4' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(fn).toBeCalled()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频失败,会有错误提示,并支持配置onError hook', done => {
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
const fn = jest.fn()
|
||||||
|
const alertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
error: fn,
|
||||||
|
},
|
||||||
|
customAlert: alertFn,
|
||||||
|
})
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest({ status: 500 })
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(fn).toBeCalled()
|
||||||
|
expect(alertFn).toBeCalledWith(
|
||||||
|
'上传视频错误',
|
||||||
|
'error',
|
||||||
|
'上传视频错误,服务器返回状态: 500'
|
||||||
|
)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频后数据返回不正常,会有错误提示,并支持配置onFail hook', done => {
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
const fn = jest.fn()
|
||||||
|
const alertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
fail: fn,
|
||||||
|
},
|
||||||
|
customAlert: alertFn,
|
||||||
|
})
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest({
|
||||||
|
status: 200,
|
||||||
|
res: '{test: 123}',
|
||||||
|
})
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(fn).toBeCalled()
|
||||||
|
expect(alertFn).toBeCalledWith(
|
||||||
|
'上传视频失败',
|
||||||
|
'error',
|
||||||
|
'上传视频返回结果错误,返回结果: {test: 123}'
|
||||||
|
)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传视频成功后,支持自定义插入视频函数', done => {
|
||||||
|
expect.assertions(1)
|
||||||
|
|
||||||
|
const insertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
customInsert: insertFn,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(insertFn).toBeCalled()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传被阻止,会有错误提示', done => {
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
const beforFn = jest.fn(() => ({ prevent: true, msg: '阻止发送请求' }))
|
||||||
|
const alertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
before: beforFn,
|
||||||
|
},
|
||||||
|
customAlert: alertFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(beforFn).toBeCalled()
|
||||||
|
expect(alertFn).toBeCalledWith('阻止发送请求', 'error')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传返回的错误码不符合条件会有错误提示,并触发fail回调', done => {
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
const failFn = jest.fn()
|
||||||
|
const alertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
fail: failFn,
|
||||||
|
},
|
||||||
|
customAlert: alertFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
|
||||||
|
const mockXHRObject = mockXHRHttpRequest({
|
||||||
|
status: 200,
|
||||||
|
res: { test: 123, errno: -1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.onreadystatechange()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(failFn).toBeCalled()
|
||||||
|
expect(alertFn).toBeCalledWith(
|
||||||
|
'上传视频失败',
|
||||||
|
'error',
|
||||||
|
'上传视频返回结果错误,返回结果 errno=-1'
|
||||||
|
)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('调用 uploadVideo 上传超时会触发超时回调', done => {
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
const timeoutFn = jest.fn()
|
||||||
|
const alertFn = jest.fn()
|
||||||
|
|
||||||
|
const upload = createUploadVideoInstance({
|
||||||
|
uploadVideoServer,
|
||||||
|
uploadVideoHooks: {
|
||||||
|
timeout: timeoutFn,
|
||||||
|
},
|
||||||
|
customAlert: alertFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = createMockFilse()
|
||||||
|
const mockXHRObject = mockXHRHttpRequest()
|
||||||
|
|
||||||
|
upload.uploadVideo(files)
|
||||||
|
|
||||||
|
mockXHRObject.ontimeout()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(timeoutFn).toBeCalled()
|
||||||
|
expect(alertFn).toBeCalledWith('上传视频超时', 'error')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue