UNPKG

wangeditor

Version:

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

851 lines (763 loc) 22.6 kB
/** * @description 封装 DOM 操作 * @wangfupeng */ import Editor from '../editor/index' import { toArray } from './util' // 记录元素基于上一个相对&绝对定位的位置信息 type OffsetDataType = { top: number left: number width: number height: number parent: Element | null } // 记录代理事件绑定 type listener = (e: Event) => void type EventItem = { elem: HTMLElement selector: string fn: listener agentFn: listener } const AGENT_EVENTS: EventItem[] = [] /** * 根据 html 字符串创建 elem * @param {String} html html */ function _createElemByHTML(html: string): HTMLElement[] { const div = document.createElement('div') div.innerHTML = html const elems = div.children return toArray(elems) } /** * 判断是否是 DOM List * @param selector DOM 元素或列表 */ function _isDOMList<T extends HTMLCollection | NodeList>(selector: unknown): selector is T { if (!selector) { return false } if (selector instanceof HTMLCollection || selector instanceof NodeList) { return true } return false } /** * 封装 querySelectorAll * @param selector css 选择器 */ function _querySelectorAll(selector: string): HTMLElement[] { const elems = document.querySelectorAll(selector) return toArray(elems) } /** * 封装 _styleArrTrim * @param styleArr css */ function _styleArrTrim(style: string | string[]): string[] { let styleArr: string[] = [] let resultArr: string[] = [] if (!Array.isArray(style)) { // 有 style,将 style 按照 `;` 拆分为数组 styleArr = style.split(';') } else { styleArr = style } styleArr.forEach(item => { // 对每项样式,按照 : 拆分为 key 和 value let arr = item.split(':').map(i => { return i.trim() }) if (arr.length === 2) { resultArr.push(arr[0] + ':' + arr[1]) } }) return resultArr } export type DomElementSelector = | string | DomElement | Document | Node | NodeList | ChildNode | ChildNode[] | Element | HTMLElement | HTMLElement[] | HTMLCollection | EventTarget | null | undefined // 构造函数 export class DomElement<T extends DomElementSelector = DomElementSelector> { // 定义属性 selector!: T length: number elems: HTMLElement[] dataSource: Map<string, any> prior?: DomElement // 通过 getNodeTop 获取顶级段落的时候,可以通过 prior 去回溯来源的子节点 /** * 构造函数 * @param selector 任一类型的选择器 */ constructor(selector: T) { // 初始化属性 this.elems = [] this.length = this.elems.length this.dataSource = new Map() if (!selector) { return } // 原本就是 DomElement 实例,则直接返回 if (selector instanceof DomElement) { return selector as DomElement<T> } let selectorResult: HTMLElement[] = [] // 存储查询结果 const nodeType = selector instanceof Node ? selector.nodeType : -1 this.selector = selector if (nodeType === 1 || nodeType === 9) { selectorResult = [selector as HTMLElement] } else if (_isDOMList(selector)) { // DOM List selectorResult = toArray(selector) } else if (selector instanceof Array) { // Element 数组(其他数据类型,暂时忽略) selectorResult = selector as HTMLElement[] } else if (typeof selector === 'string') { // 字符串 const tmpSelector = selector.replace('/\n/mg', '').trim() if (tmpSelector.indexOf('<') === 0) { // 如 <div> selectorResult = _createElemByHTML(tmpSelector) } else { // 如 #id .class selectorResult = _querySelectorAll(tmpSelector) } } const length = selectorResult.length if (!length) { // 空数组 return this } // 加入 DOM 节点 let i = 0 for (; i < length; i++) { this.elems.push(selectorResult[i]) } this.length = length } /** * 获取元素 id */ get id(): string { return this.elems[0].id } /** * 遍历所有元素,执行回调函数 * @param fn 回调函数 */ forEach(fn: (ele: HTMLElement, index?: number) => boolean | unknown): DomElement { for (let i = 0; i < this.length; i++) { const elem = this.elems[i] const result = fn.call(elem, elem, i) if (result === false) { break } } return this } /** * 克隆元素 * @param deep 是否深度克隆 */ clone(deep: boolean = false): DomElement { const cloneList: HTMLElement[] = [] this.elems.forEach(elem => { cloneList.push(elem.cloneNode(!!deep) as HTMLElement) }) return $(cloneList) } /** * 获取第几个元素 * @param index index */ get(index: number = 0): DomElement { const length = this.length if (index >= length) { index = index % length } return $(this.elems[index]) } /** * 获取第一个元素 */ first(): DomElement { return this.get(0) } /** * 获取最后一个元素 */ last(): DomElement { const length = this.length return this.get(length - 1) } /** * 绑定事件 * @param type 事件类型 * @param selector DOM 选择器 * @param fn 事件函数 */ on(type: string, fn: Function): DomElement on(type: string, selector: string, fn: Function): DomElement on(type: string, selector: string | Function, fn?: Function): DomElement { if (!type) return this // 没有 selector ,只有 type 和 fn if (typeof selector === 'function') { fn = selector selector = '' } return this.forEach(elem => { // 没有事件代理 if (!selector) { // 无代理 elem.addEventListener(type, fn as listener) return } // 有事件代理 const agentFn: listener = function (e) { const target = e.target as HTMLElement if (target.matches(selector as string)) { ;(fn as listener).call(target, e) } } elem.addEventListener(type, agentFn) // 缓存代理事件 AGENT_EVENTS.push({ elem: elem, selector: selector as string, fn: fn as listener, agentFn, }) }) } /** * 解绑事件 * @param type 事件类型 * @param selector DOM 选择器 * @param fn 事件函数 */ off(type: string, fn: Function): DomElement off(type: string, selector: string, fn: Function): DomElement off(type: string, selector: string | Function, fn?: Function): DomElement { if (!type) return this // 没有 selector ,只有 type 和 fn if (typeof selector === 'function') { fn = selector selector = '' } return this.forEach(function (elem: HTMLElement) { // 解绑事件代理 if (selector) { let idx = -1 for (let i = 0; i < AGENT_EVENTS.length; i++) { let item = AGENT_EVENTS[i] if (item.selector === selector && item.fn === fn && item.elem === elem) { idx = i break } } if (idx !== -1) { const { agentFn } = AGENT_EVENTS.splice(idx, 1)[0] elem.removeEventListener(type, agentFn) } } else { // @ts-ignore elem.removeEventListener(type, fn) } }) } /** * 设置/获取 属性 * @param key key * @param val value */ attr(key: string): string attr(key: string, val: string): DomElement attr(key: string, val?: string): DomElement | string { if (val == null) { // 获取数据 return this.elems[0].getAttribute(key) || '' } // 否则,设置属性 return this.forEach(function (elem: HTMLElement) { elem.setAttribute(key, val) }) } /** * 删除 属性 * @param key key */ removeAttr(key: string): void { this.forEach(function (elem: HTMLElement) { elem.removeAttribute(key) }) } /** * 添加 css class * @param className css class */ addClass(className?: string): DomElement { if (!className) { return this } return this.forEach(function (elem: HTMLElement) { if (elem.className) { // 当前有 class let arr: string[] = elem.className.split(/\s/) arr = arr.filter(item => { return !!item.trim() }) // 添加 class if (arr.indexOf(className) < 0) { arr.push(className) } // 修改 elem.class elem.className = arr.join(' ') } else { // 当前没有 class elem.className = className } }) } /** * 添加 css class * @param className css class */ removeClass(className?: string): DomElement { if (!className) { return this } return this.forEach(function (elem: HTMLElement) { if (!elem.className) { // 当前无 class return } let arr: string[] = elem.className.split(/\s/) arr = arr.filter(item => { item = item.trim() // 删除 class if (!item || item === className) { return false } return true }) // 修改 elem.class elem.className = arr.join(' ') }) } /** * 是否有传入的 css class * @param className css class */ hasClass(className?: string): boolean { if (!className) { return false } const elem = this.elems[0] if (!elem.className) { // 当前无 class return false } let arr: string[] = elem.className.split(/\s/) return arr.includes(className) // 是否包含 } /** * 修改 css * @param key css key * @param val css value */ // css(key: string): string css(key: string, val?: string | number): DomElement { let currentStyle: string if (val == '') { currentStyle = '' } else { currentStyle = `${key}:${val};` } return this.forEach(elem => { const style = (elem.getAttribute('style') || '').trim() if (style) { // 有 style,将 style 按照 `;` 拆分为数组 let resultArr: string[] = _styleArrTrim(style) // 替换现有的 style resultArr = resultArr.map(item => { if (item.indexOf(key) === 0) { return currentStyle } else { return item } }) // 新增 style if (currentStyle != '' && resultArr.indexOf(currentStyle) < 0) { resultArr.push(currentStyle) } // 去掉 空白 if (currentStyle == '') { resultArr = _styleArrTrim(resultArr) } // 重新设置 style elem.setAttribute('style', resultArr.join('; ')) } else { // 当前没有 style elem.setAttribute('style', currentStyle) } }) } /** * 封装 getBoundingClientRect */ getBoundingClientRect(): DOMRect { const elem = this.elems[0] return elem.getBoundingClientRect() } /** * 显示 */ show(): DomElement { return this.css('display', 'block') } /** * 隐藏 */ hide(): DomElement { return this.css('display', 'none') } /** * 获取子节点(只有 DOM 元素) */ children(): DomElement | null { const elem = this.elems[0] if (!elem) { return null } return $(elem.children) } /** * 获取子节点(包括文本节点) */ childNodes(): DomElement | null { const elem = this.elems[0] if (!elem) { return null } return $(elem.childNodes) } /** * 将子元素全部替换 * @param $children 新的child节点 */ replaceChildAll($children: DomElement): void { const parent = this.getNode() const elem = this.elems[0] while (elem.hasChildNodes()) { parent.firstChild && elem.removeChild(parent.firstChild) } this.append($children) } /** * 增加子节点 * @param $children 子节点 */ append($children: DomElement): DomElement { return this.forEach(elem => { $children.forEach(function (child: HTMLElement) { elem.appendChild(child) }) }) } /** * 移除当前节点 */ remove(): DomElement { return this.forEach(elem => { if (elem.remove) { elem.remove() } else { const parent = elem.parentElement parent && parent.removeChild(elem) } }) } /** * 当前元素,是否包含某个子元素 * @param $child 子元素 */ isContain($child: DomElement): boolean { const elem = this.elems[0] const child = $child.elems[0] return elem.contains(child) } /** * 获取当前元素 nodeName */ getNodeName(): string { const elem = this.elems[0] return elem.nodeName } /** * 根据元素位置获取元素节点(默认获取0位置的节点) * @param n 元素节点位置 */ getNode(n: number = 0): Node { let elem: Node elem = this.elems[n] return elem } /** * 查询 * @param selector css 选择器 */ find(selector: string): DomElement { const elem = this.elems[0] return $(elem.querySelectorAll(selector)) } /** * 获取/设置 元素 text * @param val text 值 */ text(): string text(val: string): DomElement text(val?: string): DomElement | string { if (!val) { // 获取 text const elem = this.elems[0] return elem.innerHTML.replace(/<[^>]+>/g, () => '') } else { // 设置 text return this.forEach(function (elem: HTMLElement) { elem.innerHTML = val }) } } /** * 设置/获取 元素 html * @param val html 值 */ html(): string html(val: string): DomElement html(val?: string): DomElement | string { const elem = this.elems[0] if (!val) { // 获取 html return elem.innerHTML } else { // 设置 html elem.innerHTML = val return this } } /** * 获取元素 value */ val(): string { const elem = this.elems[0] return (elem as any).value.trim() // 暂用 any } /** * focus 到当前元素 */ focus(): DomElement { return this.forEach(elem => { elem.focus() }) } /** * 当前元素前一个兄弟节点 */ prev(): DomElement { const elem = this.elems[0] return $(elem.previousElementSibling) } /** * 当前元素后一个兄弟节点 * 不包括文本节点、注释节点) */ next(): DomElement { const elem = this.elems[0] return $(elem.nextElementSibling) } /** * 获取当前节点的下一个兄弟节点 * 包括文本节点、注释节点即回车、换行、空格、文本等等) */ getNextSibling(): DomElement { const elem = this.elems[0] return $(elem.nextSibling) } /** * 获取父元素 */ parent(): DomElement { const elem = this.elems[0] return $(elem.parentElement) } /** * 查找父元素,直到满足 selector 条件 * @param selector css 选择器 * @param curElem 从哪个元素开始查找,默认为当前元素 */ parentUntil(selector: string, curElem?: HTMLElement): DomElement | null { const elem = curElem || this.elems[0] if (elem.nodeName === 'BODY') { return null } const parent = elem.parentElement if (parent === null) { return null } if (parent.matches(selector)) { // 找到,并返回 return $(parent) } // 继续查找,递归 return this.parentUntil(selector, parent) } /** * 查找父元素,直到满足 selector 条件,或者 到达 编辑区域容器以及菜单栏容器 * @param selector css 选择器 * @param curElem 从哪个元素开始查找,默认为当前元素 */ parentUntilEditor(selector: string, editor: Editor, curElem?: HTMLElement): DomElement | null { const elem = curElem || this.elems[0] if ($(elem).equal(editor.$textContainerElem) || $(elem).equal(editor.$toolbarElem)) { return null } const parent = elem.parentElement if (parent === null) { return null } if (parent.matches(selector)) { // 找到,并返回 return $(parent) } // 继续查找,递归 return this.parentUntilEditor(selector, editor, parent) } /** * 判读是否相等 * @param $elem 元素 */ equal($elem: DomElement | HTMLElement): boolean { if ($elem instanceof DomElement) { return this.elems[0] === $elem.elems[0] } else if ($elem instanceof HTMLElement) { return this.elems[0] === $elem } else { return false } } /** * 将该元素插入到某个元素前面 * @param selector css 选择器 */ insertBefore(selector: string | DomElement): DomElement { const $referenceNode = $(selector) const referenceNode = $referenceNode.elems[0] if (!referenceNode) { return this } return this.forEach(elem => { const parent = referenceNode.parentNode as Node parent?.insertBefore(elem, referenceNode) }) } /** * 将该元素插入到selector元素后面 * @param selector css 选择器 */ insertAfter(selector: string | DomElement): DomElement { const $referenceNode = $(selector) const referenceNode = $referenceNode.elems[0] const anchorNode = referenceNode && referenceNode.nextSibling if (!referenceNode) { return this } return this.forEach(function (elem: HTMLElement) { const parent = referenceNode.parentNode as Node if (anchorNode) { parent.insertBefore(elem, anchorNode) } else { parent.appendChild(elem) } }) } /** * 设置/获取 数据 * @param key key * @param value value */ data<T>(key: string, value?: T): T | undefined { if (value != null) { // 设置数据 this.dataSource.set(key, value) } else { // 获取数据 return this.dataSource.get(key) } } /** * 获取当前节点的顶级(段落) * @param editor 富文本实例 */ getNodeTop(editor: Editor): DomElement { // 异常抛出,空的 DomElement 直接返回 if (this.length < 1) { return this } // 获取父级元素,并判断是否是 编辑区域 // 如果是则返回当前节点 const $parent = this.parent() // fix:添加当前元素与编辑区元素的比较,防止传入的当前元素就是编辑区元素而造成的获取顶级元素为空的情况 if (editor.$textElem.equal(this) || editor.$textElem.equal($parent)) { return this } // 到了此处,即代表当前节点不是顶级段落 // 将当前节点存放于父节点的 prior 字段下 // 主要用于 回溯 子节点 // 例如:ul ol 等标签 // 实际操作的节点是 li 但是一个 ul ol 的子节点可能有多个 // 所以需要对其进行 回溯 找到对应的子节点 $parent.prior = this return $parent.getNodeTop(editor) } /** * 获取当前 节点 基与上一个拥有相对或者解决定位的父容器的位置 * @param editor 富文本实例 */ getOffsetData(): OffsetDataType { const $node = this.elems[0] return { top: $node.offsetTop, left: $node.offsetLeft, width: $node.offsetWidth, height: $node.offsetHeight, parent: $node.offsetParent, } } /** * 从上至下进行滚动 * @param top 滚动的值 */ scrollTop(top: number): void { const $node = this.elems[0] $node.scrollTo({ top }) } } // new 一个对象 function $(...arg: ConstructorParameters<typeof DomElement>): DomElement { return new DomElement(...arg) } export default $