Skip to content

Commit

Permalink
feat: list menu
Browse files Browse the repository at this point in the history
  • Loading branch information
wangfupeng1988 committed Jun 8, 2021
1 parent bfb3014 commit fe6c083
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/basic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import indent from './modules/indent'
import justify from './modules/justify'
import lineHeight from './modules/line-height'
import undoRedo from './modules/undo-redo'
import list from './modules/list'

export {
simpleStyle,
Expand All @@ -33,4 +34,5 @@ export {
justify,
lineHeight,
undoRedo,
list,
}
7 changes: 7 additions & 0 deletions packages/basic/src/modules/_helpers/icon-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,10 @@ export const UNDO_SVG =
// 重做
export const REDO_SVG =
'<svg viewBox="0 0 1024 1024"><path d="M0.00032 576a510.72 510.72 0 0 0 173.344 384l84.672-96A383.136 383.136 0 0 1 128.00032 576C128.00032 363.936 299.93632 192 512.00032 192c106.048 0 202.048 42.976 271.52 112.48L640.00032 448h384V64l-149.984 149.984A510.272 510.272 0 0 0 512.00032 64C229.21632 64 0.00032 293.216 0.00032 576z"></path></svg>'

// 无序列表
export const BULLETED_LIST_SVG =
'<svg viewBox="0 0 1024 1024"><path d="M384 64h640v128H384V64z m0 384h640v128H384v-128z m0 384h640v128H384v-128zM0 128a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z"></path></svg>'

export const NUMBERED_LIST_SVG =
'<svg viewBox="0 0 1024 1024"><path d="M384 832h640v128H384z m0-384h640v128H384z m0-384h640v128H384zM192 0v256H128V64H64V0zM128 526.016v50.016h128v64H64v-146.016l128-60V384H64v-64h192v146.016zM256 704v320H64v-64h128v-64H64v-64h128v-64H64v-64z"></path></svg>'
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ class LineHeightMenu implements ISelectMenu {

// line-height 匹配如下类型的 node
if (type.startsWith('header')) return true
if (['paragraph', 'blockquote', 'ul', 'ol'].includes(type)) return true
if (['paragraph', 'blockquote', 'bulleted-list', 'numbered-list'].includes(type)) {
return true
}

return false
},
Expand Down
17 changes: 17 additions & 0 deletions packages/basic/src/modules/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @description list module entry
* @author wangfupeng
*/

import { IModuleConf } from '@wangeditor/core'
import { renderBulletedListConf, renderNumberedListConf, renderListItemConf } from './render-elem'
import { bulletedListMenuConf, numberedListMenuConf } from './menu/index'
import withList from './plugin'

const bold: IModuleConf = {
renderElems: [renderBulletedListConf, renderNumberedListConf, renderListItemConf],
menus: [bulletedListMenuConf, numberedListMenuConf],
editorPlugin: withList,
}

export default bold
101 changes: 101 additions & 0 deletions packages/basic/src/modules/list/menu/BaseMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @description base menu
* @author wangfupeng
*/

import { Editor, Node, Transforms } from 'slate'
import { IButtonMenu, IDomEditor } from '@wangeditor/core'
import { getSelectedNodeByType } from '../../_helpers/node'

function checkList(n: Node): boolean {
// @ts-ignore
return ['bulleted-list', 'numbered-list'].includes(n.type)
}

abstract class BaseMenu implements IButtonMenu {
abstract type: string // 'bulleted-list' / 'numbered-list'
abstract title: string
abstract iconSvg: string
tag = 'button'

private getListNode(editor: IDomEditor): Node | null {
const { type } = this
return getSelectedNodeByType(editor, type)
}

getValue(editor: IDomEditor): string | boolean {
return ''
}

isActive(editor: IDomEditor): boolean {
const node = this.getListNode(editor)
return !!node
}

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

const [nodeEntry] = Editor.nodes(editor, {
// @ts-ignore
match: n => {
// @ts-ignore
const { type = '' } = n

if (type === 'pre') return true // 代码块
if (Editor.isVoid(editor, n)) return true // void node
if (type === 'table') return true // table

return false
},
universal: true,
})

// 命中,则禁用
if (nodeEntry) return true
return false
}

/**
* 获取当前选区匹配的 type 'bulleted-list' / 'numbered-list'
* @param editor editor
*/
private getMatchListType(editor: IDomEditor): string {
const [nodeEntry] = Editor.nodes(editor, {
match: n => checkList(n),
universal: true,
})
if (nodeEntry == null) return ''
const [n] = nodeEntry
// @ts-ignore
return n.type
}

exec(editor: IDomEditor, value: string | boolean): void {
const { type } = this
const active = this.isActive(editor)

// 移除 ul ol 的父节点
Transforms.unwrapNodes(editor, {
match: n => checkList(n),
split: true,
})
// 设置当前节点 type
Transforms.setNodes(editor, {
// @ts-ignore
type: active ? 'paragraph' : 'list-item',
})

const listNode = { type, children: [] }
if (!active) {
// 非 list 设置为 list ,则外层再包裹一个 listNode
Transforms.wrapNodes(editor, listNode)
}
const matchType = this.getMatchListType(editor)
if (matchType != '' && matchType !== type) {
// list 却换 type(如 ul 切换为 ol),则外层再包裹一个 listNode
Transforms.wrapNodes(editor, listNode)
}
}
}

