Skip to content

Commit

Permalink
feat: video menu
Browse files Browse the repository at this point in the history
  • Loading branch information
wangfupeng1988 committed Jun 9, 2021
1 parent 5262634 commit c1faa1c
Show file tree
Hide file tree
Showing 15 changed files with 381 additions and 19 deletions.
4 changes: 4 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
type: 'paragraph',
children: [{ text: '一行文字' }]
},
{
type: 'divider',
children: [{text: ''}]
},
{
type: 'paragraph',
children: [{ text: '一行文字' }]
Expand Down
2 changes: 2 additions & 0 deletions packages/basic/src/assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
@import "blockquote.less";
@import "emotion.less";
@import "divider.less";
@import "blockquote.less";
@import "video.less";
5 changes: 5 additions & 0 deletions packages/basic/src/assets/video.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.w-e-textarea-video-container {
text-align: center;
border: 1px solid #d9d9d9;
padding: 5px 0;
}
2 changes: 2 additions & 0 deletions packages/basic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import lineHeight from './modules/line-height'
import undoRedo from './modules/undo-redo'
import list from './modules/list'
import divider from './modules/divider'
import video from './modules/video'

export {
simpleStyle,
Expand All @@ -37,4 +38,5 @@ export {
undoRedo,
list,
divider,
video,
}
4 changes: 4 additions & 0 deletions packages/basic/src/modules/_helpers/icon-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,7 @@ export const NUMBERED_LIST_SVG =
// 分割线
export const DIVIDER_SVG =
'<svg viewBox="0 0 1092 1024"><path d="M0 51.2m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z"></path><path d="M0 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z"></path><path d="M819.2 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z"></path><path d="M409.6 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z"></path><path d="M0 870.4m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z"></path></svg>'

// 视频
export const VIDEO_SVG =
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>'
2 changes: 1 addition & 1 deletion packages/basic/src/modules/divider/render-elem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function renderDivider(
// 是否选中
const selected = isNodeSelected(editor, elemNode, 'divider')
renderStyle.boxShadow = selected ? '0 0 0 3px #B4D5FF' : 'none'
// TODO 选中时,显示拖拽框(不要在这里渲染,考虑一个独立的插件)
// TODO 抽离选中样式

const vnode = <div className="w-e-textarea-divider" style={renderStyle}></div>
// 【注意】void node 中,renderElem 不用处理 children 。core 会统一处理。
Expand Down
10 changes: 5 additions & 5 deletions packages/basic/src/modules/image/menu/InsertImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ class InsertImage implements IModalMenu {
if (type === 'code') return true // 行内代码
if (type === 'pre') return true // 代码块
if (type === 'link') return true // 链接
if (type === 'list-item') return true // list
if (type.startsWith('header')) return true // 标题
if (['ul', 'ol', 'li'].includes(type)) return true // 列表
if (type === 'blockquote') return true // 引用
if (Editor.isVoid(editor, n)) return true // void
// TODO 引用块,禁用

return false
},
Expand Down Expand Up @@ -92,9 +92,9 @@ class InsertImage implements IModalMenu {
// 绑定事件(第一次渲染时绑定,不要重复绑定)
$content.on('click', `#${buttonId}`, e => {
e.preventDefault()
const src = $(`#${srcInputId}`).val()
const alt = $(`#${altInputId}`).val()
const url = $(`#${urlInputId}`).val()
const src = $(`#${srcInputId}`).val().trim()
const alt = $(`#${altInputId}`).val().trim()
const url = $(`#${urlInputId}`).val().trim()
this.insertImage(editor, src, alt, url)
})

Expand Down
1 change: 1 addition & 0 deletions packages/basic/src/modules/image/render-elem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function renderImage(elemNode: SlateElement, children: VNode[] | null, editor: I
const selected = isNodeSelected(editor, elemNode, 'image')
renderStyle.boxShadow = selected ? '0 0 0 3px #B4D5FF' : 'none'
// TODO 选中时,显示拖拽框(不要在这里渲染,考虑一个独立的插件)
// TODO 抽离选中样式

// 尺寸
if (width) renderStyle.width = width
Expand Down
17 changes: 17 additions & 0 deletions packages/basic/src/modules/video/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @description video module
* @author wangfupeng
*/

import { IModuleConf } from '@wangeditor/core'
import withVideo from './plugin'
import { renderVideoConf } from './render-elem'
import { insertVideoMenuConf, deleteVideoMenuConf } from './menu/index'

const video: IModuleConf = {
renderElems: [renderVideoConf],
menus: [insertVideoMenuConf, deleteVideoMenuConf],
editorPlugin: withVideo,
}

export default video
51 changes: 51 additions & 0 deletions packages/basic/src/modules/video/menu/DeleteVideoMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @description delete video menu
* @author wangfupeng
*/

import { Transforms } from 'slate'
import { IButtonMenu, IDomEditor } from '@wangeditor/core'
import { checkNodeType, getSelectedNodeByType } from '../../_helpers/node'
import { TRASH_SVG } from '../../_helpers/icon-svg'

class DeleteVideoMenu implements IButtonMenu {
title = '删除视频'
iconSvg = TRASH_SVG
tag = 'button'

getValue(editor: IDomEditor): string | boolean {
// 无需获取 val
return ''
}

isActive(editor: IDomEditor): boolean {
const videoNode = getSelectedNodeByType(editor, 'video')
if (videoNode) {
// 选区处于 video node
return true
}
return false
}

isDisabled(editor: IDomEditor): boolean {
if (editor.selection == null) return true

const videoNode = getSelectedNodeByType(editor, 'video')
if (videoNode == null) {
// 选区未处于 video node ,则禁用
return true
}
return false
}

exec(editor: IDomEditor, value: string | boolean) {
if (this.isDisabled(editor)) return

// 删除视频
Transforms.removeNodes(editor, {
match: n => checkNodeType(n, 'video'),
})
}
}

export default DeleteVideoMenu
127 changes: 127 additions & 0 deletions packages/basic/src/modules/video/menu/InsertVideoMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @description insert video menu
* @author wangfupeng
*/

import { Editor, Transforms, Range, Node } from 'slate'
import { IModalMenu, IDomEditor, DomEditor, hideAllPanelsAndModals } from '@wangeditor/core'
import $, { Dom7Array } from '../../../utils/dom'
import { genRandomStr } from '../../../utils/util'
import { genModalInputElems, genModalButtonElems } from '../../_helpers/menu'
import { VIDEO_SVG } from '../../_helpers/icon-svg'

/**
* 生成唯一的 DOM ID
*/
function genDomID(): string {
return genRandomStr('w-e-insert-video')
}

class InsertVideoMenu implements IModalMenu {
title = '插入视频'
iconSvg = VIDEO_SVG
tag = 'button'
showModal = true // 点击 button 时显示 modal
modalWidth = 300
private $content: Dom7Array | null = null
private srcInputId = genDomID()
private buttonId = genDomID()

getValue(editor: IDomEditor): string | boolean {
// 插入菜单,不需要 value
return ''
}

isActive(editor: IDomEditor): boolean {
// 任何时候,都不用激活 menu
return false
}

exec(editor: IDomEditor, value: string | boolean) {
// 点击菜单时,弹出 modal 之前,不需要执行其他代码
// 此处空着即可
}

isDisabled(editor: IDomEditor): boolean {
const { selection } = editor
if (selection == null) return true
if (!Range.isCollapsed(selection)) return true // 选区非折叠,禁用

return false
}

getModalPositionNode(editor: IDomEditor): Node | null {
return null // modal 依据选区定位
}

getModalContentElem(editor: IDomEditor): Dom7Array {
const { srcInputId, buttonId } = this

// 获取 input button elem
const [$srcContainer, $inputSrc] = genModalInputElems(
'视频地址',
srcInputId,
'mp4 网址,或第三方 <iframe>...'
)
const [$buttonContainer] = genModalButtonElems(buttonId, '确定')

if (this.$content == null) {
// 第一次渲染
const $content = $('<div></div>')

// 绑定事件(第一次渲染时绑定,不要重复绑定)
$content.on('click', `#${buttonId}`, e => {
e.preventDefault()
const src = $(`#${srcInputId}`).val().trim()
this.insertVideo(editor, src)
})

// 记录属性,重要
this.$content = $content
}

const $content = this.$content
$content.html('') // 先清空内容

// append inputs and button
$content.append($srcContainer)
$content.append($buttonContainer)

// 设置 input val
$inputSrc.val('')

// focus 一个 input(异步,此时 DOM 尚未渲染)
setTimeout(() => {
$(`#${srcInputId}`).focus()
})

return $content
}

private insertVideo(editor: IDomEditor, src: string) {
if (!src) {
hideAllPanelsAndModals() // 隐藏 modal
return
}

// 还原选区
DomEditor.restoreSelection(editor)

if (this.isDisabled(editor)) return

// 新建一个 video node
const video = {
type: 'video',
src,
children: [{ text: '' }], // 【注意】void node 需要一个空 text 作为 children
}

// 插入图片
Transforms.insertNodes(editor, video)

// 隐藏 modal
hideAllPanelsAndModals()
}
}

export default InsertVideoMenu
21 changes: 21 additions & 0 deletions packages/basic/src/modules/video/menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @description video menu
* @author wangfupeng
*/

import InsertVideoMenu from './InsertVideoMenu'
import DeleteVideoMenu from './DeleteVideoMenu'

export const insertVideoMenuConf = {
key: 'insertVideo',
factory() {
return new InsertVideoMenu()
},
}

export const deleteVideoMenuConf = {
key: 'deleteVideo',
factory() {
return new DeleteVideoMenu()
},
}
54 changes: 54 additions & 0 deletions packages/basic/src/modules/video/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @description editor 插件,重写 editor API
* @author wangfupeng
*/

import { Node, Transforms } from 'slate'
import { IDomEditor } from '@wangeditor/core'

function withVideo<T extends IDomEditor>(editor: T): T {
const { isVoid, normalizeNode } = editor
const newEditor = editor

// 重写 isVoid
newEditor.isVoid = elem => {
// @ts-ignore
const { type } = elem

if (type === 'video') {
return true
}

return isVoid(elem)
}

// 重写 normalizeNode
newEditor.normalizeNode = ([node, path]) => {
// @ts-ignore
const { type } = node

// ----------------- video 后面必须跟一个 p 或 header -----------------
if (type === 'video') {
const topLevelNodes = newEditor.children || []
const nextNode = topLevelNodes[path[0] + 1] || {}
// @ts-ignore
const { type: nextNodeType = '' } = nextNode
if (nextNodeType !== 'paragraph' || nextNodeType.startsWith('header')) {
// video node 后面不是 p 或 header ,则插入一个空 p
const p = { type: 'paragraph', children: [{ text: '' }] }
const insertPath = [path[0] + 1]
Transforms.insertNodes(newEditor, p, {
at: insertPath, // 在表格后面插入
})
}
}

// 执行默认的 normalizeNode ,重要!!!
return normalizeNode([node, path])
}

// 返回 editor ,重要!
return newEditor
}

export default withVideo
Loading

0 comments on commit c1faa1c

Please sign in to comment.