UNPKG

wangeditor

Version:

wangEditor - 轻量级 web 富文本编辑器,配置方便,使用简单,开源免费

239 lines (228 loc) 8.59 kB
/** * @description 标题 * @author wangfupeng */ import DropListMenu from '../menu-constructors/DropListMenu' import $, { DomElement } from '../../utils/dom-core' import Editor from '../../editor/index' import { MenuActive } from '../menu-constructors/Menu' import { getRandomCode } from '../../utils/util' import { TCatalog } from '../../config/events' import { EMPTY_P } from '../../utils/const' class Head extends DropListMenu implements MenuActive { oldCatalogs: TCatalog[] | undefined constructor(editor: Editor) { const $elem = $( '<div class="w-e-menu" data-title="标题"><i class="w-e-icon-header"></i></div>' ) const dropListConf = { width: 100, title: '设置标题', type: 'list', // droplist 以列表形式展示 list: [ { $elem: $('<h1>H1</h1>'), value: '<h1>' }, { $elem: $('<h2>H2</h2>'), value: '<h2>' }, { $elem: $('<h3>H3</h3>'), value: '<h3>' }, { $elem: $('<h4>H4</h4>'), value: '<h4>' }, { $elem: $('<h5>H5</h5>'), value: '<h5>' }, { $elem: $(`<p>${editor.i18next.t('menus.dropListMenu.head.正文')}</p>`), value: '<p>', }, ], clickHandler: (value: string) => { // 注意 this 是指向当前的 Head 对象 this.command(value) }, } super($elem, editor, dropListConf) const onCatalogChange = editor.config.onCatalogChange // 未配置目录change监听回调时不运行下面操作 if (onCatalogChange) { this.oldCatalogs = [] this.addListenerCatalog() // 监听文本框编辑时的大纲信息 this.getCatalogs() // 初始有值的情况获取一遍大纲信息 } } /** * 执行命令 * @param value value */ public command(value: string): void { const editor = this.editor const $selectionElem = editor.selection.getSelectionContainerElem() if ($selectionElem && editor.$textElem.equal($selectionElem)) { // 不能选中多行来设置标题,否则会出现问题 // 例如选中的是 <p>xxx</p><p>yyy</p> 来设置标题,设置之后会成为 <h1>xxx<br>yyy</h1> 不符合预期 this.setMultilineHead(value) } else { // 选中内容包含序列,code,表格,分割线时不处理 if ( ['OL', 'UL', 'LI', 'TABLE', 'TH', 'TR', 'CODE', 'HR'].indexOf( $($selectionElem).getNodeName() ) > -1 ) { return } editor.cmd.do('formatBlock', value) } // 标题设置成功且不是<p>正文标签就配置大纲id value !== '<p>' && this.addUidForSelectionElem() } /** * 为标题设置大纲 */ private addUidForSelectionElem() { const editor = this.editor const tag = editor.selection.getSelectionContainerElem() const id = getRandomCode() // 默认五位数id $(tag).attr('id', id) } /** * 监听change事件来返回大纲信息 */ private addListenerCatalog() { const editor = this.editor editor.txt.eventHooks.changeEvents.push(() => { this.getCatalogs() }) } /** * 获取大纲数组 */ private getCatalogs() { const editor = this.editor const $textElem = this.editor.$textElem const onCatalogChange = editor.config.onCatalogChange const elems = $textElem.find('h1,h2,h3,h4,h5') const catalogs: TCatalog[] = [] elems.forEach((elem, index) => { const $elem = $(elem) let id = $elem.attr('id') const tag = $elem.getNodeName() const text = $elem.text() if (!id) { id = getRandomCode() $elem.attr('id', id) } // 标题为空的情况不生成目录 if (!text) return catalogs.push({ tag, id, text, }) }) // 旧目录和新目录对比是否相等,不相等则运行回调并保存新目录到旧目录变量,以方便下一次对比 if (JSON.stringify(this.oldCatalogs) !== JSON.stringify(catalogs)) { this.oldCatalogs = catalogs onCatalogChange && onCatalogChange(catalogs) } } /** * 设置选中的多行标题 * @param value 需要执行的命令值 */ private setMultilineHead(value: string) { const editor = this.editor const $selection = editor.selection // 初始选区的父节点 const containerElem = $selection.getSelectionContainerElem()?.elems[0]! // 白名单:用户选区里如果有该元素则不进行转换 const _WHITE_LIST = [ 'IMG', 'VIDEO', 'TABLE', 'TH', 'TR', 'UL', 'OL', 'PRE', 'HR', 'BLOCKQUOTE', ] // 获取选中的首、尾元素 const startElem = $($selection.getSelectionStartElem()) let endElem = $($selection.getSelectionEndElem()) // 判断用户选中元素是否为最后一个空元素,如果是将endElem指向上一个元素 if ( endElem.elems[0].outerHTML === $(EMPTY_P).elems[0].outerHTML && !endElem.elems[0].nextSibling ) { endElem = endElem.prev()! } // 存放选中的所有元素 const cacheDomList: DomElement[] = [] cacheDomList.push(startElem.getNodeTop(editor)) // 选中首尾元素在父级下的坐标 const indexList: number[] = [] // 选区共同祖先元素的所有子节点 const childList = $selection.getRange()?.commonAncestorContainer.childNodes // 找到选区的首尾元素的下标,方便最后恢复选区 childList?.forEach((item, index) => { if (item === cacheDomList[0].getNode()) { indexList.push(index) } if (item === endElem.getNodeTop(editor).getNode()) { indexList.push(index) } }) // 找到首尾元素中间所包含的所有dom let i = 0 // 数组中的当前元素不等于选区最后一个节点时循环寻找中间节点 while (cacheDomList[i].getNode() !== endElem.getNodeTop(editor).getNode()) { // 严谨性判断,是否元素为空 if (!cacheDomList[i].elems[0]) return let d = $(cacheDomList[i].next().getNode()) cacheDomList.push(d) i++ } // 将选区内的所有子节点进行遍历生成对应的标签 cacheDomList?.forEach((_node, index) => { // 判断元素是否含有白名单内的标签元素 if (!this.hasTag(_node, _WHITE_LIST)) { const $h = $(value) const $parentNode = _node.parent().getNode() // 设置标签内容 $h.html(`${_node.html()}`) // 插入生成的新标签 $parentNode.insertBefore($h.getNode(), _node.getNode()) // 移除原有的标签 _node.remove() } }) // 重新设置选区起始位置,保留拖蓝区域 $selection.createRangeByElems( containerElem.children[indexList[0]], containerElem.children[indexList[1]] ) } /** * 是否含有某元素 * @param elem 需要检查的元素 * @param whiteList 白名单 */ private hasTag(elem: DomElement, whiteList: string[]): boolean { if (!elem) return false if (whiteList.includes(elem?.getNodeName())) return true let _flag = false elem.children()?.forEach(child => { _flag = this.hasTag($(child), whiteList) }) return _flag } /** * 尝试改变菜单激活(高亮)状态 */ public tryChangeActive() { const editor = this.editor const reg = /^h/i const cmdValue = editor.cmd.queryCommandValue('formatBlock') if (reg.test(cmdValue)) { this.active() } else { this.unActive() } } } export default Head