refactor: list

This commit is contained in:
UOrb 2021-01-06 23:11:29 +08:00
parent 0408181e81
commit 3b1152303f
25 changed files with 1112 additions and 76 deletions

View File

@ -54,7 +54,7 @@ class SelectionRangeTopNodes {
* @param $node
*/
private getNextSibling($node: DomElement): DomElement {
return $($node.elems[0].nextSibling)
return $($node.elems[0].nextElementSibling)
}
/**

View File

@ -238,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()
}

View File

@ -52,7 +52,7 @@ class BackColor extends DropListMenu implements MenuActive {
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])
}

View File

@ -51,7 +51,7 @@ class FontColor extends DropListMenu implements MenuActive {
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])
}

View File

@ -48,7 +48,7 @@ class FontSize extends DropListMenu implements MenuActive {
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])
}

View File

@ -48,7 +48,7 @@ class FontStyle extends DropListMenu implements MenuActive {
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])
}

View File

@ -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)

View File

@ -79,7 +79,7 @@ class Justify extends DropListMenu implements MenuActive {
selection.saveRange()
// 获取顶级元素
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
const $elems = editor.selection.getSelectionRangeTopNodes()
if ($selectionElem?.length) {
// list 在chrome下默认多包裹一个 p导致不能通过顶层元素判断所以单独加个判断
if (this.isSpecialNode($selectionElem, $elems[0]) || this.isSpecialTopNode($elems[0])) {
@ -148,7 +148,7 @@ class Justify extends DropListMenu implements MenuActive {
* ,active进行高亮否则unActive
* ?
*/
public tryChangeActive(): void {}
public tryChangeActive(): void { }
}
export default Justify

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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())
}
}

View File

@ -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

View File

@ -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,69 +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())
// 获取选区范围内的顶级 DOM 元素
this.handleSelectionRangeNodes(type)
if (!$selectionElem?.length) return
let $dom = $($selectionElem.elems[0]).parentUntil('TABLE', $selectionElem.elems[0])
if ($dom && $($dom.elems[0]).getNodeName() === 'TABLE') {
return
}
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

131
src/menus/list/utils.ts Normal file
View File

@ -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
}

View File

@ -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') {

View File

@ -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> ,要用其他方式兼容
@ -78,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 {
@ -93,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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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()
})