export default BaseMenu
15 changes: 15 additions & 0 deletions packages/basic/src/modules/list/menu/BulletedListMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @description bulleted list menu
* @author wangfupeng
*/

import BaseMenu from './BaseMenu'
import { BULLETED_LIST_SVG } from '../../_helpers/icon-svg'

class BulletedListMenu extends BaseMenu {
type = 'bulleted-list'
title = '无序列表'
iconSvg = BULLETED_LIST_SVG
}

export default BulletedListMenu
15 changes: 15 additions & 0 deletions packages/basic/src/modules/list/menu/NumberedListMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @description numbered list menu
* @author wangfupeng
*/

import BaseMenu from './BaseMenu'
import { NUMBERED_LIST_SVG } from '../../_helpers/icon-svg'

class NumberedListMenu extends BaseMenu {
type = 'numbered-list'
title = '有序列表'
iconSvg = NUMBERED_LIST_SVG
}

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

import BulletedListMenu from './BulletedListMenu'
import NumberedListMenu from './NumberedListMenu'

export const bulletedListMenuConf = {
key: 'bulletedList',
factory() {
return new BulletedListMenu()
},
}

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

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

function withList<T extends IDomEditor>(editor: T): T {
const { insertBreak } = editor
const newEditor = editor

// 重写 insertBreak
newEditor.insertBreak = () => {
// 匹配 list-item
const [nodeEntry] = Editor.nodes(newEditor, {
// @ts-ignore
match: n => n.type === 'list-item',
universal: true,
})

if (nodeEntry == null) {
// 未匹配到 list-item
insertBreak()
return
}

const [n] = nodeEntry
const listNode = DomEditor.getParentNode(newEditor, n) // 获取 list-item 的父节点,即 list 节点
const children = listNode?.children || []
const childrenLength = children.length
if (n === children[childrenLength - 1]) {
// 当前 list-item 是 list 的最后一个 child
const str = Node.string(n)
if (str === '') {
// 当前 list-item 无内容。则删除这个空白 list-item,并跳出 list ,插入一个空行
Transforms.removeNodes(newEditor, {
// @ts-ignore
match: n => n.type === 'list-item',
})

const p = { type: 'paragraph', children: [{ text: '' }] }
Transforms.insertNodes(newEditor, p, {
mode: 'highest', // 在最高层级插入,否则会插入到 list 下面
})

return // 阻止默认的 insertBreak ,重要!!!
}
}

// 其他情况,执行默认的 insertBreak()
insertBreak()
}

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

export default withList
51 changes: 51 additions & 0 deletions packages/basic/src/modules/list/render-elem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @description register formats
* @author wangfupeng
*/

import { Element as SlateElement } from 'slate'
import { jsx, VNode } from 'snabbdom'
import { IDomEditor } from '@wangeditor/core'

function genTag(type: string): string {
if (type === 'bulleted-list') return 'ul'
if (type === 'numbered-list') return 'ol'
if (type === 'list-item') return 'li'
throw new Error(`list type '${type}' is invalid`)
}

function genRenderFn(type: string) {
/**
* render header elem
* @param elemNode slate elem
* @param children children
* @param editor editor
* @returns vnode
*/
function renderHeader(
elemNode: SlateElement,
children: VNode[] | null,
editor: IDomEditor
): VNode {
const Tag = genTag(type)
const vnode = <Tag>{children}</Tag>
return vnode
}

return renderHeader
}

const renderBulletedListConf = {
type: 'bulleted-list', // 和 elemNode.type 一致
renderFn: genRenderFn('bulleted-list'),
}
const renderNumberedListConf = {
type: 'numbered-list',
renderFn: genRenderFn('numbered-list'),
}
const renderListItemConf = {
type: 'list-item',
renderFn: genRenderFn('list-item'),
}

export { renderBulletedListConf, renderNumberedListConf, renderListItemConf }
4 changes: 4 additions & 0 deletions packages/core/src/assets/textarea.less
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@
padding: 3px 5px;
width: 50px;
}

ul, ol {
padding-left: 20px;
}
}
14 changes: 14 additions & 0 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
justify,
lineHeight,
undoRedo,
list,
} from '@wangeditor/basic'
import { INDENT_RIGHT_SVG, JUSTIFY_LEFT_SVG } from './constants/svg'

Expand Down Expand Up @@ -145,6 +146,17 @@ if (undoRedo.menus && undoRedo.menus.length) {
undoRedo.menus.forEach(menuConf => registerMenu(menuConf))
}

// --------------------- 注册 list module ---------------------
if (list.renderElems && list.renderElems.length) {
list.renderElems.forEach(renderElemConf => registerRenderElemConf(renderElemConf))
}
if (list.menus && list.menus.length) {
list.menus.forEach(menuConf => registerMenu(menuConf))
}
if (list.editorPlugin) {
plugins.push(list.editorPlugin)
}

// --------------------- 创建 editor 实例 ---------------------
let editor = createEditor(
'editor-container',
Expand Down Expand Up @@ -178,6 +190,8 @@ let editor = createEditor(
iconSvg: JUSTIFY_LEFT_SVG,
menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter'],
},
'bulletedList',
'numberedList',
'|',
'insertLink',
'updateLink',
Expand Down

0 comments on commit fe6c083

Please sign in to comment.