/** * Range封装 * @file * @module UE.dom * @class Range * @since 1.2.6.1 */ /** * dom操作封装 * @unfile * @module UE.dom */ /** * Range实现类,本类是UEditor底层核心类,封装不同浏览器之间的Range操作。 * @unfile * @module UE.dom * @class Range */ (function() { var guid = 0, fillChar = domUtils.fillChar, fillData; /** * 更新range的collapse状态 * @param {Range} range range对象 */ function updateCollapse(range) { range.collapsed = range.startContainer && range.endContainer && range.startContainer === range.endContainer && range.startOffset == range.endOffset; } function selectOneNode(rng) { return ( !rng.collapsed && rng.startContainer.nodeType == 1 && rng.startContainer === rng.endContainer && rng.endOffset - rng.startOffset == 1 ); } function setEndPoint(toStart, node, offset, range) { //如果node是自闭合标签要处理 if ( node.nodeType == 1 && (dtd.$empty[node.tagName] || dtd.$nonChild[node.tagName]) ) { offset = domUtils.getNodeIndex(node) + (toStart ? 0 : 1); node = node.parentNode; } if (toStart) { range.startContainer = node; range.startOffset = offset; if (!range.endContainer) { range.collapse(true); } } else { range.endContainer = node; range.endOffset = offset; if (!range.startContainer) { range.collapse(false); } } updateCollapse(range); return range; } function execContentsAction(range, action) { //调整边界 //range.includeBookmark(); var start = range.startContainer, end = range.endContainer, startOffset = range.startOffset, endOffset = range.endOffset, doc = range.document, frag = doc.createDocumentFragment(), tmpStart, tmpEnd; if (start.nodeType == 1) { start = start.childNodes[startOffset] || (tmpStart = start.appendChild(doc.createTextNode(""))); } if (end.nodeType == 1) { end = end.childNodes[endOffset] || (tmpEnd = end.appendChild(doc.createTextNode(""))); } if (start === end && start.nodeType == 3) { frag.appendChild( doc.createTextNode( start.substringData(startOffset, endOffset - startOffset) ) ); //is not clone if (action) { start.deleteData(startOffset, endOffset - startOffset); range.collapse(true); } return frag; } var current, currentLevel, clone = frag, startParents = domUtils.findParents(start, true), endParents = domUtils.findParents(end, true); for (var i = 0; startParents[i] == endParents[i]; ) { i++; } for (var j = i, si; (si = startParents[j]); j++) { current = si.nextSibling; if (si == start) { if (!tmpStart) { if (range.startContainer.nodeType == 3) { clone.appendChild( doc.createTextNode(start.nodeValue.slice(startOffset)) ); //is not clone if (action) { start.deleteData( startOffset, start.nodeValue.length - startOffset ); } } else { clone.appendChild(!action ? start.cloneNode(true) : start); } } } else { currentLevel = si.cloneNode(false); clone.appendChild(currentLevel); } while (current) { if (current === end || current === endParents[j]) { break; } si = current.nextSibling; clone.appendChild(!action ? current.cloneNode(true) : current); current = si; } clone = currentLevel; } clone = frag; if (!startParents[i]) { clone.appendChild(startParents[i - 1].cloneNode(false)); clone = clone.firstChild; } for (var j = i, ei; (ei = endParents[j]); j++) { current = ei.previousSibling; if (ei == end) { if (!tmpEnd && range.endContainer.nodeType == 3) { clone.appendChild( doc.createTextNode(end.substringData(0, endOffset)) ); //is not clone if (action) { end.deleteData(0, endOffset); } } } else { currentLevel = ei.cloneNode(false); clone.appendChild(currentLevel); } //如果两端同级,右边第一次已经被开始做了 if (j != i || !startParents[i]) { while (current) { if (current === start) { break; } ei = current.previousSibling; clone.insertBefore( !action ? current.cloneNode(true) : current, clone.firstChild ); current = ei; } } clone = currentLevel; } if (action) { range .setStartBefore( !endParents[i] ? endParents[i - 1] : !startParents[i] ? startParents[i - 1] : endParents[i] ) .collapse(true); } tmpStart && domUtils.remove(tmpStart); tmpEnd && domUtils.remove(tmpEnd); return frag; } /** * 创建一个跟document绑定的空的Range实例 * @constructor * @param { Document } document 新建的选区所属的文档对象 */ /** * @property { Node } startContainer 当前Range的开始边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } startOffset 当前Range的开始边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Node } endContainer 当前Range的结束边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } endOffset 当前Range的结束边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Boolean } collapsed 当前Range是否闭合 * @default true * @remind Range是闭合的时候, startContainer === endContainer && startOffset === endOffset */ /** * @property { Document } document 当前Range所属的Document对象 * @remind 不同range的的document属性可以是不同的 */ var Range = (dom.Range = function(document) { var me = this; me.startContainer = me.startOffset = me.endContainer = me.endOffset = null; me.document = document; me.collapsed = true; }); /** * 删除fillData * @param doc * @param excludeNode */ function removeFillData(doc, excludeNode) { try { if (fillData && domUtils.inDoc(fillData, doc)) { if (!fillData.nodeValue.replace(fillCharReg, "").length) { var tmpNode = fillData.parentNode; domUtils.remove(fillData); while ( tmpNode && domUtils.isEmptyInlineElement(tmpNode) && //safari的contains有bug (browser.safari ? !( domUtils.getPosition(tmpNode, excludeNode) & domUtils.POSITION_CONTAINS ) : !tmpNode.contains(excludeNode)) ) { fillData = tmpNode.parentNode; domUtils.remove(tmpNode); tmpNode = fillData; } } else { fillData.nodeValue = fillData.nodeValue.replace(fillCharReg, ""); } } } catch (e) {} } /** * @param node * @param dir */ function mergeSibling(node, dir) { var tmpNode; node = node[dir]; while (node && domUtils.isFillChar(node)) { tmpNode = node[dir]; domUtils.remove(node); node = tmpNode; } } Range.prototype = { /** * 克隆选区的内容到一个DocumentFragment里 * @method cloneContents * @return { DocumentFragment | NULL } 如果选区是闭合的将返回null, 否则, 返回包含所clone内容的DocumentFragment元素 * @example * ```html *
* * xx[xxx]x * * * * ``` */ cloneContents: function() { return this.collapsed ? null : execContentsAction(this, 0); }, /** * 删除当前选区范围中的所有内容 * @method deleteContents * @remind 执行完该操作后, 当前Range对象变成了闭合状态 * @return { UE.dom.Range } 当前操作的Range对象 * @example * ```html * * * xx[xxx]x * * * * ``` */ deleteContents: function() { var txt; if (!this.collapsed) { execContentsAction(this, 1); } if (browser.webkit) { txt = this.startContainer; if (txt.nodeType == 3 && !txt.nodeValue.length) { this.setStartBefore(txt).collapse(true); domUtils.remove(txt); } } return this; }, /** * 将当前选区的内容提取到一个DocumentFragment里 * @method extractContents * @remind 执行该操作后, 选区将变成闭合状态 * @warning 执行该操作后, 原来选区所选中的内容将从dom树上剥离出来 * @return { DocumentFragment } 返回包含所提取内容的DocumentFragment对象 * @example * ```html * * * xx[xxx]x * * * */ extractContents: function() { return this.collapsed ? null : execContentsAction(this, 2); }, /** * 设置Range的开始容器节点和偏移量 * @method setStart * @remind 如果给定的节点是元素节点,那么offset指的是其子元素中索引为offset的元素, * 如果是文本节点,那么offset指的是其文本内容的第offset个字符 * @remind 如果提供的容器节点是一个不能包含子元素的节点, 则该选区的开始容器将被设置 * 为该节点的父节点, 此时, 其距离开始容器的偏移量也变成了该节点在其父节点 * 中的索引 * @param { Node } node 将被设为当前选区开始边界容器的节点对象 * @param { int } offset 选区的开始位置偏移量 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxxxxxxxxxx[xxx] * * * ``` * @example * ```html * * xxxxxxx[xxxx]x
==> range.applyInlineStyle("strong") ==>xxxx[xxxx]x
* ``` */ /** * 给range选区中的内容添加给定的inline标签, 并且为标签附加上一些初始化属性。 * @method applyInlineStyle * @param { String } tagName 需要添加的标签名 * @param { Object } attrs 跟随新添加的标签的属性 * @return { UE.dom.Range } 当前选区 * @example * ```html *xxxx[xxxx]x
* * ==> * * * range.applyInlineStyle("strong",{"style":"font-size:12px"}) * * ==> * *xxxx[xxxx]x
* ``` */ applyInlineStyle: function(tagName, attrs, list) { if (this.collapsed) return this; this.trimBoundary() .enlarge(false, function(node) { return node.nodeType == 1 && domUtils.isBlockElm(node); }) .adjustmentBoundary(); var bookmark = this.createBookmark(), end = bookmark.end, filterFn = function(node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != "br" : !domUtils.isWhitespace(node); }, current = domUtils.getNextDomNode(bookmark.start, false, filterFn), node, pre, range = this.cloneRange(); while ( current && domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING ) { if (current.nodeType == 3 || dtd[tagName][current.tagName]) { range.setStartBefore(current); node = current; while ( node && (node.nodeType == 3 || dtd[tagName][node.tagName]) && node !== end ) { pre = node; node = domUtils.getNextDomNode( node, node.nodeType == 1, null, function(parent) { return dtd[tagName][parent.tagName]; } ); } var frag = range.setEndAfter(pre).extractContents(), elm; if (list && list.length > 0) { var level, top; top = level = list[0].cloneNode(false); for (var i = 1, ci; (ci = list[i++]); ) { level.appendChild(ci.cloneNode(false)); level = level.firstChild; } elm = level; } else { elm = range.document.createElement(tagName); } if (attrs) { domUtils.setAttributes(elm, attrs); } elm.appendChild(frag); //针对嵌套span的全局样式指定,做容错处理 if (elm.tagName == "SPAN" && attrs && attrs.style) { utils.each(elm.getElementsByTagName("span"), function(s) { s.style.cssText = s.style.cssText + ";" + attrs.style; }); } range.insertNode(list ? top : elm); //处理下滑线在a上的情况 var aNode; if ( tagName == "span" && attrs.style && /text\-decoration/.test(attrs.style) && (aNode = domUtils.findParentByTagName(elm, "a", true)) ) { domUtils.setAttributes(aNode, attrs); domUtils.remove(elm, true); elm = aNode; } else { domUtils.mergeSibling(elm); domUtils.clearEmptySibling(elm); } //去除子节点相同的 domUtils.mergeChild(elm, attrs); current = domUtils.getNextDomNode(elm, false, filterFn); domUtils.mergeToParent(elm); if (node === end) { break; } } else { current = domUtils.getNextDomNode(current, true, filterFn); } } return this.moveToBookmark(bookmark); }, /** * 移除当前选区内指定的inline标签,但保留其中的内容 * @method removeInlineStyle * @param { String } tagName 需要移除的标签名 * @return { UE.dom.Range } 当前的range对象 * @example * ```html * xx[xxxxyyyzz]z => range.removeInlineStyle(["em"]) => xx[xxxxyyyzz]z * ``` */ /** * 移除当前选区内指定的一组inline标签,但保留其中的内容 * @method removeInlineStyle * @param { Array } tagNameArr 需要移除的标签名的数组 * @return { UE.dom.Range } 当前的range对象 * @see UE.dom.Range:removeInlineStyle(String) */ removeInlineStyle: function(tagNames) { if (this.collapsed) return this; tagNames = utils.isArray(tagNames) ? tagNames : [tagNames]; this.shrinkBoundary().adjustmentBoundary(); var start = this.startContainer, end = this.endContainer; while (1) { if (start.nodeType == 1) { if (utils.indexOf(tagNames, start.tagName.toLowerCase()) > -1) { break; } if (start.tagName.toLowerCase() == "body") { start = null; break; } } start = start.parentNode; } while (1) { if (end.nodeType == 1) { if (utils.indexOf(tagNames, end.tagName.toLowerCase()) > -1) { break; } if (end.tagName.toLowerCase() == "body") { end = null; break; } } end = end.parentNode; } var bookmark = this.createBookmark(), frag, tmpRange; if (start) { tmpRange = this.cloneRange() .setEndBefore(bookmark.start) .setStartBefore(start); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(start, true); start.parentNode.insertBefore(bookmark.start, start); } if (end) { tmpRange = this.cloneRange() .setStartAfter(bookmark.end) .setEndAfter(end); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(end, false, true); end.parentNode.insertBefore(bookmark.end, end.nextSibling); } var current = domUtils.getNextDomNode(bookmark.start, false, function( node ) { return node.nodeType == 1; }), next; while (current && current !== bookmark.end) { next = domUtils.getNextDomNode(current, true, function(node) { return node.nodeType == 1; }); if (utils.indexOf(tagNames, current.tagName.toLowerCase()) > -1) { domUtils.remove(current, true); } current = next; } return this.moveToBookmark(bookmark); }, /** * 获取当前选中的自闭合的节点 * @method getClosedNode * @return { Node | NULL } 如果当前选中的是自闭合节点, 则返回该节点, 否则返回NULL */ getClosedNode: function() { var node; if (!this.collapsed) { var range = this.cloneRange().adjustmentBoundary().shrinkBoundary(); if (selectOneNode(range)) { var child = range.startContainer.childNodes[range.startOffset]; if ( child && child.nodeType == 1 && (dtd.$empty[child.tagName] || dtd.$nonChild[child.tagName]) ) { node = child; } } } return node; }, /** * 在页面上高亮range所表示的选区 * @method select * @return { UE.dom.Range } 返回当前Range对象 */ //这里不区分ie9以上,trace:3824 select: browser.ie ? function(noFillData, textRange) { var nativeRange; if (!this.collapsed) this.shrinkBoundary(); var node = this.getClosedNode(); if (node && !textRange) { try { nativeRange = this.document.body.createControlRange(); nativeRange.addElement(node); nativeRange.select(); } catch (e) {} return this; } var bookmark = this.createBookmark(), start = bookmark.start, end; nativeRange = this.document.body.createTextRange(); nativeRange.moveToElementText(start); nativeRange.moveStart("character", 1); if (!this.collapsed) { var nativeRangeEnd = this.document.body.createTextRange(); end = bookmark.end; nativeRangeEnd.moveToElementText(end); nativeRange.setEndPoint("EndToEnd", nativeRangeEnd); } else { if (!noFillData && this.startContainer.nodeType != 3) { //使用|x固定住光标 var tmpText = this.document.createTextNode(fillChar), tmp = this.document.createElement("span"); tmp.appendChild(this.document.createTextNode(fillChar)); start.parentNode.insertBefore(tmp, start); start.parentNode.insertBefore(tmpText, start); //当点b,i,u时,不能清除i上边的b removeFillData(this.document, tmpText); fillData = tmpText; mergeSibling(tmp, "previousSibling"); mergeSibling(start, "nextSibling"); nativeRange.moveStart("character", -1); nativeRange.collapse(true); } } this.moveToBookmark(bookmark); tmp && domUtils.remove(tmp); //IE在隐藏状态下不支持range操作,catch一下 try { nativeRange.select(); } catch (e) {} return this; } : function(notInsertFillData) { function checkOffset(rng) { function check(node, offset, dir) { if (node.nodeType == 3 && node.nodeValue.length < offset) { rng[dir + "Offset"] = node.nodeValue.length; } } check(rng.startContainer, rng.startOffset, "start"); check(rng.endContainer, rng.endOffset, "end"); } var win = domUtils.getWindow(this.document), sel = win.getSelection(), txtNode; //FF下关闭自动长高时滚动条在关闭dialog时会跳 //ff下如果不body.focus将不能定位闭合光标到编辑器内 browser.gecko ? this.document.body.focus() : win.focus(); if (sel) { sel.removeAllRanges(); // trace:870 chrome/safari后边是br对于闭合得range不能定位 所以去掉了判断 // this.startContainer.nodeType != 3 &&! ((child = this.startContainer.childNodes[this.startOffset]) && child.nodeType == 1 && child.tagName == 'BR' if (this.collapsed && !notInsertFillData) { // //opear如果没有节点接着,原生的不能够定位,不能在body的第一级插入空白节点 // if (notInsertFillData && browser.opera && !domUtils.isBody(this.startContainer) && this.startContainer.nodeType == 1) { // var tmp = this.document.createTextNode(''); // this.insertNode(tmp).setStart(tmp, 0).collapse(true); // } // //处理光标落在文本节点的情况 //处理以下的情况 //|xxxx //xxxx|xxxx //xxxx| var start = this.startContainer, child = start; if (start.nodeType == 1) { child = start.childNodes[this.startOffset]; } if ( !(start.nodeType == 3 && this.startOffset) && (child ? !child.previousSibling || child.previousSibling.nodeType != 3 : !start.lastChild || start.lastChild.nodeType != 3) ) { txtNode = this.document.createTextNode(fillChar); //跟着前边走 this.insertNode(txtNode); removeFillData(this.document, txtNode); mergeSibling(txtNode, "previousSibling"); mergeSibling(txtNode, "nextSibling"); fillData = txtNode; this.setStart(txtNode, browser.webkit ? 1 : 0).collapse(true); } } var nativeRange = this.document.createRange(); if ( this.collapsed && browser.opera && this.startContainer.nodeType == 1 ) { var child = this.startContainer.childNodes[this.startOffset]; if (!child) { //往前靠拢 child = this.startContainer.lastChild; if (child && domUtils.isBr(child)) { this.setStartBefore(child).collapse(true); } } else { //向后靠拢 while (child && domUtils.isBlockElm(child)) { if (child.nodeType == 1 && child.childNodes[0]) { child = child.childNodes[0]; } else { break; } } child && this.setStartBefore(child).collapse(true); } } //是createAddress最后一位算的不准,现在这里进行微调 checkOffset(this); nativeRange.setStart(this.startContainer, this.startOffset); nativeRange.setEnd(this.endContainer, this.endOffset); sel.addRange(nativeRange); } return this; }, /** * 滚动到当前range开始的位置 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @return { UE.dom.Range } 当前Range对象 */ /** * 滚动到距离当前range开始位置 offset 的位置处 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @param { Number } offset 距离range开始位置处的偏移量, 如果为正数, 则向下偏移, 反之, 则向上偏移 * @return { UE.dom.Range } 当前Range对象 */ scrollToView: function(win, offset) { win = win ? window : domUtils.getWindow(this.document); var me = this, span = me.document.createElement("span"); //trace:717 span.innerHTML = " "; me.cloneRange().insertNode(span); domUtils.scrollToView(span, win, offset); domUtils.remove(span); return me; }, /** * 判断当前选区内容是否占位符 * @private * @method inFillChar * @return { Boolean } 如果是占位符返回true,否则返回false */ inFillChar: function() { var start = this.startContainer; if ( this.collapsed && start.nodeType == 3 && start.nodeValue.replace(new RegExp("^" + domUtils.fillChar), "") .length + 1 == start.nodeValue.length ) { return true; } return false; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * ** aaaa * * * bbbb * * *
* * * * ``` */ createAddress: function(ignoreEnd, ignoreTxt) { var addr = {}, me = this; function getAddress(isStart) { var node = isStart ? me.startContainer : me.endContainer; var parents = domUtils.findParents(node, true, function(node) { return !domUtils.isBody(node); }), addrs = []; for (var i = 0, ci; (ci = parents[i++]); ) { addrs.push(domUtils.getNodeIndex(ci, ignoreTxt)); } var firstIndex = 0; if (ignoreTxt) { if (node.nodeType == 3) { var tmpNode = node.previousSibling; while (tmpNode && tmpNode.nodeType == 3) { firstIndex += tmpNode.nodeValue.replace(fillCharReg, "").length; tmpNode = tmpNode.previousSibling; } firstIndex += isStart ? me.startOffset : me.endOffset; // - (fillCharReg.test(node.nodeValue) ? 1 : 0 ) } else { node = node.childNodes[isStart ? me.startOffset : me.endOffset]; if (node) { firstIndex = domUtils.getNodeIndex(node, ignoreTxt); } else { node = isStart ? me.startContainer : me.endContainer; var first = node.firstChild; while (first) { if (domUtils.isFillChar(first)) { first = first.nextSibling; continue; } firstIndex++; if (first.nodeType == 3) { while (first && first.nodeType == 3) { first = first.nextSibling; } } else { first = first.nextSibling; } } } } } else { firstIndex = isStart ? domUtils.isFillChar(node) ? 0 : me.startOffset : me.endOffset; } if (firstIndex < 0) { firstIndex = 0; } addrs.push(firstIndex); return addrs; } addr.startAddress = getAddress(true); if (!ignoreEnd) { addr.endAddress = me.collapsed ? [].concat(addr.startAddress) : getAddress(); } return addr; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * ** aaaa * * * bbbb * * *
* * * * ``` */ moveToAddress: function(addr, ignoreEnd) { var me = this; function getNode(address, isStart) { var tmpNode = me.document.body, parentNode, offset; for (var i = 0, ci, l = address.length; i < l; i++) { ci = address[i]; parentNode = tmpNode; tmpNode = tmpNode.childNodes[ci]; if (!tmpNode) { offset = ci; break; } } if (isStart) { if (tmpNode) { me.setStartBefore(tmpNode); } else { me.setStart(parentNode, offset); } } else { if (tmpNode) { me.setEndBefore(tmpNode); } else { me.setEnd(parentNode, offset); } } } getNode(addr.startAddress, true); !ignoreEnd && addr.endAddress && getNode(addr.endAddress); return me; }, /** * 判断给定的Range对象是否和当前Range对象表示的是同一个选区 * @method equals * @param { UE.dom.Range } 需要判断的Range对象 * @return { Boolean } 如果给定的Range对象与当前Range对象表示的是同一个选区, 则返回true, 否则返回false */ equals: function(rng) { for (var p in this) { if (this.hasOwnProperty(p)) { if (this[p] !== rng[p]) return false; } } return true; }, /** * 遍历range内的节点。每当遍历一个节点时, 都会执行参数项 doFn 指定的函数, 该函数的接受当前遍历的节点 * 作为其参数。 * @method traversal * @param { Function } doFn 对每个遍历的节点要执行的方法, 该方法接受当前遍历的节点作为其参数 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * * * * * * * * * * ``` */ /** * 遍历range内的节点。 * 每当遍历一个节点时, 都会执行参数项 doFn 指定的函数, 该函数的接受当前遍历的节点 * 作为其参数。 * 可以通过参数项 filterFn 来指定一个过滤器, 只有符合该过滤器过滤规则的节点才会触 * 发doFn函数的执行 * @method traversal * @param { Function } doFn 对每个遍历的节点要执行的方法, 该方法接受当前遍历的节点作为其参数 * @param { Function } filterFn 过滤器, 该函数接受当前遍历的节点作为参数, 如果该节点满足过滤 * 规则, 请返回true, 该节点会触发doFn, 否则, 请返回false, 则该节点不 * 会触发doFn。 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:traversal(Function) * @example * ```html * * * * * * * * * * * ``` */ traversal: function(doFn, filterFn) { if (this.collapsed) return this; var bookmark = this.createBookmark(), end = bookmark.end, current = domUtils.getNextDomNode(bookmark.start, false, filterFn); while ( current && current !== end && domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING ) { var tmpNode = domUtils.getNextDomNode(current, false, filterFn); doFn(current); current = tmpNode; } return this.moveToBookmark(bookmark); } }; })();