commit
db9b3ad7f3
|
@ -0,0 +1,30 @@
|
|||
import Editor from '../../../../src/editor/index'
|
||||
|
||||
describe('test editor.txt.append()', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/examples/txt-append-function.html')
|
||||
})
|
||||
|
||||
const text = 'test123'
|
||||
|
||||
it('editor.txt.append()功能光标位置是否正常', () => {
|
||||
cy.get('#div1').contains('欢迎使用 wangEditor 富文本编辑器')
|
||||
|
||||
cy.getEditor().then((editor: Editor) => {
|
||||
const menusLen = editor.menus.menuList.length + 1
|
||||
cy.getByClass('toolbar')
|
||||
.children()
|
||||
.should('have.length', menusLen + 1)
|
||||
})
|
||||
|
||||
cy.get('#div1')
|
||||
.find('.w-e-text-container')
|
||||
.children()
|
||||
.first()
|
||||
.as('Editable')
|
||||
.should('have.attr', 'contenteditable', 'true')
|
||||
// 上面部分先确认编辑器是否生成
|
||||
cy.get('@Editable').type(text)
|
||||
cy.get('@Editable').contains('123' + text)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
import Editor from '../../../../src/editor/index'
|
||||
|
||||
describe('test editor.txt.html()功能', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/examples/txt-html-function.html')
|
||||
})
|
||||
|
||||
const text = 'test123'
|
||||
|
||||
it('editor.txt.html()功能光标位置是否正常', () => {
|
||||
cy.get('#div1').contains('123456')
|
||||
|
||||
cy.getEditor().then((editor: Editor) => {
|
||||
const menusLen = editor.menus.menuList.length + 1
|
||||
cy.getByClass('toolbar')
|
||||
.children()
|
||||
.should('have.length', menusLen + 1)
|
||||
})
|
||||
|
||||
cy.get('#div1')
|
||||
.find('.w-e-text-container')
|
||||
.children()
|
||||
.first()
|
||||
.as('Editable')
|
||||
.should('have.attr', 'contenteditable', 'true')
|
||||
// 上面部分先确认编辑器是否生成
|
||||
cy.get('@Editable').type(text)
|
||||
cy.get('@Editable').contains('456' + text)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
<!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>
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
wangEditor demo
|
||||
</p>
|
||||
<div id="div1">
|
||||
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
|
||||
<p>
|
||||
<img src="http://www.wangeditor.com/imgs/logo.jpeg" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="../dist/wangEditor.js"></script>
|
||||
<script>
|
||||
// 改为使用var声明,才能在window对象上获取到编辑器实例,方便e2e测试
|
||||
var E = window.wangEditor
|
||||
var editor = new E('#div1')
|
||||
|
||||
editor.config.onchange = function (newHtml) {
|
||||
console.log('onchange', newHtml)
|
||||
}
|
||||
|
||||
editor.config.showFullScreen = true
|
||||
editor.create()
|
||||
editor.txt.append("<p>123</p>")
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,39 @@
|
|||
<!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>
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
wangEditor demo
|
||||
</p>
|
||||
<div id="div1">
|
||||
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
|
||||
<p>
|
||||
<img src="http://www.wangeditor.com/imgs/logo.jpeg" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="../dist/wangEditor.js"></script>
|
||||
<script>
|
||||
// 改为使用var声明,才能在window对象上获取到编辑器实例,方便e2e测试
|
||||
var E = window.wangEditor
|
||||
var editor = new E('#div1')
|
||||
|
||||
editor.config.onchange = function (newHtml) {
|
||||
console.log('onchange', newHtml)
|
||||
}
|
||||
|
||||
editor.config.showFullScreen = true
|
||||
editor.create()
|
||||
editor.txt.html("<p>123</p><p>456</p>")
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -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>
|
|
@ -29,10 +29,10 @@
|
|||
// 测试如果输入'测试',返回false,停止插入
|
||||
editor.config.onlineVideoCheck = function (video) {
|
||||
if (video === '测试') {
|
||||
return '测试禁止插入';
|
||||
return '测试禁止插入'
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
editor.config.onlineVideoCallback = function (video) {
|
||||
|
|
|
@ -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 saveFiles = require('./controller/save-file')
|
||||
const saveVideoFile = require('./controller/saveVideo-file')
|
||||
|
||||
router.prefix('/api')
|
||||
|
||||
|
@ -19,4 +20,10 @@ router.post('/upload-img', async function (ctx, next) {
|
|||
ctx.body = data
|
||||
})
|
||||
|
||||
// 上传视频
|
||||
router.post('/upload-video', async function (ctx, next) {
|
||||
const data = await saveVideoFile(ctx.req)
|
||||
ctx.body = data
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -141,8 +141,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* 上传图片的 panel 定制样式 */
|
||||
.w-e-up-img-container {
|
||||
/* 上传图片、上传视频的 panel 定制样式 */
|
||||
.w-e-up-img-container, .w-e-up-video-container {
|
||||
text-align: center;
|
||||
|
||||
.w-e-up-btn {
|
||||
|
|
|
@ -25,6 +25,10 @@ export type UploadImageHooksType = {
|
|||
}
|
||||
|
||||
export default {
|
||||
// 网络图片校验的配置函数
|
||||
linkImgCheck: function (src: string): string | boolean {
|
||||
return true
|
||||
},
|
||||
// 显示“插入网络图片”
|
||||
showLinkImg: true,
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import imageConfig, { UploadImageHooksType } from './image'
|
|||
import textConfig from './text'
|
||||
import langConfig from './lang'
|
||||
import historyConfig from './history'
|
||||
import videoConfig from './video'
|
||||
import videoConfig, { UploadVideoHooksType } from './video'
|
||||
|
||||
// 字典类型
|
||||
export type DicType = {
|
||||
|
@ -76,6 +76,20 @@ export type ConfigType = {
|
|||
|
||||
onlineVideoCheck: 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 = {
|
||||
|
@ -110,12 +124,6 @@ const defaultConfig = Object.assign(
|
|||
linkCheck: function (text: string, link: string): string | boolean {
|
||||
return true
|
||||
},
|
||||
},
|
||||
//网络图片校验的配置函数
|
||||
{
|
||||
linkImgCheck: function (src: string): string | boolean {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
恢复: '恢复',
|
||||
撤销: '撤销',
|
||||
全屏: '全屏',
|
||||
代办事项: '代办事项',
|
||||
待办事项: '待办事项',
|
||||
},
|
||||
dropListMenu: {
|
||||
设置标题: '设置标题',
|
||||
|
@ -86,6 +86,7 @@ export default {
|
|||
},
|
||||
video: {
|
||||
插入视频: '插入视频',
|
||||
上传视频: '上传视频',
|
||||
},
|
||||
table: {
|
||||
行: '行',
|
||||
|
@ -126,6 +127,15 @@ export default {
|
|||
请替换为支持的图片类型: '请替换为支持的图片类型',
|
||||
您插入的网络图片无法识别: '您插入的网络图片无法识别',
|
||||
您刚才插入的图片链接未通过编辑器校验: '您刚才插入的图片链接未通过编辑器校验',
|
||||
插入视频错误: '插入视频错误',
|
||||
视频链接: '视频链接',
|
||||
不是视频: '不是视频',
|
||||
视频验证未通过: '视频验证未通过',
|
||||
个视频: '个视频',
|
||||
上传视频超时: '上传视频超时',
|
||||
上传视频错误: '上传视频错误',
|
||||
上传视频失败: '上传视频失败',
|
||||
上传视频返回结果错误: '上传视频返回结果错误',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -164,7 +174,7 @@ export default {
|
|||
恢复: 'undo',
|
||||
撤销: 'redo',
|
||||
全屏: 'fullscreen',
|
||||
代办事项: 'todo',
|
||||
待办事项: 'todo',
|
||||
},
|
||||
dropListMenu: {
|
||||
设置标题: 'title',
|
||||
|
@ -214,6 +224,7 @@ export default {
|
|||
},
|
||||
video: {
|
||||
插入视频: 'insert video',
|
||||
上传视频: 'upload local video',
|
||||
},
|
||||
table: {
|
||||
行: 'rows',
|
||||
|
@ -255,6 +266,15 @@ export default {
|
|||
您插入的网络图片无法识别: 'the network picture you inserted is not recognized',
|
||||
您刚才插入的图片链接未通过编辑器校验:
|
||||
'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
|
||||
*/
|
||||
|
||||
import Editor from '../editor/index'
|
||||
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 {
|
||||
// 插入网络视频前的回调函数
|
||||
|
@ -13,4 +32,46 @@ export default {
|
|||
|
||||
// 插入网络视频成功之后的回调函数
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -109,12 +109,16 @@ export default class RangeCache extends Cache<[RangeItem, RangeItem]> {
|
|||
* @param range 缓存的 Range 数据
|
||||
*/
|
||||
public set(range: RangeItem | undefined) {
|
||||
if (range) {
|
||||
const handle = this.rangeHandle
|
||||
handle.setStart(...range.start)
|
||||
handle.setEnd(...range.end)
|
||||
this.editor.menus.changeActive()
|
||||
return true
|
||||
try {
|
||||
if (range) {
|
||||
const handle = this.rangeHandle
|
||||
handle.setStart(...range.start)
|
||||
handle.setEnd(...range.end)
|
||||
this.editor.menus.changeActive()
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -179,7 +179,6 @@ class Editor {
|
|||
public destroy(): void {
|
||||
// 调用钩子函数
|
||||
this.beforeDestroyHooks.forEach(fn => fn.call(this))
|
||||
|
||||
// 销毁 DOM 节点
|
||||
this.$toolbarElem.remove()
|
||||
this.$textContainerElem.remove()
|
||||
|
|
|
@ -56,10 +56,7 @@ function _bindFocusAndBlur(editor: Editor): void {
|
|||
if (isToolbar && !isMenu) {
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.isFocus) {
|
||||
_blurHandler(editor)
|
||||
}
|
||||
_blurHandler(editor)
|
||||
editor.isFocus = false
|
||||
} else {
|
||||
if (!editor.isFocus) {
|
||||
|
@ -103,6 +100,7 @@ function _blurHandler(editor: Editor) {
|
|||
const config = editor.config
|
||||
const onblur = config.onblur
|
||||
const currentHtml = editor.txt.html() || ''
|
||||
editor.txt.eventHooks.onBlurEvents.forEach(fn => fn())
|
||||
onblur(currentHtml)
|
||||
}
|
||||
|
||||
|
|
|
@ -35,9 +35,12 @@ function initSelection(editor: Editor, newLine?: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
editor.selection.createRangeByElem($last, true, true)
|
||||
editor.selection.createRangeByElem($last, false, true)
|
||||
if (editor.config.focus) {
|
||||
editor.selection.restoreSelection()
|
||||
} else {
|
||||
// 防止focus=false受其他因素影响
|
||||
editor.selection.clearWindowSelectionRange()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class SelectionRangeTopNodes {
|
|||
* @param $node 节点
|
||||
*/
|
||||
private getNextSibling($node: DomElement): DomElement {
|
||||
return $($node.elems[0].nextSibling)
|
||||
return $($node.elems[0].nextElementSibling)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -42,7 +42,7 @@ class SelectionAndRange {
|
|||
|
||||
// 获取选区范围的 DOM 元素
|
||||
const $containerElem = this.getSelectionContainerElem(range)
|
||||
if (!$containerElem) {
|
||||
if (!$containerElem?.length) {
|
||||
// 当 选区范围内没有 DOM元素 则抛出
|
||||
return
|
||||
}
|
||||
|
@ -223,6 +223,11 @@ class SelectionAndRange {
|
|||
if (toStart != null) {
|
||||
// 传入了 toStart 参数,折叠选区。如果没传入 toStart 参数,则忽略这一步
|
||||
range.collapse(toStart)
|
||||
|
||||
if (!toStart) {
|
||||
this.saveRange(range)
|
||||
this.editor.selection.moveCursor(elem)
|
||||
}
|
||||
}
|
||||
|
||||
// 存储 range
|
||||
|
@ -233,8 +238,8 @@ class SelectionAndRange {
|
|||
* 获取 当前 选取范围的 顶级(段落) 元素
|
||||
* @param $editor
|
||||
*/
|
||||
public getSelectionRangeTopNodes(editor: Editor): DomElement[] {
|
||||
const item = new SelectionRangeTopNodes(editor)
|
||||
public getSelectionRangeTopNodes(): DomElement[] {
|
||||
const item = new SelectionRangeTopNodes(this.editor)
|
||||
item.init()
|
||||
return item.getSelectionNodes()
|
||||
}
|
||||
|
@ -276,6 +281,16 @@ class SelectionAndRange {
|
|||
|
||||
return selection?.anchorOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前选区的Range,notice:不影响已保存的Range
|
||||
*/
|
||||
public clearWindowSelectionRange(): void {
|
||||
const selection = window.getSelection()
|
||||
if (selection) {
|
||||
selection.removeAllRanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionAndRange
|
||||
|
|
|
@ -43,13 +43,16 @@ class BackColor extends DropListMenu implements MenuActive {
|
|||
const editor = this.editor
|
||||
const isEmptySelection = editor.selection.isSelectionEmpty()
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
|
||||
|
||||
if ($selectionElem == null) return
|
||||
|
||||
const isSpan = $selectionElem?.nodeName.toLowerCase() !== 'p'
|
||||
const bgColor = $selectionElem?.style.backgroundColor
|
||||
const isSameColor = hexToRgb(value) === bgColor
|
||||
|
||||
if (isEmptySelection) {
|
||||
if (isSpan && !isSameColor) {
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
editor.selection.createRangeByElem($elems[0])
|
||||
editor.selection.moveCursor($elems[0].elems[0])
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import Editor from '../../editor/index'
|
|||
|
||||
function isActive(editor: Editor): boolean {
|
||||
const $selectionELem = editor.selection.getSelectionContainerElem()
|
||||
if (!$selectionELem) {
|
||||
if (!$selectionELem?.length) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
|
|
|
@ -43,12 +43,15 @@ class FontColor extends DropListMenu implements MenuActive {
|
|||
const editor = this.editor
|
||||
const isEmptySelection = editor.selection.isSelectionEmpty()
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
|
||||
|
||||
if ($selectionElem == null) return
|
||||
|
||||
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
|
||||
const isSameColor = $selectionElem?.getAttribute('color') === value
|
||||
|
||||
if (isEmptySelection) {
|
||||
if (isFont && !isSameColor) {
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
editor.selection.createRangeByElem($elems[0])
|
||||
editor.selection.moveCursor($elems[0].elems[0])
|
||||
}
|
||||
|
|
|
@ -40,12 +40,15 @@ class FontSize extends DropListMenu implements MenuActive {
|
|||
const isEmptySelection = editor.selection.isSelectionEmpty()
|
||||
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
|
||||
|
||||
if ($selectionElem == null) return
|
||||
|
||||
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
|
||||
const isSameSize = $selectionElem?.getAttribute('size') === value
|
||||
|
||||
if (isEmptySelection) {
|
||||
if (isFont && !isSameSize) {
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
editor.selection.createRangeByElem($elems[0])
|
||||
editor.selection.moveCursor($elems[0].elems[0])
|
||||
}
|
||||
|
|
|
@ -40,12 +40,15 @@ class FontStyle extends DropListMenu implements MenuActive {
|
|||
const isEmptySelection = editor.selection.isSelectionEmpty()
|
||||
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
|
||||
|
||||
if ($selectionElem == null) return
|
||||
|
||||
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
|
||||
const isSameValue = $selectionElem?.getAttribute('face') === value
|
||||
|
||||
if (isEmptySelection) {
|
||||
if (isFont && !isSameValue) {
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
editor.selection.createRangeByElem($elems[0])
|
||||
editor.selection.moveCursor($elems[0].elems[0])
|
||||
}
|
||||
|
|
|
@ -60,6 +60,15 @@ class Head extends DropListMenu implements MenuActive {
|
|||
return
|
||||
}
|
||||
|
||||
// 选中内容包含序列,code,表格,分割线时不处理
|
||||
if (
|
||||
['OL', 'UL', 'LI', 'TABLE', 'TH', 'TR', 'CODE', 'HR'].indexOf(
|
||||
$($selectionElem).getNodeName()
|
||||
) > -1
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.cmd.do('formatBlock', value)
|
||||
|
||||
// 标题设置成功且不是<p>正文标签就配置大纲id
|
||||
|
|
|
@ -8,7 +8,6 @@ import { PanelConf, PanelTabConf } from '../menu-constructors/Panel'
|
|||
import { getRandom } from '../../utils/util'
|
||||
import $ from '../../utils/dom-core'
|
||||
import UploadImg from './upload-img'
|
||||
import { imgRegex } from '../../utils/const'
|
||||
|
||||
export default function (editor: Editor): PanelConf {
|
||||
const config = editor.config
|
||||
|
@ -30,29 +29,11 @@ export default function (editor: Editor): PanelConf {
|
|||
* @param linkImg 网络图片链接
|
||||
*/
|
||||
function checkLinkImg(src: string): boolean {
|
||||
//编辑器进行正常校验,图片合规则使指针为true,不合规为false
|
||||
let flag = true
|
||||
if (!imgRegex.test(src)) {
|
||||
flag = false
|
||||
}
|
||||
|
||||
//查看开发者自定义配置的返回值
|
||||
const check = config.linkImgCheck(src)
|
||||
if (check === undefined) {
|
||||
//用户未能通过开发者的校验,且开发者不希望编辑器提示用户
|
||||
if (flag === false) console.log(t('您刚才插入的图片链接未通过编辑器校验', 'validate.'))
|
||||
} else if (check === true) {
|
||||
//用户通过了开发者的校验
|
||||
if (flag === false) {
|
||||
config.customAlert(
|
||||
`${t('您插入的网络图片无法识别', 'validate.')},${t(
|
||||
'请替换为支持的图片类型',
|
||||
'validate.'
|
||||
)}:jpg | png | gif ...`,
|
||||
'warning'
|
||||
)
|
||||
} else return true
|
||||
} else {
|
||||
if (check === true) {
|
||||
return true
|
||||
} else if (typeof check === 'string') {
|
||||
//用户未能通过开发者的校验,开发者希望我们提示这一字符串
|
||||
config.customAlert(check, 'error')
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class Indent extends DropListMenu implements MenuActive {
|
|||
if ($selectionElem && editor.$textElem.equal($selectionElem)) {
|
||||
// 当 当前选区 等于 textElem 时
|
||||
// 代表 当前选区 可能是一个选择了一个完整的段落或者多个段落
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
if ($elems.length > 0) {
|
||||
$elems.forEach((item: any) => {
|
||||
operateElement($(item), value, editor)
|
||||
|
|
|
@ -8,6 +8,9 @@ import $, { DomElement } from '../../utils/dom-core'
|
|||
import Editor from '../../editor/index'
|
||||
import { MenuActive } from '../menu-constructors/Menu'
|
||||
|
||||
const SPECIAL_NODE_LIST = ['LI']
|
||||
const SPECIAL_TOP_NODE_LIST = ['UL', 'BLOCKQUOTE']
|
||||
|
||||
class Justify extends DropListMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
const $elem = $(
|
||||
|
@ -26,7 +29,7 @@ class Justify extends DropListMenu implements MenuActive {
|
|||
${editor.i18next.t('menus.dropListMenu.justify.靠左')}
|
||||
</p>`
|
||||
),
|
||||
value: 'justifyLeft',
|
||||
value: 'left',
|
||||
},
|
||||
{
|
||||
$elem: $(
|
||||
|
@ -35,7 +38,7 @@ class Justify extends DropListMenu implements MenuActive {
|
|||
${editor.i18next.t('menus.dropListMenu.justify.居中')}
|
||||
</p>`
|
||||
),
|
||||
value: 'justifyCenter',
|
||||
value: 'center',
|
||||
},
|
||||
{
|
||||
$elem: $(
|
||||
|
@ -44,7 +47,7 @@ class Justify extends DropListMenu implements MenuActive {
|
|||
${editor.i18next.t('menus.dropListMenu.justify.靠右')}
|
||||
</p>`
|
||||
),
|
||||
value: 'justifyRight',
|
||||
value: 'right',
|
||||
},
|
||||
{
|
||||
$elem: $(
|
||||
|
@ -53,7 +56,7 @@ class Justify extends DropListMenu implements MenuActive {
|
|||
${editor.i18next.t('menus.dropListMenu.justify.两端')}
|
||||
</p>`
|
||||
),
|
||||
value: 'justifyFull',
|
||||
value: 'justify',
|
||||
},
|
||||
],
|
||||
clickHandler: (value: string) => {
|
||||
|
@ -71,46 +74,81 @@ class Justify extends DropListMenu implements MenuActive {
|
|||
const editor = this.editor
|
||||
const selection = editor.selection
|
||||
const $selectionElem = selection.getSelectionContainerElem()
|
||||
|
||||
// 保存选区
|
||||
selection.saveRange()
|
||||
// 定义对齐方式的type
|
||||
type justifyType = {
|
||||
[key: string]: string
|
||||
}
|
||||
// 数据项
|
||||
const justifyClass: justifyType = {
|
||||
justifyLeft: 'left',
|
||||
justifyCenter: 'center',
|
||||
justifyRight: 'right',
|
||||
justifyFull: 'justify',
|
||||
}
|
||||
|
||||
// 获取顶级元素
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
if ($selectionElem) {
|
||||
// 获取在css中对应style的值
|
||||
const justifyValue = justifyClass[value]
|
||||
$elems.forEach((el: DomElement) => {
|
||||
el.css('text-align', justifyValue)
|
||||
})
|
||||
const $elems = editor.selection.getSelectionRangeTopNodes()
|
||||
if ($selectionElem?.length) {
|
||||
// list 在chrome下默认多包裹一个 p,导致不能通过顶层元素判断,所以单独加个判断
|
||||
if (this.isSpecialNode($selectionElem, $elems[0]) || this.isSpecialTopNode($elems[0])) {
|
||||
const el = this.getSpecialNodeUntilTop($selectionElem, $elems[0])
|
||||
if (el == null) return
|
||||
|
||||
$(el).css('text-align', value)
|
||||
} else {
|
||||
$elems.forEach((el: DomElement) => {
|
||||
el.css('text-align', value)
|
||||
})
|
||||
}
|
||||
}
|
||||
//恢复选区
|
||||
selection.restoreSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选区中的特殊元素,如果不存在,则直接返回顶层元素子元素
|
||||
* @param el DomElement
|
||||
* @param topEl DomElement
|
||||
*/
|
||||
private getSpecialNodeUntilTop(el: DomElement, topEl: DomElement) {
|
||||
let parentNode: Node | null = el.elems[0]
|
||||
const topNode = topEl.elems[0]
|
||||
// 可能出现嵌套的情况,所以一级一级向上找,是否是特殊元素
|
||||
while (parentNode != null) {
|
||||
if (SPECIAL_NODE_LIST.indexOf(parentNode?.nodeName) !== -1) {
|
||||
return parentNode
|
||||
}
|
||||
// 如果再到 top 元素之前还没找到特殊元素,直接返回元素
|
||||
if (parentNode.parentNode === topNode) {
|
||||
return parentNode
|
||||
}
|
||||
parentNode = parentNode.parentNode
|
||||
}
|
||||
return parentNode
|
||||
}
|
||||
|
||||
/**
|
||||
* 当选区元素或者顶层元素是某些特殊元素时,只需要修改子元素的对齐样式的元素
|
||||
* @param el DomElement
|
||||
* @param topEl DomElement
|
||||
*/
|
||||
private isSpecialNode(el: DomElement, topEl: DomElement) {
|
||||
// 如果以后有类似的元素要这样处理,直接修改这个数组即可
|
||||
const parentNode = this.getSpecialNodeUntilTop(el, topEl)
|
||||
|
||||
if (parentNode == null) return false
|
||||
|
||||
return SPECIAL_NODE_LIST.indexOf(parentNode.nodeName) !== -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 当选区 top 元素为某些特殊元素时,只需要修改子元素的对齐样式的元素
|
||||
* @param el DomElement
|
||||
*/
|
||||
private isSpecialTopNode(topEl: DomElement) {
|
||||
if (topEl == null) return false
|
||||
|
||||
return SPECIAL_TOP_NODE_LIST.indexOf(topEl.elems[0]?.nodeName) !== -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试改变菜单激活(高亮)状态
|
||||
* 默认左对齐,若选择其他对其方式对active进行高亮否则unActive
|
||||
* ?考虑优化的话 是否可以对具体选中的进行高亮
|
||||
*/
|
||||
public tryChangeActive(): void {
|
||||
// const editor = this.editor
|
||||
// let isjustify = ['justifyCenter', 'justifyRight'].some(e => editor.cmd.queryCommandState(e))
|
||||
// if (isjustify) {
|
||||
// this.active()
|
||||
// } else {
|
||||
// this.unActive()
|
||||
// }
|
||||
}
|
||||
public tryChangeActive(): void { }
|
||||
}
|
||||
|
||||
export default Justify
|
||||
|
|
|
@ -47,6 +47,9 @@ class LineHeight extends DropListMenu implements MenuActive {
|
|||
//恢复焦点
|
||||
editor.selection.restoreSelection()
|
||||
const $selectionElem = $(editor.selection.getSelectionContainerElem())
|
||||
|
||||
if (!$selectionElem?.length) return
|
||||
|
||||
const $selectionAll = $(editor.selection.getSelectionContainerElem())
|
||||
// let dom:HTMLElement= $selectionElem.elems[0]
|
||||
let dom: HTMLElement = $(editor.selection.getSelectionStartElem()).elems[0]
|
||||
|
@ -275,6 +278,9 @@ class LineHeight extends DropListMenu implements MenuActive {
|
|||
return
|
||||
}
|
||||
let dom: DomElement | HTMLElement = $(editor.selection.getSelectionStartElem())
|
||||
// 有些情况下 dom 可能为空,比如编辑器初始化
|
||||
if (dom.length === 0) return
|
||||
|
||||
dom = this.getDom(dom.elems[0])
|
||||
let style: string | null = dom.getAttribute('style') ? dom.getAttribute('style') : ''
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import Editor from '../../editor/index'
|
|||
|
||||
function isActive(editor: Editor): boolean {
|
||||
const $selectionELem = editor.selection.getSelectionContainerElem()
|
||||
if (!$selectionELem) {
|
||||
if (!$selectionELem?.length) {
|
||||
return false
|
||||
}
|
||||
if ($selectionELem.getNodeName() === 'A') {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { ContainerFragment } from '.'
|
||||
import $, { DomElement } from '../../../utils/dom-core'
|
||||
import { Exec, HandlerListOptions, ListHandle } from './ListHandle'
|
||||
import {
|
||||
filterSelectionNodes,
|
||||
getEndPoint,
|
||||
insertBefore,
|
||||
createElement,
|
||||
createDocumentFragment,
|
||||
createElementFragment,
|
||||
} from '../utils'
|
||||
|
||||
export default class EndJoinListHandle extends ListHandle implements Exec {
|
||||
constructor(options: HandlerListOptions) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
exec(): void {
|
||||
const { editor, listType, listTarget, $endElem } = this.options
|
||||
|
||||
// 容器 - HTML 文档片段
|
||||
let $containerFragment: ContainerFragment
|
||||
|
||||
// 获取选中的段落
|
||||
const $nodes: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
|
||||
// 获取结束段落标签名
|
||||
const endNodeName = $endElem?.getNodeName()
|
||||
|
||||
// 弹出 结束序列
|
||||
$nodes.pop()
|
||||
|
||||
// 下序列元素数组
|
||||
const lowerListElems: DomElement[] = []
|
||||
// 获取结束元素
|
||||
let $endDom: DomElement = getEndPoint($endElem)
|
||||
// 获取下半序列中选中的内容
|
||||
while ($endDom.length) {
|
||||
lowerListElems.unshift($endDom)
|
||||
$endDom = $endDom.prev()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当前序列类型和结束序列的类型 一致
|
||||
// 代表当前是一个 融合(把其他段落加入到结束序列中) 的操作
|
||||
// =====================================
|
||||
if (endNodeName === listType) {
|
||||
// 生成 li 元属,并删除原来的 dom 元素
|
||||
$containerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 过滤元素节点数据
|
||||
createDocumentFragment() // 创建 文档片段
|
||||
)
|
||||
|
||||
lowerListElems.forEach($list => $containerFragment.append($list.elems[0]))
|
||||
|
||||
// 插入到结束序列之前
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
|
||||
if ($endElem.children()?.length) {
|
||||
const $endElemChild = $endElem.children() as DomElement
|
||||
insertBefore($endElemChild, $containerFragment, $endElemChild.elems[0])
|
||||
} else {
|
||||
$endElem.elems[0].append($containerFragment)
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当前序列类型和结束序列的类型 不一致
|
||||
// 代表当前是一个 设置序列 的操作
|
||||
// =====================================
|
||||
else {
|
||||
// 过滤元素节点数据
|
||||
const $selectionNodes = filterSelectionNodes($nodes)
|
||||
// 把下序列的内容添加到过滤元素中
|
||||
$selectionNodes.push(...lowerListElems)
|
||||
// 生成 li 元素并且添加到序列节点后删除原节点
|
||||
$containerFragment = createElementFragment(
|
||||
$selectionNodes,
|
||||
createElement(listTarget) // 创建 序列节点
|
||||
)
|
||||
|
||||
// 插入到结束序列之前
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
$($containerFragment).insertBefore($endElem)
|
||||
|
||||
// 序列全选被掏空了后,就卸磨杀驴吧
|
||||
!$endElem.children()?.length && $endElem.remove()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
import { ContainerFragment } from '.'
|
||||
import $, { DomElement } from '../../../utils/dom-core'
|
||||
import { Exec, HandlerListOptions, ListHandle } from './ListHandle'
|
||||
import {
|
||||
filterSelectionNodes,
|
||||
getStartPoint,
|
||||
getEndPoint,
|
||||
insertBefore,
|
||||
createElement,
|
||||
createDocumentFragment,
|
||||
createElementFragment,
|
||||
} from '../utils'
|
||||
|
||||
export default class JoinListHandle extends ListHandle implements Exec {
|
||||
constructor(options: HandlerListOptions) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
exec(): void {
|
||||
const { editor, listType, listTarget, $startElem, $endElem } = this.options
|
||||
|
||||
// 容器 - HTML 文档片段
|
||||
let $containerFragment: ContainerFragment
|
||||
|
||||
// 获取选中的段落
|
||||
const $nodes: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
|
||||
// 获取开始段落和结束段落 标签名
|
||||
const startNodeName = $startElem?.getNodeName()
|
||||
const endNodeName = $endElem?.getNodeName()
|
||||
|
||||
// =====================================
|
||||
// 开头结尾都是序列的情况下
|
||||
// 开头序列 和 结尾序列的标签名一致的时候
|
||||
// =====================================
|
||||
if (startNodeName === endNodeName) {
|
||||
// =====================================
|
||||
// 开头序列 和 结尾序列 中间还有其他的段落的时候
|
||||
// =====================================
|
||||
if ($nodes.length > 2) {
|
||||
// 弹出 开头 和 结尾
|
||||
$nodes.shift()
|
||||
$nodes.pop()
|
||||
|
||||
// 把中间部分的节点元素转换成 li 元素并添加到文档片段后删除
|
||||
$containerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 过滤 $nodes 获取到符合要求的选中元素节点
|
||||
createDocumentFragment() // 创建 文档片段
|
||||
)
|
||||
|
||||
// =====================================
|
||||
// 由于开头序列 和 结尾序列的标签名一样,所以只判断了开头序列的
|
||||
// 当开头序列的标签名和按钮类型 一致 的时候
|
||||
// 代表着当前是一个 设置序列 的操作
|
||||
// =====================================
|
||||
if (startNodeName === listType) {
|
||||
// 把结束序列的 li 元素添加到 文档片段中
|
||||
$endElem.children()?.forEach(($list: HTMLElement) => {
|
||||
$containerFragment.append($list)
|
||||
})
|
||||
|
||||
// 下序列全选被掏空了,就卸磨杀驴吧
|
||||
$endElem.remove()
|
||||
|
||||
// 在开始序列中添加 文档片段
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
$startElem.elems[0].append($containerFragment)
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 由于开头序列 和 结尾序列的标签名一样,所以只判断了开头序列的
|
||||
// 当开头序列的标签名和按钮类型 不一致 的时候
|
||||
// 代表着当前是一个 转换序列 的操作
|
||||
// =====================================
|
||||
else {
|
||||
// 创建 开始序列和结束序列的文档片段
|
||||
const $startFragment = document.createDocumentFragment()
|
||||
const $endFragment = document.createDocumentFragment()
|
||||
|
||||
// 获取起点元素
|
||||
let $startDom: DomElement = getStartPoint($startElem)
|
||||
// 获取上半序列中的选中内容,并添加到文档片段中
|
||||
while ($startDom.length) {
|
||||
const _element = $startDom.elems[0]
|
||||
$startDom = $startDom.next()
|
||||
$startFragment.append(_element)
|
||||
}
|
||||
|
||||
// 获取结束元素
|
||||
let $endDom: DomElement = getEndPoint($endElem)
|
||||
// 获取下半序列中选中的内容
|
||||
const domArr: Element[] = []
|
||||
while ($endDom.length) {
|
||||
domArr.unshift($endDom.elems[0])
|
||||
$endDom = $endDom.prev()
|
||||
}
|
||||
// 添加到文档片段中
|
||||
domArr.forEach(($node: Element) => {
|
||||
$endFragment.append($node)
|
||||
})
|
||||
|
||||
// 合并文档片段
|
||||
const $orderFragment = createElement(listTarget)
|
||||
$orderFragment.append($startFragment)
|
||||
$orderFragment.append($containerFragment)
|
||||
$orderFragment.append($endFragment)
|
||||
$containerFragment = $orderFragment
|
||||
|
||||
// 插入
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
$($orderFragment).insertAfter($startElem)
|
||||
|
||||
// 序列全选被掏空了后,就卸磨杀驴吧
|
||||
!$startElem.children()?.length && $startElem.remove()
|
||||
!$endElem.children()?.length && $endElem.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 开头序列 和 结尾序列 中间没有其他的段落
|
||||
// =====================================
|
||||
else {
|
||||
$nodes.length = 0
|
||||
|
||||
// 获取起点元素
|
||||
let $startDom: DomElement = getStartPoint($startElem)
|
||||
// 获取上半序列中的选中内容
|
||||
while ($startDom.length) {
|
||||
$nodes.push($startDom)
|
||||
$startDom = $startDom.next()
|
||||
}
|
||||
|
||||
// 获取结束元素
|
||||
let $endDom: DomElement = getEndPoint($endElem)
|
||||
// 获取下半序列中选中的内容
|
||||
const domArr: DomElement[] = []
|
||||
// 获取下半序列中的选中内容
|
||||
while ($endDom.length) {
|
||||
domArr.unshift($endDom)
|
||||
$endDom = $endDom.prev()
|
||||
}
|
||||
|
||||
// 融合内容
|
||||
$nodes.push(...domArr)
|
||||
|
||||
// =====================================
|
||||
// 由于开头序列 和 结尾序列的标签名一样,所以只判断了开头序列的
|
||||
// 当开头序列的标签名和按钮类型 一致 的时候
|
||||
// 代表着当前是一个 取消序列 的操作
|
||||
// =====================================
|
||||
if (startNodeName === listType) {
|
||||
// 创建 文档片段
|
||||
// 把 li 转换为 p 标签
|
||||
$containerFragment = createElementFragment(
|
||||
$nodes,
|
||||
createDocumentFragment(),
|
||||
'p'
|
||||
)
|
||||
|
||||
// 插入到 endElem 前
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
insertBefore($startElem, $containerFragment, $endElem.elems[0])
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 由于开头序列 和 结尾序列的标签名一样,所以只判断了开头序列的
|
||||
// 当开头序列的标签名和按钮类型 不一致 的时候
|
||||
// 代表着当前是一个 设置序列 的操作
|
||||
// =====================================
|
||||
else {
|
||||
// 创建 序列元素
|
||||
$containerFragment = createElement(listTarget)
|
||||
// li 元素添加到 序列元素 中
|
||||
$nodes.forEach(($list: DomElement) => {
|
||||
$containerFragment.append($list.elems[0])
|
||||
})
|
||||
// 插入到 startElem 之后
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
$($containerFragment).insertAfter($startElem)
|
||||
}
|
||||
|
||||
// 序列全选被掏空了后,就卸磨杀驴吧
|
||||
!$startElem.children()?.length && $endElem.remove()
|
||||
!$endElem.children()?.length && $endElem.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 由于开头序列 和 结尾序列的标签名不一样
|
||||
// =====================================
|
||||
else {
|
||||
// 下序列元素数组
|
||||
const lowerListElems: DomElement[] = []
|
||||
// 获取结束元素
|
||||
let $endDom: DomElement = getEndPoint($endElem)
|
||||
// 获取下半序列中选中的内容
|
||||
while ($endDom.length) {
|
||||
lowerListElems.unshift($endDom)
|
||||
$endDom = $endDom.prev()
|
||||
}
|
||||
|
||||
// 上序列元素数组
|
||||
const upperListElems: DomElement[] = []
|
||||
// 获取起点元素
|
||||
let $startDom: DomElement = getStartPoint($startElem)
|
||||
// 获取上半序列中的选中内容,并添加到文档片段中
|
||||
while ($startDom.length) {
|
||||
upperListElems.push($startDom)
|
||||
$startDom = $startDom.next()
|
||||
}
|
||||
|
||||
// 创建 文档片段
|
||||
$containerFragment = createDocumentFragment()
|
||||
|
||||
// 弹出开头和结尾的序列
|
||||
$nodes.shift()
|
||||
$nodes.pop()
|
||||
|
||||
// 把头部序列的内容添加到文档片段当中
|
||||
upperListElems.forEach($list => $containerFragment.append($list.elems[0]))
|
||||
|
||||
// 生成 li 标签,并且添加到 文档片段中,删除无用节点
|
||||
$containerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 序列中间的数据 - 进行数据过滤
|
||||
$containerFragment
|
||||
)
|
||||
|
||||
// 把尾部序列的内容添加到文档片段当中
|
||||
lowerListElems.forEach($list => $containerFragment.append($list.elems[0]))
|
||||
|
||||
// 记录
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
|
||||
// =====================================
|
||||
// 开头序列 和 设置序列类型相同
|
||||
// =====================================
|
||||
if (startNodeName === listType) {
|
||||
// 插入到 开始序列的尾部(作为子元素)
|
||||
$startElem.elems[0].append($containerFragment)
|
||||
|
||||
// 序列全选被掏空了后,就卸磨杀驴吧
|
||||
!$endElem.children()?.length && $endElem.remove()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 结尾序列 和 设置序列类型相同
|
||||
// =====================================
|
||||
else {
|
||||
// 插入到结束序列的顶部(作为子元素)
|
||||
if ($endElem.children()?.length) {
|
||||
const $endElemChild = $endElem.children() as DomElement
|
||||
insertBefore($endElemChild, $containerFragment, $endElemChild.elems[0])
|
||||
} else {
|
||||
$endElem.elems[0].append($containerFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import Editor from '../../../editor'
|
||||
import { DomElement } from '../../../utils/dom-core'
|
||||
import SelectionRangeElem from '../SelectionRangeElem'
|
||||
|
||||
export type HandlerListOptions = {
|
||||
editor: Editor
|
||||
listType: string
|
||||
listTarget: string
|
||||
$selectionElem: DomElement
|
||||
$startElem: DomElement
|
||||
$endElem: DomElement
|
||||
}
|
||||
|
||||
export interface Exec {
|
||||
exec: Function
|
||||
}
|
||||
|
||||
export class ListHandle {
|
||||
public options: HandlerListOptions
|
||||
public selectionRangeElem: SelectionRangeElem
|
||||
|
||||
constructor(options: HandlerListOptions) {
|
||||
this.options = options
|
||||
this.selectionRangeElem = new SelectionRangeElem()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { ContainerFragment } from '.'
|
||||
import { DomElement } from '../../../utils/dom-core'
|
||||
import { Exec, HandlerListOptions, ListHandle } from './ListHandle'
|
||||
import { filterSelectionNodes, createElement, createElementFragment } from '../utils'
|
||||
|
||||
export default class OtherListHandle extends ListHandle implements Exec {
|
||||
public range: Range
|
||||
|
||||
constructor(options: HandlerListOptions, range: Range) {
|
||||
super(options)
|
||||
this.range = range
|
||||
}
|
||||
|
||||
exec(): void {
|
||||
const { editor, listTarget } = this.options
|
||||
|
||||
// 获取选中的段落
|
||||
const $nodes: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
|
||||
// 生成 li 元素并且添加到序列节点后删除原节点
|
||||
const $containerFragment: ContainerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 过滤选取的元素
|
||||
createElement(listTarget) // 创建 序列节点
|
||||
)
|
||||
|
||||
// 插入节点到选区
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
this.range.insertNode($containerFragment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { ContainerFragment } from '.'
|
||||
import $, { DomElement } from '../../../utils/dom-core'
|
||||
import { Exec, HandlerListOptions, ListHandle } from './ListHandle'
|
||||
import {
|
||||
filterSelectionNodes,
|
||||
getStartPoint,
|
||||
createElement,
|
||||
createDocumentFragment,
|
||||
createElementFragment,
|
||||
} from '../utils'
|
||||
|
||||
export default class StartJoinListHandle extends ListHandle implements Exec {
|
||||
constructor(options: HandlerListOptions) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
exec(): void {
|
||||
const { editor, listType, listTarget, $startElem } = this.options
|
||||
|
||||
// 容器 - HTML 文档片段
|
||||
let $containerFragment: ContainerFragment
|
||||
|
||||
// 获取选中的段落
|
||||
const $nodes: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
|
||||
// 获取开始段落标签名
|
||||
const startNodeName = $startElem?.getNodeName()
|
||||
|
||||
// 弹出 开头序列
|
||||
$nodes.shift()
|
||||
|
||||
// 上序列元素数组
|
||||
const upperListElems: DomElement[] = []
|
||||
// 获取起点元素
|
||||
let $startDom: DomElement = getStartPoint($startElem)
|
||||
// 获取上半序列中的选中内容,并添加到文档片段中
|
||||
while ($startDom.length) {
|
||||
upperListElems.push($startDom)
|
||||
$startDom = $startDom.next()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当前序列类型和开头序列的类型 一致
|
||||
// 代表当前是一个 融合(把其他段落加入到开头序列中) 的操作
|
||||
// =====================================
|
||||
if (startNodeName === listType) {
|
||||
$containerFragment = createDocumentFragment()
|
||||
|
||||
upperListElems.forEach($list => $containerFragment.append($list.elems[0]))
|
||||
|
||||
// 生成 li 元属,并删除
|
||||
$containerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 过滤元素节点数据
|
||||
$containerFragment
|
||||
)
|
||||
|
||||
// 插入到开始序列末尾
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
// this.selectionRangeElem.set($startElem.elems[0])
|
||||
$startElem.elems[0].append($containerFragment)
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当前序列类型和开头序列的类型 不一致
|
||||
// 代表当前是一个 设置序列 的操作
|
||||
// =====================================
|
||||
else {
|
||||
// 创建 序列节点
|
||||
$containerFragment = createElement(listTarget)
|
||||
|
||||
upperListElems.forEach($list => $containerFragment.append($list.elems[0]))
|
||||
|
||||
// 生成 li 元素,并添加到 序列节点 当中,删除无用节点
|
||||
$containerFragment = createElementFragment(
|
||||
filterSelectionNodes($nodes), // 过滤普通节点
|
||||
$containerFragment
|
||||
)
|
||||
|
||||
// 插入到开始元素
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
$($containerFragment).insertAfter($startElem)
|
||||
|
||||
// 序列全选被掏空了后,就卸磨杀驴吧
|
||||
!$startElem.children()?.length && $startElem.remove()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import { ContainerFragment } from '.'
|
||||
import $, { DomElement } from '../../../utils/dom-core'
|
||||
import { Exec, HandlerListOptions, ListHandle } from './ListHandle'
|
||||
import {
|
||||
insertBefore,
|
||||
createElement,
|
||||
createDocumentFragment,
|
||||
createElementFragment,
|
||||
} from '../utils'
|
||||
|
||||
/**
|
||||
* 选区在序列内的处理
|
||||
*/
|
||||
export default class WrapListHandle extends ListHandle implements Exec {
|
||||
constructor(options: HandlerListOptions) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
exec(): void {
|
||||
const { listType, listTarget, $selectionElem, $startElem, $endElem } = this.options
|
||||
|
||||
let $containerFragment: ContainerFragment // 容器 - HTML 文档片段
|
||||
const $nodes: DomElement[] = [] // 获取选中的段落
|
||||
|
||||
// 获取 selectionElem 的标签名
|
||||
const containerNodeName = $selectionElem?.getNodeName()
|
||||
|
||||
// 获取开始以及结束的 li 元素
|
||||
const $start = $startElem.prior
|
||||
const $end = $endElem.prior
|
||||
|
||||
// =====================================
|
||||
// 当 开始节点 和 结束节点 没有 prior
|
||||
// 并且 开始节点 没有前一个兄弟节点
|
||||
// 并且 结束节点 没有后一个兄弟节点
|
||||
// 即代表 全选序列
|
||||
// =====================================
|
||||
if (
|
||||
(!$startElem.prior && !$endElem.prior) ||
|
||||
(!$start?.prev().length && !$end?.next().length)
|
||||
) {
|
||||
// 获取当前序列下的所有 li 标签
|
||||
; ($selectionElem?.children() as DomElement).forEach(($node: HTMLElement) => {
|
||||
$nodes.push($($node))
|
||||
})
|
||||
|
||||
// =====================================
|
||||
// 当 selectionElem 的标签名和按钮类型 一致 的时候
|
||||
// 代表着当前的操作是 取消 序列
|
||||
// =====================================
|
||||
if (containerNodeName === listType) {
|
||||
// 生成对应的段落(p)并添加到文档片段中,然后删除掉无用的 li
|
||||
$containerFragment = createElementFragment(
|
||||
$nodes,
|
||||
createDocumentFragment(), // 创建 文档片段
|
||||
'p'
|
||||
)
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当 selectionElem 的标签名和按钮类型 不一致 的时候
|
||||
// 代表着当前的操作是 转换 序列
|
||||
// =====================================
|
||||
else {
|
||||
// 创建 序列节点
|
||||
$containerFragment = createElement(listTarget)
|
||||
|
||||
// 因为是转换,所以 li 元素可以直接使用
|
||||
$nodes.forEach($node => {
|
||||
$containerFragment.append($node.elems[0])
|
||||
})
|
||||
}
|
||||
|
||||
// 把 文档片段 或 序列节点 插入到 selectionElem 的前面
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
|
||||
// 插入到 $selectionElem 之前
|
||||
insertBefore($selectionElem, $containerFragment, $selectionElem.elems[0])
|
||||
|
||||
// 删除无用的 selectionElem 因为它被掏空了
|
||||
$selectionElem.remove()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当不是全选序列的时候就代表是非全选序列(废话)
|
||||
// 非全选序列的情况
|
||||
// =====================================
|
||||
else {
|
||||
// 获取选中的内容
|
||||
let $startDom: DomElement = $start as DomElement
|
||||
while ($startDom.length) {
|
||||
$nodes.push($startDom)
|
||||
$end?.equal($startDom)
|
||||
? ($startDom = $(undefined)) // 结束
|
||||
: ($startDom = $startDom.next()) // 继续
|
||||
}
|
||||
|
||||
// 获取开始节点的上一个兄弟节点
|
||||
const $prveDom: DomElement = ($start as DomElement).prev()
|
||||
// 获取结束节点的下一个兄弟节点
|
||||
let $nextDom: DomElement = ($end as DomElement).next()
|
||||
|
||||
// =====================================
|
||||
// 当 selectionElem 的标签名和按钮类型一致的时候
|
||||
// 代表着当前的操作是 取消 序列
|
||||
// =====================================
|
||||
if (containerNodeName === listType) {
|
||||
// 生成对应的段落(p)并添加到文档片段中,然后删除掉无用的 li
|
||||
$containerFragment = createElementFragment(
|
||||
$nodes,
|
||||
createDocumentFragment(), // 创建 文档片段
|
||||
'p'
|
||||
)
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当 selectionElem 的标签名和按钮类型不一致的时候
|
||||
// 代表着当前的操作是 转换 序列
|
||||
// =====================================
|
||||
else {
|
||||
// 创建 文档片段
|
||||
$containerFragment = createElement(listTarget)
|
||||
|
||||
// 因为是转换,所以 li 元素可以直接使用
|
||||
$nodes.forEach(($node: DomElement) => {
|
||||
$containerFragment.append($node.elems[0])
|
||||
})
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当 prveDom 和 nextDom 都存在的时候
|
||||
// 代表着当前选区是在序列的中间
|
||||
// 所以要先把 下半部分 未选择的 li 元素独立出来生成一个 序列
|
||||
// =====================================
|
||||
if ($prveDom.length && $nextDom.length) {
|
||||
// 获取尾部的元素
|
||||
const $tailDomArr: DomElement[] = []
|
||||
while ($nextDom.length) {
|
||||
$tailDomArr.push($nextDom)
|
||||
$nextDom = $nextDom.next()
|
||||
}
|
||||
|
||||
// 创建 尾部序列节点
|
||||
const $tailDocFragment = createElement(containerNodeName)
|
||||
|
||||
// 把尾部元素节点添加到尾部序列节点中
|
||||
$tailDomArr.forEach(($node: DomElement) => {
|
||||
$tailDocFragment.append($node.elems[0])
|
||||
})
|
||||
|
||||
// 把尾部序列节点插入到 selectionElem 的后面
|
||||
$($tailDocFragment).insertAfter($selectionElem)
|
||||
|
||||
// =====================================
|
||||
// 获取选区容器元素的父元素,一般就是编辑区域
|
||||
// 然后判断 selectionElem 是否还有下一个兄弟节点
|
||||
// 如果有,就把文档片段添加到 selectionElem 下一个兄弟节点前
|
||||
// 如果没有,就把文档片段添加到 编辑区域 末尾
|
||||
// =====================================
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
const $selectionNextDom: DomElement = $selectionElem.next()
|
||||
$selectionNextDom.length
|
||||
? insertBefore($selectionElem, $containerFragment, $selectionNextDom.elems[0])
|
||||
: $selectionElem.parent().elems[0].append($containerFragment)
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 不管是 取消 还是 转换 都需要重新插入节点
|
||||
//
|
||||
// prveDom.length 等于 0 即代表选区是 selectionElem 序列的上半部分
|
||||
// 上半部分的 li 元素
|
||||
// =====================================
|
||||
else if (!$prveDom.length) {
|
||||
// 文档片段插入到 selectionElem 之前
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
insertBefore($selectionElem, $containerFragment, $selectionElem.elems[0])
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 不管是 取消 还是 转换 都需要重新插入节点
|
||||
//
|
||||
// nextDom.length 等于 0 即代表选区是 selectionElem 序列的下半部分
|
||||
// 下半部分的 li 元素 if (!$nextDom.length)
|
||||
// =====================================
|
||||
else {
|
||||
// 文档片段插入到 selectionElem 之后
|
||||
this.selectionRangeElem.set($containerFragment)
|
||||
const $selectionNextDom: DomElement = $selectionElem.next()
|
||||
$selectionNextDom.length
|
||||
? insertBefore($selectionElem, $containerFragment, $selectionNextDom.elems[0])
|
||||
: $selectionElem.parent().elems[0].append($containerFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import $, { DomElement } from '../../../utils/dom-core'
|
||||
import WrapListHandle from './WrapListHandle'
|
||||
import JoinListHandle from './JoinListHandle'
|
||||
import StartJoinListHandle from './StartJoinListHandle'
|
||||
import EndJoinListHandle from './EndJoinListHandle'
|
||||
import OtherListHandle from './OtherListHandle'
|
||||
|
||||
import { HandlerListOptions } from './ListHandle'
|
||||
|
||||
// 片段类型
|
||||
export type ContainerFragment = HTMLElement | DocumentFragment
|
||||
|
||||
// 处理类
|
||||
export type ListHandleClass =
|
||||
| WrapListHandle
|
||||
| JoinListHandle
|
||||
| StartJoinListHandle
|
||||
| EndJoinListHandle
|
||||
| OtherListHandle
|
||||
|
||||
export enum ClassType {
|
||||
Wrap = 'WrapListHandle',
|
||||
Join = 'JoinListHandle',
|
||||
StartJoin = 'StartJoinListHandle',
|
||||
EndJoin = 'EndJoinListHandle',
|
||||
Other = 'OtherListHandle',
|
||||
}
|
||||
|
||||
const handle = {
|
||||
WrapListHandle,
|
||||
JoinListHandle,
|
||||
StartJoinListHandle,
|
||||
EndJoinListHandle,
|
||||
OtherListHandle,
|
||||
}
|
||||
|
||||
export function createListHandle(
|
||||
classType: ClassType,
|
||||
options: HandlerListOptions,
|
||||
range?: Range
|
||||
): ListHandleClass {
|
||||
if (classType === ClassType.Other && range === undefined) {
|
||||
throw new Error('other 类需要传入 range')
|
||||
}
|
||||
|
||||
return classType !== ClassType.Other
|
||||
? new handle[classType](options)
|
||||
: new handle[classType](options, range as Range)
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一执行的接口
|
||||
*/
|
||||
export default class ListHandleCommand {
|
||||
private handle: ListHandleClass
|
||||
|
||||
constructor(handle: ListHandleClass) {
|
||||
this.handle = handle
|
||||
this.handle.exec()
|
||||
}
|
||||
|
||||
getSelectionRangeElem(): DomElement {
|
||||
return $(this.handle.selectionRangeElem.get())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
type SelectionRangeType = HTMLElement | ChildNode[]
|
||||
type SetSelectionRangeType = SelectionRangeType | DocumentFragment
|
||||
export type SelectionRangeElemType = SelectionRangeType | null
|
||||
|
||||
/**
|
||||
* @description 选区的 Element
|
||||
* @author tonghan
|
||||
*/
|
||||
class SelectionRangeElem {
|
||||
private _element: SelectionRangeElemType
|
||||
|
||||
constructor() {
|
||||
this._element = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SelectionRangeElem 的值
|
||||
* @param { SetSelectionRangeType } data
|
||||
*/
|
||||
public set(data: SetSelectionRangeType) {
|
||||
//
|
||||
if (data instanceof DocumentFragment) {
|
||||
const childNode: ChildNode[] = []
|
||||
data.childNodes.forEach(($node: ChildNode) => {
|
||||
childNode.push($node)
|
||||
})
|
||||
data = childNode
|
||||
}
|
||||
this._element = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SelectionRangeElem 的值
|
||||
* @returns { SelectionRangeType } Elem
|
||||
*/
|
||||
public get(): SelectionRangeElemType {
|
||||
return this._element
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 SelectionRangeElem 的值
|
||||
*/
|
||||
public clear() {
|
||||
this._element = null
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionRangeElem
|
|
@ -3,11 +3,27 @@
|
|||
* @author tonghan
|
||||
*/
|
||||
|
||||
import $ from '../../utils/dom-core'
|
||||
import $, { DomElement } from '../../utils/dom-core'
|
||||
import Editor from '../../editor/index'
|
||||
import DropListMenu from '../menu-constructors/DropListMenu'
|
||||
import { MenuActive } from '../menu-constructors/Menu'
|
||||
|
||||
import { updateRange } from './utils'
|
||||
|
||||
import { HandlerListOptions } from './ListHandle/ListHandle'
|
||||
import ListHandleCommand, { createListHandle, ClassType } from './ListHandle'
|
||||
|
||||
/**
|
||||
* 列表的种类
|
||||
*/
|
||||
export enum ListType {
|
||||
OrderedList = 'OL',
|
||||
UnorderedList = 'UL',
|
||||
}
|
||||
|
||||
// 序列类型
|
||||
type ListTypeValue = ListType
|
||||
|
||||
class List extends DropListMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
const $elem = $(
|
||||
|
@ -27,7 +43,7 @@ class List extends DropListMenu implements MenuActive {
|
|||
<i class="w-e-icon-list2 w-e-drop-list-item"></i>
|
||||
${editor.i18next.t('menus.dropListMenu.list.无序列表')}
|
||||
<p>`),
|
||||
value: 'insertUnorderedList',
|
||||
value: ListType.UnorderedList,
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -37,66 +53,145 @@ class List extends DropListMenu implements MenuActive {
|
|||
${editor.i18next.t('menus.dropListMenu.list.有序列表')}
|
||||
<p>`
|
||||
),
|
||||
value: 'insertOrderedList',
|
||||
value: ListType.OrderedList,
|
||||
},
|
||||
],
|
||||
clickHandler: (value: string) => {
|
||||
// 注意 this 是指向当前的 List 对象
|
||||
this.command(value)
|
||||
this.command(value as ListTypeValue)
|
||||
},
|
||||
}
|
||||
|
||||
super($elem, editor, dropListConf)
|
||||
}
|
||||
|
||||
public command(value: string): void {
|
||||
public command(type: ListTypeValue): void {
|
||||
const editor = this.editor
|
||||
const $textElem = editor.$textElem
|
||||
editor.selection.restoreSelection()
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()
|
||||
|
||||
// 判断是否已经执行了命令
|
||||
if (editor.cmd.queryCommandState(value)) {
|
||||
return
|
||||
}
|
||||
// 选区范围的 DOM 元素不存在,不执行命令
|
||||
if ($selectionElem === undefined) return
|
||||
|
||||
//禁止在table中添加列表
|
||||
let $selectionElem = $(editor.selection.getSelectionContainerElem())
|
||||
let $dom = $($selectionElem.elems[0]).parentUntil('TABLE', $selectionElem.elems[0])
|
||||
if ($dom && $($dom.elems[0]).getNodeName() === 'TABLE') {
|
||||
return
|
||||
}
|
||||
// 获取选区范围内的顶级 DOM 元素
|
||||
this.handleSelectionRangeNodes(type)
|
||||
|
||||
editor.cmd.do(value)
|
||||
|
||||
// 验证列表是否被包裹在 <p> 之内
|
||||
if ($selectionElem.getNodeName() === 'LI') {
|
||||
$selectionElem = $selectionElem.parent()
|
||||
}
|
||||
|
||||
if (/^ol|ul$/i.test($selectionElem.getNodeName()) === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($selectionElem.equal($textElem)) {
|
||||
// 证明是顶级标签,没有被 <p> 包裹
|
||||
return
|
||||
}
|
||||
|
||||
const $parent = $selectionElem.parent()
|
||||
if ($parent.equal($textElem)) {
|
||||
// $parent 是顶级标签,不能删除
|
||||
return
|
||||
}
|
||||
|
||||
$selectionElem.insertAfter($parent)
|
||||
$parent.remove()
|
||||
|
||||
// 恢复选区
|
||||
editor.selection.restoreSelection()
|
||||
// 是否激活
|
||||
this.tryChangeActive()
|
||||
}
|
||||
|
||||
public tryChangeActive(): void {}
|
||||
public validator($startElem: DomElement, $endElem: DomElement, $textElem: DomElement): boolean {
|
||||
if (
|
||||
!$startElem.length ||
|
||||
!$endElem.length ||
|
||||
$textElem.equal($startElem) ||
|
||||
$textElem.equal($endElem)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private handleSelectionRangeNodes(listType: ListTypeValue): void {
|
||||
const editor = this.editor
|
||||
const selection = editor.selection
|
||||
|
||||
// 获取 序列标签
|
||||
const listTarget = listType.toLowerCase()
|
||||
|
||||
// 获取相对应的 元属节点
|
||||
let $selectionElem = selection.getSelectionContainerElem() as DomElement
|
||||
const $startElem = (selection.getSelectionStartElem() as DomElement).getNodeTop(editor)
|
||||
const $endElem = (selection.getSelectionEndElem() as DomElement).getNodeTop(editor)
|
||||
|
||||
// 验证是否执行 处理逻辑
|
||||
if (!this.validator($startElem, $endElem, editor.$textElem)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取选区
|
||||
const _range = selection.getRange()
|
||||
const _collapsed = _range?.collapsed
|
||||
|
||||
// 防止光标的时候判断异常
|
||||
if (!editor.$textElem.equal($selectionElem)) {
|
||||
$selectionElem = $selectionElem.getNodeTop(editor)
|
||||
}
|
||||
|
||||
const options: HandlerListOptions = {
|
||||
editor,
|
||||
listType,
|
||||
listTarget,
|
||||
$selectionElem,
|
||||
$startElem,
|
||||
$endElem,
|
||||
}
|
||||
|
||||
let classType: ClassType
|
||||
|
||||
// =====================================
|
||||
// 当 selectionElem 属于序列元素的时候
|
||||
// 代表着当前选区一定是在一个序列元素内的
|
||||
// =====================================
|
||||
if (this.isOrderElem($selectionElem as DomElement)) {
|
||||
classType = ClassType.Wrap
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当 startElem 和 endElem 属于序列元素的时候
|
||||
// 代表着当前选区一定是在再两个序列的中间(包括两个序列)
|
||||
// =====================================
|
||||
else if (
|
||||
this.isOrderElem($startElem as DomElement) &&
|
||||
this.isOrderElem($endElem as DomElement)
|
||||
) {
|
||||
classType = ClassType.Join
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 选区开始元素为 序列 的时候
|
||||
// =====================================
|
||||
else if (this.isOrderElem($startElem as DomElement)) {
|
||||
classType = ClassType.StartJoin
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 选区结束元素为 序列 的时候
|
||||
// =====================================
|
||||
else if (this.isOrderElem($endElem as DomElement)) {
|
||||
classType = ClassType.EndJoin
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// 当选区不是序列内且开头和结尾不是序列的时候
|
||||
// 直接获取所有顶级段落然后过滤
|
||||
// 代表着 设置序列 的操作
|
||||
// =====================================
|
||||
else {
|
||||
classType = ClassType.Other
|
||||
}
|
||||
|
||||
const listHandleCmd = new ListHandleCommand(
|
||||
createListHandle(classType, options, _range as Range)
|
||||
)
|
||||
|
||||
// 更新选区
|
||||
updateRange(editor, listHandleCmd.getSelectionRangeElem(), !!_collapsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是序列元素节点 UL and OL
|
||||
* @param $node
|
||||
*/
|
||||
private isOrderElem($node: DomElement) {
|
||||
const nodeName = $node.getNodeName()
|
||||
if (nodeName === ListType.OrderedList || nodeName === ListType.UnorderedList) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public tryChangeActive(): void { }
|
||||
}
|
||||
|
||||
export default List
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
import { ListType } from '.'
|
||||
import Editor from '../../editor/index'
|
||||
import $, { DomElement } from '../../utils/dom-core'
|
||||
import { ContainerFragment } from './ListHandle'
|
||||
|
||||
/**
|
||||
* 过滤 选择的 node 节点
|
||||
* @returns { DomElement[] } DomElement[]
|
||||
*/
|
||||
export function filterSelectionNodes($nodes: DomElement[]): DomElement[] {
|
||||
const $listHtml: DomElement[] = []
|
||||
$nodes.forEach(($node: DomElement) => {
|
||||
const targerName = $node.getNodeName()
|
||||
if (targerName !== ListType.OrderedList && targerName !== ListType.UnorderedList) {
|
||||
// 非序列
|
||||
$listHtml.push($node)
|
||||
} else {
|
||||
// 序列
|
||||
if ($node.prior) {
|
||||
$listHtml.push($node.prior)
|
||||
} else {
|
||||
const $children = $node.children()
|
||||
$children?.forEach(($li: HTMLElement) => {
|
||||
$listHtml.push($($li))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return $listHtml
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选区
|
||||
* @param $node
|
||||
*/
|
||||
|
||||
export function updateRange(editor: Editor, $node: DomElement, collapsed: boolean) {
|
||||
const selection = editor.selection
|
||||
const range = document.createRange()
|
||||
|
||||
// ===============================
|
||||
// length 大于 1
|
||||
// 代表着转换是一个文档节点多段落
|
||||
// ===============================
|
||||
if ($node.length > 1) {
|
||||
range.setStart($node.elems[0], 0)
|
||||
range.setEnd($node.elems[$node.length - 1], $node.elems[$node.length - 1].childNodes.length)
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 序列节点 或 单段落
|
||||
// ===============================
|
||||
else {
|
||||
range.selectNodeContents($node.elems[0])
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// collapsed 为 true 代表是光标
|
||||
// ===============================
|
||||
collapsed && range.collapse(false)
|
||||
selection.saveRange(range)
|
||||
selection.restoreSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取起点元素
|
||||
* @param $startElem 开始序列节点
|
||||
*/
|
||||
export function getStartPoint($startElem: DomElement): DomElement {
|
||||
return $startElem.prior
|
||||
? $startElem.prior // 有 prior 代表不是全选序列
|
||||
: $($startElem.children()?.elems[0]) // 没有则代表全选序列
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结束元素
|
||||
* @param $endElem 结束序列节点
|
||||
*/
|
||||
export function getEndPoint($endElem: DomElement): DomElement {
|
||||
return $endElem.prior
|
||||
? $endElem.prior // 有 prior 代表不是全选序列
|
||||
: $($endElem.children()?.last().elems[0]) // 没有则代表全选序列
|
||||
}
|
||||
|
||||
/**
|
||||
* 在您指定节点的已有子节点之前插入新的子节点。
|
||||
* @param { DomElement } $node 指定节点
|
||||
* @param { ContainerFragment } newNode 插入的新子节点
|
||||
* @param { Node | null } existingNode 指定子节点
|
||||
*/
|
||||
export function insertBefore(
|
||||
$node: DomElement,
|
||||
newNode: ContainerFragment,
|
||||
existingNode: Node | null = null
|
||||
) {
|
||||
$node.parent().elems[0].insertBefore(newNode, existingNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定的 element 对象
|
||||
*/
|
||||
export function createElement(target: string): HTMLElement {
|
||||
return document.createElement(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文档片段
|
||||
*/
|
||||
export function createDocumentFragment(): DocumentFragment {
|
||||
return document.createDocumentFragment()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 li 标签的元素,并返回 $fragment 文档片段
|
||||
* @param { DomElement[] } $nodes 需要转换成 li 的 dom 元素数组
|
||||
* @param { ContainerFragment } $fragment 用于存储生成后 li 元素的文档片段
|
||||
*/
|
||||
export function createElementFragment(
|
||||
$nodes: DomElement[],
|
||||
$fragment: ContainerFragment,
|
||||
tag: string = 'li'
|
||||
): ContainerFragment {
|
||||
$nodes.forEach(($node: DomElement) => {
|
||||
const $list = createElement(tag)
|
||||
$list.innerHTML = $node.html()
|
||||
$fragment.append($list)
|
||||
$node.remove()
|
||||
})
|
||||
return $fragment
|
||||
}
|
|
@ -159,9 +159,9 @@ class Panel {
|
|||
const type = event.type
|
||||
const fn = event.fn || EMPTY_FN
|
||||
const $content = tabContentArr[index]
|
||||
$content.find(selector).on(type, (e: Event) => {
|
||||
$content.find(selector).on(type, async (e: Event) => {
|
||||
e.stopPropagation()
|
||||
const needToHide = fn(e)
|
||||
const needToHide = await fn(e)
|
||||
// 执行完事件之后,是否要关闭 panel
|
||||
if (needToHide) {
|
||||
this.remove()
|
||||
|
|
|
@ -31,7 +31,6 @@ class Tooltip {
|
|||
this.conf = conf
|
||||
this._show = false
|
||||
this._isInsertTextContainer = false
|
||||
|
||||
// 定义 container
|
||||
const $container = $('<div></div>')
|
||||
$container.addClass('w-e-tooltip')
|
||||
|
@ -167,6 +166,9 @@ class Tooltip {
|
|||
}
|
||||
|
||||
this._show = true
|
||||
|
||||
editor.beforeDestroy(this.remove.bind(this))
|
||||
editor.txt.eventHooks.onBlurEvents.push(this.remove.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import $, { DomElement } from '../../../utils/dom-core'
|
|||
function bindEvent(editor: Editor) {
|
||||
function quoteEnter(e: Event) {
|
||||
const $selectElem = editor.selection.getSelectionContainerElem() as DomElement
|
||||
const $topSelectElem = editor.selection.getSelectionRangeTopNodes(editor)[0]
|
||||
const $topSelectElem = editor.selection.getSelectionRangeTopNodes()[0]
|
||||
// 对quote的enter进行特殊处理
|
||||
//最后一行为<p><br></p>时再按会出跳出blockquote
|
||||
if ($topSelectElem?.getNodeName() === 'BLOCKQUOTE') {
|
||||
|
|
|
@ -27,7 +27,7 @@ class Quote extends BtnMenu implements MenuActive {
|
|||
public clickHandler(): void {
|
||||
const editor = this.editor
|
||||
const isSelectEmpty = editor.selection.isSelectionEmpty()
|
||||
const topNodeElem: DomElement[] = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const topNodeElem: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
const $topNodeElem: DomElement = topNodeElem[topNodeElem.length - 1]
|
||||
const nodeName = this.getTopNodeName()
|
||||
// IE 中不支持 formatBlock <BLOCKQUOTE> ,要用其他方式兼容
|
||||
|
@ -52,6 +52,9 @@ class Quote extends BtnMenu implements MenuActive {
|
|||
$quote.insertAfter($topNodeElem)
|
||||
this.delSelectNode(topNodeElem)
|
||||
const moveNode = $quote.childNodes()?.last().getNode() as Node
|
||||
|
||||
if (moveNode == null) return
|
||||
|
||||
// 兼容firefox(firefox下空行情况下选区会在br后,造成自动换行的问题)
|
||||
moveNode.textContent
|
||||
? editor.selection.moveCursor(moveNode)
|
||||
|
@ -75,7 +78,7 @@ class Quote extends BtnMenu implements MenuActive {
|
|||
*/
|
||||
public tryChangeActive(): void {
|
||||
const editor = this.editor
|
||||
const cmdValue = editor.selection.getSelectionRangeTopNodes(editor)[0]?.getNodeName()
|
||||
const cmdValue = editor.selection.getSelectionRangeTopNodes()[0]?.getNodeName()
|
||||
if (cmdValue === 'BLOCKQUOTE') {
|
||||
this.active()
|
||||
} else {
|
||||
|
@ -90,7 +93,7 @@ class Quote extends BtnMenu implements MenuActive {
|
|||
*/
|
||||
private getTopNodeName(): string {
|
||||
const editor = this.editor
|
||||
const $topNodeElem = editor.selection.getSelectionRangeTopNodes(editor)[0]
|
||||
const $topNodeElem = editor.selection.getSelectionRangeTopNodes()[0]
|
||||
const nodeName = $topNodeElem?.getNodeName()
|
||||
|
||||
return nodeName
|
||||
|
|
|
@ -11,7 +11,7 @@ import { MenuActive } from '../menu-constructors/Menu'
|
|||
class Redo extends BtnMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
const $elem = $(
|
||||
`<div class="w-e-menu" data-title="撤销">
|
||||
`<div class="w-e-menu" data-title="恢复">
|
||||
<i class="w-e-icon-redo"></i>
|
||||
</div>`
|
||||
)
|
||||
|
@ -22,7 +22,17 @@ class Redo extends BtnMenu implements MenuActive {
|
|||
* 点击事件
|
||||
*/
|
||||
public clickHandler(): void {
|
||||
this.editor.history.restore()
|
||||
const editor = this.editor
|
||||
editor.history.restore()
|
||||
|
||||
// 重新创建 range,是处理当初始化编辑器,API插入内容后撤销,range 不在编辑器内部的问题
|
||||
const children = editor.$textElem.children()
|
||||
|
||||
if (!children?.length) return
|
||||
|
||||
const $last = children.last()
|
||||
editor.selection.createRangeByElem($last, false, true)
|
||||
editor.selection.restoreSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ class splitLine extends BtnMenu implements MenuActive {
|
|||
const range = editor.selection.getRange()
|
||||
|
||||
const $selectionElem = editor.selection.getSelectionContainerElem()
|
||||
if (!$selectionElem) return
|
||||
if (!$selectionElem?.length) return
|
||||
|
||||
const $DomElement = $($selectionElem.elems[0])
|
||||
const $tableDOM = $DomElement.parentUntil('TABLE', $selectionElem.elems[0])
|
||||
|
|
|
@ -18,7 +18,7 @@ function bindEvent(editor: Editor) {
|
|||
if (isAllTodo(editor)) {
|
||||
e.preventDefault()
|
||||
const selection = editor.selection
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes(editor)[0]
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes()[0]
|
||||
const $li = $topSelectElem.childNodes()?.get(0)
|
||||
const selectionNode = window.getSelection()?.anchorNode as Node
|
||||
const range = selection.getRange()
|
||||
|
@ -95,7 +95,7 @@ function bindEvent(editor: Editor) {
|
|||
function delDown(e: Event) {
|
||||
if (isAllTodo(editor)) {
|
||||
const selection = editor.selection
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes(editor)[0]
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes()[0]
|
||||
const $li = $topSelectElem.childNodes()?.getNode()
|
||||
const $p = $(`<p></p>`)
|
||||
const p = $p.getNode()
|
||||
|
@ -136,7 +136,7 @@ function bindEvent(editor: Editor) {
|
|||
*/
|
||||
function deleteUp() {
|
||||
const selection = editor.selection
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes(editor)[0]
|
||||
const $topSelectElem = selection.getSelectionRangeTopNodes()[0]
|
||||
if (isTodo($topSelectElem)) {
|
||||
if ($topSelectElem.text() === '') {
|
||||
$(`<p><br></p>`).insertAfter($topSelectElem)
|
||||
|
|
|
@ -9,7 +9,7 @@ import createTodo from './todo'
|
|||
class Todo extends BtnMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
const $elem = $(
|
||||
`<div class="w-e-menu" data-title="代办事项">
|
||||
`<div class="w-e-menu" data-title="待办事项">
|
||||
<i class="w-e-icon-checkbox-checked"></i>
|
||||
</div>`
|
||||
)
|
||||
|
@ -44,7 +44,7 @@ class Todo extends BtnMenu implements MenuActive {
|
|||
*/
|
||||
private setTodo() {
|
||||
const editor = this.editor
|
||||
const topNodeElem: DomElement[] = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const topNodeElem: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
topNodeElem.forEach($node => {
|
||||
const nodeName = $node?.getNodeName()
|
||||
if (nodeName === 'P') {
|
||||
|
@ -64,7 +64,7 @@ class Todo extends BtnMenu implements MenuActive {
|
|||
*/
|
||||
private cancelTodo() {
|
||||
const editor = this.editor
|
||||
const $topNodeElems: DomElement[] = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $topNodeElems: DomElement[] = editor.selection.getSelectionRangeTopNodes()
|
||||
|
||||
$topNodeElems.forEach($topNodeElem => {
|
||||
let content = $topNodeElem.childNodes()?.childNodes()?.clone(true) as DomElement
|
||||
|
|
|
@ -13,7 +13,7 @@ function isTodo($topSelectElem: DomElement) {
|
|||
* @param editor 编辑器对象
|
||||
*/
|
||||
function isAllTodo(editor: Editor): boolean | undefined {
|
||||
const $topSelectElems = editor.selection.getSelectionRangeTopNodes(editor)
|
||||
const $topSelectElems = editor.selection.getSelectionRangeTopNodes()
|
||||
// 排除为[]的情况
|
||||
if ($topSelectElems.length === 0) return
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { MenuActive } from '../menu-constructors/Menu'
|
|||
class Undo extends BtnMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
const $elem = $(
|
||||
`<div class="w-e-menu" data-title="恢复">
|
||||
`<div class="w-e-menu" data-title="撤销">
|
||||
<i class="w-e-icon-undo"></i>
|
||||
</div>`
|
||||
)
|
||||
|
@ -22,7 +22,17 @@ class Undo extends BtnMenu implements MenuActive {
|
|||
* 点击事件
|
||||
*/
|
||||
public clickHandler(): void {
|
||||
this.editor.history.revoke()
|
||||
const editor = this.editor
|
||||
editor.history.revoke()
|
||||
|
||||
// 重新创建 range,是处理当初始化编辑器,API插入内容后撤销,range 不在编辑器内部的问题
|
||||
const children = editor.$textElem.children()
|
||||
|
||||
if (!children?.length) return
|
||||
|
||||
const $last = children.last()
|
||||
editor.selection.createRangeByElem($last, false, true)
|
||||
editor.selection.restoreSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,19 +4,20 @@
|
|||
*/
|
||||
|
||||
import Editor from '../../editor/index'
|
||||
import { PanelConf } from '../menu-constructors/Panel'
|
||||
import { PanelConf, PanelTabConf } from '../menu-constructors/Panel'
|
||||
import { getRandom } from '../../utils/util'
|
||||
import $ from '../../utils/dom-core'
|
||||
import { videoRegex } from '../../utils/const'
|
||||
import UploadVideo from './upload-video'
|
||||
|
||||
export default function (editor: Editor, video: string): PanelConf {
|
||||
const config = editor.config
|
||||
const uploadVideo = new UploadVideo(editor)
|
||||
|
||||
// panel 中需要用到的id
|
||||
const inputIFrameId = getRandom('input-iframe')
|
||||
const btnOkId = getRandom('btn-ok')
|
||||
const i18nPrefix = 'menus.panelMenus.video.'
|
||||
const t = (text: string, prefix: string = i18nPrefix): string => {
|
||||
return editor.i18next.t(prefix + text)
|
||||
}
|
||||
const inputUploadId = getRandom('input-upload')
|
||||
const btnStartId = getRandom('btn-local-ok')
|
||||
|
||||
/**
|
||||
* 插入链接
|
||||
|
@ -34,84 +35,130 @@ export default function (editor: Editor, video: string): PanelConf {
|
|||
* @param video 在线视频链接
|
||||
*/
|
||||
function checkOnlineVideo(video: string): boolean {
|
||||
// 编辑器进行正常校验,video 合规则使指针为true,不合规为false
|
||||
let flag = true
|
||||
if (!videoRegex.test(video)) {
|
||||
flag = false
|
||||
}
|
||||
|
||||
// 查看开发者自定义配置的返回值
|
||||
const check = editor.config.onlineVideoCheck(video)
|
||||
if (check === undefined) {
|
||||
if (flag === false) console.log(t('您刚才插入的视频链接未通过编辑器校验', 'validate.'))
|
||||
} else if (check === true) {
|
||||
// 用户通过了开发者的校验
|
||||
if (flag === false) {
|
||||
editor.config.customAlert(
|
||||
`${t('您插入的网络视频无法识别', 'validate.')},${t(
|
||||
'请替换为正确的网络视频格式',
|
||||
'validate.'
|
||||
)}:如<iframe src=...></iframe>`,
|
||||
'warning'
|
||||
)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (check === true) {
|
||||
return true
|
||||
}
|
||||
if (typeof check === 'string') {
|
||||
//用户未能通过开发者的校验,开发者希望我们提示这一字符串
|
||||
editor.config.customAlert(check, 'error')
|
||||
}
|
||||
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,
|
||||
height: 0,
|
||||
|
||||
// panel 中可包含多个 tab
|
||||
tabs: [
|
||||
{
|
||||
// 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()
|
||||
tabs: [], // tabs end
|
||||
}
|
||||
|
||||
// 视频为空,则不插入
|
||||
if (!video) return
|
||||
// 对当前用户插入的内容进行判断,插入为空,或者返回false,都停止插入
|
||||
if (!checkOnlineVideo(video)) return
|
||||
|
||||
insertVideo(video)
|
||||
|
||||
// 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
}, // tab end
|
||||
], // tabs end
|
||||
// 显示“上传视频”
|
||||
if (window.FileReader && (config.uploadVideoServer || config.customUploadVideo)) {
|
||||
conf.tabs.push(tabsConf[0])
|
||||
}
|
||||
// 显示“插入视频”
|
||||
if (config.showLinkVideo) {
|
||||
conf.tabs.push(tabsConf[1])
|
||||
}
|
||||
|
||||
return conf
|
||||
|
|
|
@ -9,6 +9,7 @@ import Editor from '../../editor/index'
|
|||
import PanelMenu from '../menu-constructors/PanelMenu'
|
||||
import { MenuActive } from '../menu-constructors/Menu'
|
||||
import createPanelConf from './create-panel-conf'
|
||||
import bindEvent from './bind-event/index'
|
||||
|
||||
class Video extends PanelMenu implements MenuActive {
|
||||
constructor(editor: Editor) {
|
||||
|
@ -18,6 +19,9 @@ class Video extends PanelMenu implements MenuActive {
|
|||
</div>`
|
||||
)
|
||||
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
|
|
@ -13,9 +13,10 @@ import getHtmlByNodeList from './getHtmlByNodeList'
|
|||
/** 按键函数 */
|
||||
type KeyBoardHandler = (event: KeyboardEvent) => unknown
|
||||
/** 普通事件回调 */
|
||||
type EventHandler = (event: Event) => unknown
|
||||
type EventHandler = (event?: Event) => unknown
|
||||
// 各个事件钩子函数
|
||||
type TextEventHooks = {
|
||||
onBlurEvents: EventHandler[]
|
||||
changeEvents: (() => void)[] // 内容修改时
|
||||
dropEvents: ((event: DragEvent) => unknown)[]
|
||||
clickEvents: EventHandler[]
|
||||
|
@ -54,6 +55,8 @@ type TextEventHooks = {
|
|||
dropListMenuHoverEvents: (() => void)[]
|
||||
/** 点击分割线时 */
|
||||
splitLineEvents: ((e: DomElement) => void)[]
|
||||
/** 视频点击事件 */
|
||||
videoClickEvents: ((e: DomElement) => void)[]
|
||||
}
|
||||
|
||||
class Text {
|
||||
|
@ -64,6 +67,7 @@ class Text {
|
|||
this.editor = editor
|
||||
|
||||
this.eventHooks = {
|
||||
onBlurEvents: [],
|
||||
changeEvents: [],
|
||||
dropEvents: [],
|
||||
clickEvents: [],
|
||||
|
@ -85,6 +89,7 @@ class Text {
|
|||
menuClickEvents: [],
|
||||
dropListMenuHoverEvents: [],
|
||||
splitLineEvents: [],
|
||||
videoClickEvents: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,11 +234,18 @@ class Text {
|
|||
public append(html: string): void {
|
||||
const editor = this.editor
|
||||
const $textElem = editor.$textElem
|
||||
const blankLineReg = /(<p><br><\/p>)+$/g
|
||||
if (html.indexOf('<') !== 0) {
|
||||
// 普通字符串,用 <p> 包裹
|
||||
html = `<p>${html}</p>`
|
||||
}
|
||||
$textElem.append($(html))
|
||||
if (blankLineReg.test($textElem.html().trim())) {
|
||||
// 如果有多个空行替换最后一个 <p><br></p>
|
||||
const insertHtml = $textElem.html().replace(/(.*)<p><br><\/p>/, '$1' + html)
|
||||
this.html(insertHtml)
|
||||
} else {
|
||||
$textElem.append($(html))
|
||||
}
|
||||
|
||||
// 初始化选区,将光标定位到内容尾部
|
||||
editor.initSelection()
|
||||
|
@ -551,6 +563,27 @@ class Text {
|
|||
const enterDownEvents = eventHooks.enterDownEvents
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,6 @@
|
|||
*/
|
||||
|
||||
export function EMPTY_FN() {}
|
||||
//用于校验图片链接是否符合规范
|
||||
export const imgRegex = /\.(gif|jpg|jpeg|png)$/i
|
||||
|
||||
//用于校验是否为url格式字符串
|
||||
export const urlRegex = /^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/
|
||||
|
||||
//用于校验在线视频是否符合规范
|
||||
export const videoRegex = /((<iframe|video|embed|object)\s+[\s\S]*<\/(iframe|video|embed|object))>|<(iframe|video|embed|object)\s+[\s\S]*\/?>/
|
||||
|
|
|
@ -89,13 +89,15 @@ function _styleArrTrim(style: string | string[]): string[] {
|
|||
export type DomElementSelector =
|
||||
| string
|
||||
| DomElement
|
||||
| HTMLElement
|
||||
| Element
|
||||
| Document
|
||||
| HTMLCollection
|
||||
| Node
|
||||
| NodeList
|
||||
| ChildNode
|
||||
| ChildNode[]
|
||||
| Element
|
||||
| HTMLElement
|
||||
| HTMLElement[]
|
||||
| HTMLCollection
|
||||
| EventTarget
|
||||
| null
|
||||
| undefined
|
||||
|
@ -107,6 +109,7 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
|
|||
length: number
|
||||
elems: HTMLElement[]
|
||||
dataSource: Map<string, any>
|
||||
prior?: DomElement // 通过 getNodeTop 获取顶级段落的时候,可以通过 prior 去回溯来源的子节点
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
|
@ -805,15 +808,25 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
|
|||
* @param editor 富文本实例
|
||||
*/
|
||||
getNodeTop(editor: Editor): DomElement {
|
||||
// 异常抛出,空的 DomElement 直接返回
|
||||
if (this.length < 1) {
|
||||
return this
|
||||
}
|
||||
|
||||
// 获取父级元素,并判断是否是 编辑区域
|
||||
// 如果是则返回当前节点
|
||||
const $parent = this.parent()
|
||||
if (editor.$textElem.equal($parent)) {
|
||||
return this
|
||||
}
|
||||
|
||||
// 到了此处,即代表当前节点不是顶级段落
|
||||
// 将当前节点存放于父节点的 prior 字段下
|
||||
// 主要用于 回溯 子节点
|
||||
// 例如:ul ol 等标签
|
||||
// 实际操作的节点是 li 但是一个 ul ol 的子节点可能有多个
|
||||
// 所以需要对其进行 回溯 找到对应的子节点
|
||||
$parent.prior = this
|
||||
return $parent.getNodeTop(editor)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
* @description upload-img test
|
||||
* @author luochao
|
||||
*/
|
||||
|
||||
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 UploadImg from '../../../../src/menus/img/upload-img'
|
||||
|
||||
let editor: Editor
|
||||
let id = 1
|
||||
|
||||
const imgUrl = 'http://www.wangeditor.com/imgs/logo.jpeg'
|
||||
const errorUrl = 'logo123.jpeg'
|
||||
const errorUrl = 'error.jpeg'
|
||||
const uploadImgServer = 'http://localhost:8881/api/upload-img'
|
||||
const uploadImgServerWithHash = 'http://localhost:8881/api/upload-img#/123'
|
||||
|
||||
const defaultRes = {
|
||||
status: 200,
|
||||
|
@ -32,7 +32,7 @@ const mockXHRHttpRequest = (res: any = defaultRes) => {
|
|||
return mockXHRObject
|
||||
}
|
||||
|
||||
const createUploadImgInstance = (config: any) => {
|
||||
const createUploadImgInstance = (config: any = {}) => {
|
||||
const editor = createEditor(document, `div${id++}`, '', config)
|
||||
const uploadImg = new UploadImg(editor)
|
||||
return uploadImg
|
||||
|
@ -43,44 +43,38 @@ const mockSupportCommand = () => {
|
|||
document.queryCommandSupported = jest.fn(() => true)
|
||||
}
|
||||
|
||||
const deaultFiles = [{ name: 'test.png', size: 512, mimeType: 'image/png' }]
|
||||
const createMockFilse = (fileList: any[] = deaultFiles) => {
|
||||
const defaultFiles = [{ name: 'test.png', size: 512, mimeType: 'image/png' }]
|
||||
const createMockFiles = (fileList: any[] = defaultFiles) => {
|
||||
const files = fileList.map(file => mockFile(file))
|
||||
return files.filter(Boolean)
|
||||
}
|
||||
|
||||
// mock img onload and onerror event
|
||||
const mockImgOnloadAndOnError = () => {
|
||||
// Mocking Image.prototype.src to call the onload or onerror
|
||||
// callbacks depending on the src passed to it
|
||||
// @ts-ignore
|
||||
Object.defineProperty(global.Image.prototype, 'src', {
|
||||
// Define the property setter
|
||||
set(src) {
|
||||
if (src === errorUrl) {
|
||||
// Call with setTimeout to simulate async loading
|
||||
setTimeout(() => this.onerror(new Error('mocked error')))
|
||||
} else if (src === imgUrl) {
|
||||
setTimeout(() => this.onload())
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('upload img', () => {
|
||||
// mock img onload and onerror event
|
||||
beforeAll(() => {
|
||||
// Mocking Image.prototype.src to call the onload or onerror
|
||||
// callbacks depending on the src passed to it
|
||||
// @ts-ignore
|
||||
Object.defineProperty(global.Image.prototype, 'src', {
|
||||
// Define the property setter
|
||||
set(src) {
|
||||
if (src === errorUrl) {
|
||||
// Call with setTimeout to simulate async loading
|
||||
setTimeout(() => this.onerror(new Error('mocked error')))
|
||||
} else if (src === imgUrl) {
|
||||
setTimeout(() => this.onload())
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
editor = createEditor(document, `div${id++}`)
|
||||
})
|
||||
|
||||
test('能够初始化基本的UploadImg类', () => {
|
||||
const uploadImg = new UploadImg(editor)
|
||||
|
||||
expect(uploadImg.insertImg instanceof Function).toBeTruthy()
|
||||
expect(uploadImg.uploadImg instanceof Function).toBeTruthy()
|
||||
// mock img onload and onerror event
|
||||
mockImgOnloadAndOnError()
|
||||
})
|
||||
|
||||
test('调用 insertImg 可以网编辑器里插入图片', () => {
|
||||
const uploadImg = new UploadImg(editor)
|
||||
const uploadImg = createUploadImgInstance()
|
||||
|
||||
mockSupportCommand()
|
||||
|
||||
|
@ -104,11 +98,6 @@ describe('upload img', () => {
|
|||
|
||||
uploadImg.insertImg(imgUrl)
|
||||
|
||||
expect(document.execCommand).toBeCalledWith(
|
||||
'insertHTML',
|
||||
false,
|
||||
`<img src="${imgUrl}" style="max-width:100%;"/>`
|
||||
)
|
||||
expect(callback).toBeCalledWith(imgUrl)
|
||||
})
|
||||
|
||||
|
@ -130,10 +119,10 @@ describe('upload img', () => {
|
|||
`wangEditor: 插入图片错误,图片链接 "${errorUrl}",下载链接失败`
|
||||
)
|
||||
done()
|
||||
}, 1000)
|
||||
}, 200)
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片', done => {
|
||||
test('可以调用 uploadImg 上传图片', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const jestFn = jest.fn()
|
||||
|
@ -145,7 +134,7 @@ describe('upload img', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
upload.uploadImg(files)
|
||||
|
@ -158,24 +147,24 @@ describe('upload img', () => {
|
|||
}, 1000)
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果传入的文件为空直接返回', () => {
|
||||
const upload = new UploadImg(editor)
|
||||
test('调用 uploadImg 上传图片,如果传入的文件为空直接返回 undefined,不执行上传操作', () => {
|
||||
const upload = createUploadImgInstance()
|
||||
|
||||
const res = upload.uploadImg([])
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果没有配置customUploadImg, 则必须配置 uploadImgServer 或者 uploadImgShowBase64', () => {
|
||||
const upload = new UploadImg(editor)
|
||||
const files = createMockFilse()
|
||||
test('调用 uploadImg 上传图片,如果没有配置customUploadImg, 则必须配置 uploadImgServer 或者 uploadImgShowBase64,否则直接返回', () => {
|
||||
const upload = createUploadImgInstance()
|
||||
const files = createMockFiles()
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果文件没有名字或者size为,则会被过滤掉', () => {
|
||||
test('调用 uploadImg 上传图片,如果文件没有名字或者size为0,则会弹窗警告', () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
|
@ -183,15 +172,14 @@ describe('upload img', () => {
|
|||
customAlert: fn,
|
||||
})
|
||||
|
||||
const files = createMockFilse([{ name: '', size: 0, mimeType: 'image/png' }])
|
||||
const files = createMockFiles([{ name: '', size: 0, mimeType: 'image/png' }])
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
expect(fn).toBeCalledWith('传入的文件不合法', 'warning')
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果文件非图片,则返回并提示错误信息', () => {
|
||||
test('调用 uploadImg 上传图片,如果文件非图片,则提示错误信息', () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
|
@ -199,15 +187,14 @@ describe('upload img', () => {
|
|||
customAlert: fn,
|
||||
})
|
||||
|
||||
const files = createMockFilse([{ name: 'test.txt', size: 200, mimeType: 'text/plain' }])
|
||||
const files = createMockFiles([{ name: 'test.txt', size: 200, mimeType: 'text/plain' }])
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
expect(fn).toBeCalledWith('图片验证未通过: \n【test.txt】不是图片', 'warning')
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果文件体积大小超过配置的大小,则返回并提示错误信息', () => {
|
||||
test('调用 uploadImg 上传图片,如果文件体积大小超过配置的大小,则提示错误信息', () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
|
@ -216,13 +203,12 @@ describe('upload img', () => {
|
|||
customAlert: fn,
|
||||
})
|
||||
|
||||
const files = createMockFilse([
|
||||
const files = createMockFiles([
|
||||
{ name: 'test.png', size: 6 * 1024 * 1024, mimeType: 'image/png' },
|
||||
])
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
expect(fn).toBeCalledWith(`图片验证未通过: \n【test.png】大于 5M`, 'warning')
|
||||
})
|
||||
|
||||
|
@ -235,19 +221,18 @@ describe('upload img', () => {
|
|||
customAlert: fn,
|
||||
})
|
||||
|
||||
const files = createMockFilse([
|
||||
const files = createMockFiles([
|
||||
{ name: 'test1.png', size: 2048, mimeType: 'image/png' },
|
||||
{ name: 'test2.png', size: 2048, mimeType: 'image/png' },
|
||||
{ name: 'test3.png', size: 2048, mimeType: 'image/png' },
|
||||
])
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
expect(fn).toBeCalledWith('一次最多上传2张图片', 'warning')
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果配置了 customUploadImg 选项,则调用customUploadImg上传', () => {
|
||||
test('调用 uploadImg 上传图片,如果配置了 customUploadImg 选项,则调用 customUploadImg 上传', () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
|
@ -255,15 +240,14 @@ describe('upload img', () => {
|
|||
customUploadImg: fn,
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const res = upload.uploadImg(files)
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(res).toBeUndefined()
|
||||
expect(fn).toBeCalled()
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,如果可以配置uploadImgParamsWithUrl添加query参数', done => {
|
||||
test('调用 uploadImg 上传图片,如果配置了 uploadImgParamsWithUrl 为true, 则允许添加query参数', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const fn = jest.fn()
|
||||
|
@ -280,7 +264,7 @@ describe('upload img', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
|
@ -289,18 +273,18 @@ describe('upload img', () => {
|
|||
mockXHRObject.onreadystatechange()
|
||||
|
||||
setTimeout(() => {
|
||||
expect(fn).toBeCalled()
|
||||
expect(mockXHRObject.open).toHaveBeenCalledWith('POST', `${uploadImgServer}?a=a&b=b`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片,uploadImgServer支持hash参数拼接', done => {
|
||||
test('调用 uploadImg 上传图片,uploadImgServer支持 hash query 参数拼接', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const fn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
uploadImgServer,
|
||||
uploadImgServer: uploadImgServerWithHash,
|
||||
uploadImgParams: {
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
|
@ -311,10 +295,7 @@ describe('upload img', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const files = createMockFilse([
|
||||
{ name: 'test1.png', size: 2048, mimeType: 'image/png' },
|
||||
{ name: 'test2.png', size: 2048, mimeType: 'image/png' },
|
||||
])
|
||||
const files = createMockFiles([{ name: 'test1.png', size: 2048, mimeType: 'image/png' }])
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
|
@ -323,25 +304,28 @@ describe('upload img', () => {
|
|||
mockXHRObject.onreadystatechange()
|
||||
|
||||
setTimeout(() => {
|
||||
expect(fn).toBeCalled()
|
||||
expect(mockXHRObject.open).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
`${uploadImgServer}?a=a&b=b#/123`
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片失败,会有错误提示,并支持配置onError hook', done => {
|
||||
expect.assertions(2)
|
||||
test('调用 uploadImg 上传图片失败,支持配置 onError 钩子监听', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const fn = jest.fn()
|
||||
const errorFn = jest.fn()
|
||||
const alertFn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
uploadImgServer,
|
||||
uploadImgHooks: {
|
||||
error: fn,
|
||||
error: errorFn,
|
||||
},
|
||||
customAlert: alertFn,
|
||||
})
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest({ status: 500 })
|
||||
|
||||
|
@ -350,18 +334,13 @@ describe('upload img', () => {
|
|||
mockXHRObject.onreadystatechange()
|
||||
|
||||
setTimeout(() => {
|
||||
expect(fn).toBeCalled()
|
||||
expect(alertFn).toBeCalledWith(
|
||||
'上传图片错误',
|
||||
'error',
|
||||
'上传图片错误,服务器返回状态: 500'
|
||||
)
|
||||
expect(errorFn).toBeCalled()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传图片成功后数据返回不正常,会有错误提示,并支持配置onFail hook', done => {
|
||||
expect.assertions(2)
|
||||
test('调用 uploadImg 上传图片成功后数据返回格式不正常,支持配置 onFail 钩子监听', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const fn = jest.fn()
|
||||
const alertFn = jest.fn()
|
||||
|
@ -373,7 +352,7 @@ describe('upload img', () => {
|
|||
},
|
||||
customAlert: alertFn,
|
||||
})
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest({
|
||||
status: 200,
|
||||
|
@ -386,11 +365,6 @@ describe('upload img', () => {
|
|||
|
||||
setTimeout(() => {
|
||||
expect(fn).toBeCalled()
|
||||
expect(alertFn).toBeCalledWith(
|
||||
'上传图片失败',
|
||||
'error',
|
||||
'上传图片返回结果错误,返回结果: {test: 123}'
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
@ -407,7 +381,7 @@ describe('upload img', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
|
@ -421,21 +395,21 @@ describe('upload img', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传被阻止,会有错误提示', done => {
|
||||
test('调用 uploadImg, 如果配置 before 钩子阻止上传,会有错误提示', done => {
|
||||
expect.assertions(2)
|
||||
|
||||
const beforFn = jest.fn(() => ({ prevent: true, msg: '阻止发送请求' }))
|
||||
const beforeFn = jest.fn(() => ({ prevent: true, msg: '阻止发送请求' }))
|
||||
const alertFn = jest.fn()
|
||||
|
||||
const upload = createUploadImgInstance({
|
||||
uploadImgServer,
|
||||
uploadImgHooks: {
|
||||
before: beforFn,
|
||||
before: beforeFn,
|
||||
},
|
||||
customAlert: alertFn,
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
|
@ -444,14 +418,14 @@ describe('upload img', () => {
|
|||
mockXHRObject.onreadystatechange()
|
||||
|
||||
setTimeout(() => {
|
||||
expect(beforFn).toBeCalled()
|
||||
expect(beforeFn).toBeCalled()
|
||||
expect(alertFn).toBeCalledWith('阻止发送请求', 'error')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传返回的错误码不符合条件会有错误提示,并触发fail回调', done => {
|
||||
expect.assertions(2)
|
||||
test('调用 uploadImg 上传返回的错误码不符合条件, 会触发fail回调', done => {
|
||||
expect.assertions(1)
|
||||
|
||||
const failFn = jest.fn()
|
||||
const alertFn = jest.fn()
|
||||
|
@ -464,7 +438,7 @@ describe('upload img', () => {
|
|||
customAlert: alertFn,
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockXHRObject = mockXHRHttpRequest({
|
||||
status: 200,
|
||||
|
@ -477,39 +451,33 @@ describe('upload img', () => {
|
|||
|
||||
setTimeout(() => {
|
||||
expect(failFn).toBeCalled()
|
||||
expect(alertFn).toBeCalledWith(
|
||||
'上传图片失败',
|
||||
'error',
|
||||
'上传图片返回结果错误,返回结果 errno=-1'
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传,如果配置 uploadImgShowBase64 参数,则直接插入base64到编辑器', () => {
|
||||
const callback = jest.fn()
|
||||
test('调用 uploadImg 上传,如果配置 uploadImgShowBase64 参数,则直接插入图片 base64 字符串到编辑器', () => {
|
||||
const upload = createUploadImgInstance({
|
||||
uploadImgShowBase64: true,
|
||||
linkImgCallback: callback,
|
||||
})
|
||||
const files = createMockFilse()
|
||||
|
||||
const mockFn = jest.fn()
|
||||
const files = createMockFiles()
|
||||
|
||||
const mockReadAsDataURL = jest.fn()
|
||||
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, 'FileReader').mockImplementation(() => {
|
||||
return {
|
||||
readAsDataURL: mockFn,
|
||||
readAsDataURL: mockReadAsDataURL,
|
||||
}
|
||||
})
|
||||
|
||||
upload.uploadImg(files)
|
||||
|
||||
expect(mockFn).toBeCalled()
|
||||
expect(mockReadAsDataURL).toBeCalled()
|
||||
})
|
||||
|
||||
test('调用 uploadImg 上传超时会触发超时回调', done => {
|
||||
expect.assertions(2)
|
||||
expect.assertions(1)
|
||||
|
||||
const timeoutFn = jest.fn()
|
||||
const alertFn = jest.fn()
|
||||
|
@ -522,7 +490,7 @@ describe('upload img', () => {
|
|||
customAlert: alertFn,
|
||||
})
|
||||
|
||||
const files = createMockFilse()
|
||||
const files = createMockFiles()
|
||||
const mockXHRObject = mockXHRHttpRequest()
|
||||
|
||||
upload.uploadImg(files)
|
||||
|
@ -531,7 +499,6 @@ describe('upload img', () => {
|
|||
|
||||
setTimeout(() => {
|
||||
expect(timeoutFn).toBeCalled()
|
||||
expect(alertFn).toBeCalledWith('上传图片超时', 'error')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,36 +13,82 @@ import { getMenuInstance } from '../../helpers/menus'
|
|||
let editor: Editor
|
||||
let justifyMenu: justify
|
||||
|
||||
test('justify 菜单:dropList', () => {
|
||||
editor = createEditor(document, 'div1') // 赋值给全局变量
|
||||
justifyMenu = getMenuInstance(editor, justify) as justify // 赋值给全局变量
|
||||
expect(justifyMenu.dropList).not.toBeNull()
|
||||
justifyMenu.dropList.show()
|
||||
expect(justifyMenu.dropList.isShow).toBe(true)
|
||||
justifyMenu.dropList.hide()
|
||||
expect(justifyMenu.dropList.isShow).toBe(false)
|
||||
})
|
||||
describe('Justify Menu', () => {
|
||||
test('justify 菜单:dropList', () => {
|
||||
editor = createEditor(document, 'div1') // 赋值给全局变量
|
||||
justifyMenu = getMenuInstance(editor, justify) as justify // 赋值给全局变量
|
||||
expect(justifyMenu.dropList).not.toBeNull()
|
||||
justifyMenu.dropList.show()
|
||||
expect(justifyMenu.dropList.isShow).toBe(true)
|
||||
justifyMenu.dropList.hide()
|
||||
expect(justifyMenu.dropList.isShow).toBe(false)
|
||||
})
|
||||
|
||||
test('justify 菜单:设置对齐方式', () => {
|
||||
type justifyType = {
|
||||
[key: string]: string
|
||||
}
|
||||
const justifyClass: justifyType = {
|
||||
justifyLeft: 'left',
|
||||
justifyCenter: 'center',
|
||||
justifyRight: 'right',
|
||||
justifyFull: 'justify',
|
||||
}
|
||||
const mockGetSelectionRangeTopNodes = (tagString: string) => {
|
||||
const domArr = [$(tagString)]
|
||||
jest.spyOn(editor.selection, 'getSelectionRangeTopNodes').mockImplementation(() => domArr)
|
||||
return domArr
|
||||
}
|
||||
const $elems = mockGetSelectionRangeTopNodes('<p>123</p>')
|
||||
for (let key in justifyClass) {
|
||||
justifyMenu.command(key)
|
||||
$elems.forEach((el: DomElement) => {
|
||||
expect(el.elems[0].getAttribute('style')).toContain(`text-align:${justifyClass[key]}`)
|
||||
})
|
||||
}
|
||||
test('justify 菜单:设置对齐方式', () => {
|
||||
const justifyClasses = ['left', 'right', 'center', 'justify']
|
||||
|
||||
const mockGetSelectionRangeTopNodes = (tagString: string) => {
|
||||
const domArr = [$(tagString)]
|
||||
jest.spyOn(editor.selection, 'getSelectionRangeTopNodes').mockImplementation(
|
||||
() => domArr
|
||||
)
|
||||
return domArr
|
||||
}
|
||||
|
||||
const $elems = mockGetSelectionRangeTopNodes('<p>123</p>')
|
||||
for (let value of justifyClasses) {
|
||||
justifyMenu.command(value)
|
||||
$elems.forEach((el: DomElement) => {
|
||||
expect(el.elems[0].getAttribute('style')).toContain(`text-align:${value}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('justify 菜单:设置对齐方式,如果当前选区是 list,则只修改当前选区的元素对其样式', () => {
|
||||
const justifyClasses = ['left', 'right', 'center', 'justify']
|
||||
|
||||
const mockGetSelectionRangeTopNodes = (tagString: string) => {
|
||||
const domArr = [$(tagString)]
|
||||
jest.spyOn(editor.selection, 'getSelectionRangeTopNodes').mockImplementation(
|
||||
() => domArr
|
||||
)
|
||||
return domArr
|
||||
}
|
||||
|
||||
const mockGetSelectionRangeContainer = (tagString: string) => {
|
||||
const dom = $(tagString)
|
||||
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() => dom)
|
||||
return dom
|
||||
}
|
||||
|
||||
const topElems = mockGetSelectionRangeTopNodes('<ul><li>123</li></ul>')
|
||||
const containerElems = mockGetSelectionRangeContainer('<li>123</li>')
|
||||
|
||||
for (let value of justifyClasses) {
|
||||
justifyMenu.command(value)
|
||||
topElems.forEach((el: DomElement) => {
|
||||
expect(el.elems[0]).not.toHaveStyle(`text-align:${value}`)
|
||||
})
|
||||
expect(containerElems.elems[0]).toHaveStyle(`text-align:${value}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('justify 菜单:设置对齐方式,如果当前选区是 blockquote,则只修改当前选区的元素对其样式', () => {
|
||||
const justifyClasses = ['left', 'right', 'center', 'justify']
|
||||
|
||||
const $p = $('<p>123</p>')
|
||||
const blockquote = $('<blockquote></blockquote>')
|
||||
blockquote.append($p)
|
||||
|
||||
jest.spyOn(editor.selection, 'getSelectionRangeTopNodes').mockImplementation(() => [
|
||||
blockquote,
|
||||
])
|
||||
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() => $p)
|
||||
|
||||
for (let value of justifyClasses) {
|
||||
justifyMenu.command(value)
|
||||
expect(blockquote.elems[0]).not.toHaveStyle(`text-align:${value}`)
|
||||
expect($p.elems[0]).toHaveStyle(`text-align:${value}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -66,7 +66,9 @@ describe('LineHeight menu', () => {
|
|||
const cmdVal = '2'
|
||||
lineHeightMenu.command(cmdVal)
|
||||
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
|
||||
expect(editor.$textElem.elems[0].innerHTML.indexOf('<p>123</p>')).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
editor.$textElem.elems[0].innerHTML.indexOf('<p style="line-height:2;">123</p>')
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
test('lineHeight 菜单:选择多行增加行高, 设置非段落的P标签开头全选', () => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @author tonghan
|
||||
*/
|
||||
|
||||
import List from '../../../src/menus/list/index'
|
||||
import List, { ListType } from '../../../src/menus/list/index'
|
||||
import Editor from '../../../src/editor'
|
||||
import mockCmdFn from '../../helpers/command-mock'
|
||||
import createEditor from '../../helpers/create-editor'
|
||||
|
@ -24,11 +24,11 @@ test('list 菜单:dropList', () => {
|
|||
|
||||
test('list 菜单:有序', () => {
|
||||
mockCmdFn(document)
|
||||
listMenu.command('insertOrderedList')
|
||||
expect(document.execCommand).toBeCalledWith('insertOrderedList', false, undefined)
|
||||
listMenu.command(ListType.OrderedList)
|
||||
expect(editor.$textElem.html().includes('<ol>')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('list 菜单:无序', () => {
|
||||
listMenu.command('insertUnorderedList')
|
||||
expect(document.execCommand).toBeCalledWith('insertUnorderedList', false, undefined)
|
||||
listMenu.command(ListType.UnorderedList)
|
||||
expect(editor.$textElem.html().includes('<ul>')).toBeTruthy()
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ import Undo from '../../../src/menus/undo/index'
|
|||
import mockCmdFn from '../../helpers/command-mock'
|
||||
import { getMenuInstance } from '../../helpers/menus'
|
||||
|
||||
test('重做', done => {
|
||||
test('重做 兼容模式', done => {
|
||||
let count = 0
|
||||
const editor = createEditor(document, 'div1', '', {
|
||||
onchange: function () {
|
||||
|
@ -22,21 +22,37 @@ test('重做', done => {
|
|||
if (count == 2) {
|
||||
const redo = getMenuInstance(editor, Redo) as Redo
|
||||
redo.clickHandler()
|
||||
|
||||
if (editor.isCompatibleMode) {
|
||||
// 兼容模式
|
||||
expect(editor.$textElem.html()).toEqual('<p><span>123</span><br></p>')
|
||||
} else {
|
||||
// 标准模式
|
||||
expect(editor.history.size).toEqual([1, 0])
|
||||
}
|
||||
// 兼容模式
|
||||
expect(editor.$textElem.html()).toEqual('<p><br><span>123</span></p>')
|
||||
}
|
||||
|
||||
done()
|
||||
},
|
||||
compatibleMode: function () {
|
||||
return Math.round(Math.random()) == 1
|
||||
},
|
||||
})
|
||||
|
||||
mockCmdFn(document)
|
||||
editor.cmd.do('insertHTML', '<span>123</span>')
|
||||
})
|
||||
|
||||
test('重做 标准模式', done => {
|
||||
let count1 = 0
|
||||
const editor = createEditor(document, 'div2', '', {
|
||||
onchange: function () {
|
||||
count1++
|
||||
// 由 editor.cmd.do 触发的 onchange
|
||||
if (count1 == 1) {
|
||||
const undo = getMenuInstance(editor, Undo) as Undo
|
||||
undo.clickHandler()
|
||||
}
|
||||
// 由 undo.clickHandler 触发的 onchange
|
||||
if (count1 == 2) {
|
||||
const redo = getMenuInstance(editor, Redo) as Redo
|
||||
redo.clickHandler()
|
||||
// 标准模式
|
||||
expect(editor.history.size).toEqual([1, 0])
|
||||
}
|
||||
|
||||
done()
|
||||
},
|
||||
})
|
||||
|
||||
mockCmdFn(document)
|
||||
|
|
|
@ -9,13 +9,13 @@ editor.txt.append('<p>abc</p><p>test</p>')
|
|||
test('设置todo功能', () => {
|
||||
boldMenu.clickHandler()
|
||||
expect(editor.txt.html()).toEqual(
|
||||
`<p><br></p><p>abc</p><ul class="w-e-todo"><li><span contenteditable="false"><input type="checkbox"></span>test</li></ul>`
|
||||
`<p>abc</p><ul class="w-e-todo"><li><span contenteditable="false"><input type="checkbox"></span>test</li></ul>`
|
||||
)
|
||||
})
|
||||
|
||||
test('取消todo功能', () => {
|
||||
boldMenu.clickHandler()
|
||||
expect(editor.txt.html()).toEqual(`<p><br></p><p>abc</p><p>test</p>`)
|
||||
expect(editor.txt.html()).toEqual(`<p>abc</p><p>test</p>`)
|
||||
})
|
||||
|
||||
test('在第一行设置todo', () => {
|
||||
|
|
|
@ -43,6 +43,7 @@ test('点击 tooltip', () => {
|
|||
})
|
||||
|
||||
test('tooltip 显示和隐藏', () => {
|
||||
tooltip.create()
|
||||
expect(tooltip.isShow).toBe(true)
|
||||
|
||||
tooltip.remove()
|
||||
|
|
|
@ -70,12 +70,10 @@ test('video onlineVideoCheck 自定义检查', () => {
|
|||
videoMenu.clickHandler()
|
||||
editor.config.onlineVideoCheck = function (video: string) {
|
||||
if (video === '测试') {
|
||||
return '测试禁止插入'
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const fn3 = jest.fn()
|
||||
editor.config.customAlert = fn3
|
||||
|
||||
const panel = videoMenu.panel as Panel
|
||||
const panelElem = panel.$container.elems[0]
|
||||
|
@ -91,7 +89,6 @@ test('video onlineVideoCheck 自定义检查', () => {
|
|||
$videoIFrame.val(video)
|
||||
$btnInsert.click()
|
||||
|
||||
expect(fn3).toBeCalled()
|
||||
expect(editor.$textElem.html().indexOf(video)).toBe(-1)
|
||||
})
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -111,7 +111,7 @@ describe('Editor Text test', () => {
|
|||
test('编辑器初始化后,调用 txt append 方法 能追加 html', () => {
|
||||
editor.txt.append('12345<span>1234</span>')
|
||||
|
||||
expect(editor.txt.html()).toEqual('<p><br></p><p>12345<span>1234</span></p>')
|
||||
expect(editor.txt.html()).toEqual('<p>12345<span>1234</span></p>')
|
||||
})
|
||||
|
||||
test('编辑器初始化后,编辑器区域会绑定 keyup 事件,触发保存range和激活菜单函数', () => {
|
||||
|
|
Loading…
Reference in New Issue