聊聊开发富文本编辑器:range 对象

5,258 阅读9分钟

接下来,我会用两篇文章来聊聊开发富文本编辑器中的关键点

今天,先来聊聊开发富文本编辑器:Range对象

本章内容如下:

  • Range对象
  • Range对象的属性和方法
  • selection对象
  • 常见案例代码

有段时间对富文本编辑器如何判断鼠标位置和获取选择的文本感到好奇,所以就翻看了bootstrap-wysiwyg富文本编辑器插件的源码,发现下面这段代码:

getCurrentRange = function() {   
  var sel = window.getSelection();   
  if (sel.getRangeAt && sel.rangeCount) {   
    return sel.getRangeAt(0);   
  }   
},   
saveSelection = function() {   
  selectedRange = getCurrentRange();   
},   
restoreSelection = function() {   
  var selection = window.getSelection();   
  if (selectedRange) {   
    try {   
      selection.removeAllRanges();   
    } catch (ex) {   
      document.body.createTextRange().select();   
      document.selection.empty();   
    }  
    selection.addRange(selectedRange);   
  }   
}

由于之前没接触过,所以选择了百度,得出下面结果:

Range 对象表示文档的连续范围区域,简单的说就是高亮选区。一个Range的开始点和结束点位置任意,开始点和结束点也可以是一样的(空Range)。最常见的就是用户在浏览器窗口中用鼠标拖动选中的区域。

不过,不同的浏览器,Range对象是不一样的,在Chrome、Mozilla、Safari等主流浏览器上,Range属于selection对象(表示range范围),而在IE下,Range属于textRange对象(表示文本范围)。

在下面的所有列子中,皆以selection对象为主,同时会加上textRange对象的兼容代码。

1、拖动选择获取

每一个浏览器窗口都有一个selection(或text Range)对象,我们可以通过window.getSelection()方法来获取:

var selectedRange;
function saveSelection(){
  if(window.getSelection){
    /*主流的浏览器,包括chrome、Mozilla、Safari*/
    return window.getSelection();
  }else if(document.selection){
    /*IE下的处理*/
    return document.selection.createRange();
  }
  return null;
};

注意:标准dom是从window中获取selection对象,而ie是从document对象中获取。

实例(可以试试在不同浏览器下的执行,点击里面的按钮btn1):Demo

1.1 优化获取代码

一个selection对象有可能不是只有一个Range对象,有可能有多个,每一个Range对象代表用户鼠标所选取范围内的一段连续区域。(在Firefox中,可以通过 ctrl键可以选取多个连续的区域,因此在Firefox中一个selection对象有多个range对象,在其他浏览器中,用户只能选取一段连续的区 域,因此只有一个range对象。)

如何获取某个Range对象呢?我们可以通过selection对象的getRangeAt方法来获取:

range = window.getSelection().getRangeAt(index)

getRangeAt()方法接受一个参数,代表该Range对象的序列号,也可以说你拖动选择的顺序号,它的值有如下几种情况:

当用户没有按下鼠标时候,该参数的值为0.  
当用户按下鼠标的时候,该参数值为1.  
当用户使用鼠标同时按住ctrl键时选取了一个或者多个区域时候,该参数值代表用户选取区域的数量。  
当用户取消区域的选取时,该属性值为1,代表页面上存在一个空的Range对象;

要获取Range对象,一般我们会判断是否有Range对象,我们可以通过selection对象的rangeCount属性(类似数组的length,返回Range对象的数量)来判断是否有多个Range对象,然后再去调用getRangeAt()方法。

对于富文本编辑器来说,一般情况下,我们只需要一个选择区域(Range对象),优化后的代码如下:

function saveSelection(){
  if(window.getSelection){
    /*主流的浏览器,包括chrome、Mozilla、Safari*/
    var sel = window.getSelection();
    if(sel.rangeCount > 0){
      return sel.getRangeAt(0);
    }
  }else if(document.selection){
    /*IE下的处理*/
    return document.selection.createRange();
  }
  return null;
};
实例(可以试试在不同浏览器下的执行,点击里面的按钮btn2):Demo

2、Range对象的属性和方法

2.1 创建Range对象(范围)

document.createRange():用于创建一个Range对象(范围)

在IE下:

document.body.createTextRange():用于创建一个textRange对象

2.2 Range对象的属性

endContainer:返回范围的结束点的Document节点,通常是文本节点。

endOffset:返回endContainer中的结束点位置。

startContainer: 返回范围的开始点中的Document节点,通常是文本节点。   

startOffset:返回startContainer中的开始点位置。

collapsed:用于判断范围的开始点与结束点是否处于相同的位置,如果相同,该属性值返回true,即范围是空的或折叠的。

commonAncestorContainer:范围的开始点和结束点的(即它们的祖先节点)、嵌套最深的 Document 节点。

注意:所有属性都是只读的。如果范围中存在空格,也会计算在偏移量内。

2.3 Range对象的方法

2.3.1 范围选择

selectNode(node):设置该范围的边界点,使它包含指定节点和指定节点的所有子孙节点。

selectNodeContents(node):设置该范围的边界点,使它包含指定节点的子孙节点,但不包含指定节点本身。

2.3.2 操作范围

deleteContents():将Range对象中所包含的内容从页面中删除

setStart(node,index):将指定节点中的某处位置指定为Range对象所代表区域的起点位置

setEnd(node,index):将指定节点中的某处位置指定Range对象所代表区域的结束位置

setStartBefore(node):将指定节点的起点位置指定为Range对象所代表区域的起点位置。

setStartAfter():将指定节点的终点位置指定为Range对象所代表区域的起点位置。

setEndBefore():将指定节点的起点位置指定为Range对象所代表区域的终点位置。

setEndAfter(): 将指定节点的终点位置指定为Range对象所代表区域的终点位置。

cloneRange():对当前的Range对象进行复制,该方法返回一个复制的Range对象

cloneContents():复制当前Range对象所代表区域中的HTML代码并返回新的DocumentFragment对象。

extractContents():将Range对象所代表区域中的html代码克隆到一个DocumentFragment对象中,然后从页面中删除这段HTML代码

detach():释放Range对象。

insertNode(node):将指定的节点插入到某个Range对象所代表的区域中,插入位置为Range对象所代表区域的起点位置,如果该节点已经存在于页面中,该节点将被移动到Range对象代表的区域的起点处。
实例(关于setStart和setEnd,点击里面的按钮btn6):Demo
实例(关于deleteContents,点击里面的按钮btn7):Demo
实例(关于extractContents,点击里面的按钮btn8):Demo

2.3.3 其他方法

collpase(boolean)  用于使范围的边界点重合。当为true时,将范围的结束点设为与开始点相同的值;当为false时,将范围的开始点设为与结束点相同的值。

compareBoundaryPoints(how,sourceRange):用来比较两个Range对象,返回1,0,-1(0表示相等,等于1时,当前范围在sourceRange之后,等于-1时,当前范围在sourceRange之前)

toString():返回该范围表示的文档区域的纯文本内容。

(1)compareBoundaryPoints()

how的常量:

START_TO_START	用指定范围的开始点与当前范围的开始点进行比较。  
START_TO_END	用指定范围的开始点与当前范围的结束点进行比较。  
END_TO_END	用指定范围的结束点与当前范围的结束点进行比较。  
END_TO_START	用指定范围的结束点与当前范围的开始点进行比较。

4、selection对象

selection对象可看作是Range对象的集合,包含一个或多个Range对象。

4.1 属性

anchorNode:返回范围的开始点的Document节点,和range对象的endContainer作用一样。

anchorOffset:返回startContainer中的开始点位置,和range对象的startOffset作用一样。

focusNode:返回范围的结束点的Document节点,和range对象的endContainer作用一样。

focusOffset:返回endContainer中的结束点位置,和range对象的endOffset作用一样。

isCollapsed:范围的开始点与结束点是否重叠

这是新标准中selection的属性,通过这些属性,我们省却了先获取range对象再获取偏移量和节点的繁琐。

4.2 方法

removeAllRanges():删除selection中原有的所有range

addRange(range):将新的range添加到selection中

5、HTMLInputElement的属性方法

5.1 在IE下Range对象

(1)属性

htmlText:返回字符串,为textRange的HTML内容,与innerHTML作用一样,只读 
text:返回字符串,为textRange的文本内容,相当于innerText,可读写。

(2)方法

moveStart("character",index):选定范围的开始点向后移动index个字符

moveEnd("character",index):选定范围的结束点向后移动index个字符

pasterHTML():黏贴HTML到一个文本节点时,该文本节点自动分隔。

5.2 在其他主流浏览器上

(1)属性

selectionStart:获取范围的开始点,可读写

selectionEnd:获取范围的结束点,可读写

selectionDiraction

(2) 方法

select():在焦点状态下,移动光标至第一个字符后面

setSelectionRange(start,end):设置范围的开始点和结束点。

注意:selectionStartselectionEnd会记录元素最后一次selection的相关属性,意思就是当元素失去焦点后,使用selectionStartselectionEnd仍能够获取到元素失去焦点时的值。

setSelectionRange(start,end)

如果textbox没有selection时,selectionStart和selectionEnd相等,都是焦点的位置。
在使用setSelectionRange()时  
如果end小于start,会讲selectionStart和selectionEnd都设置为end;  
如果start和end参数大于textbox内容的长度(textbox.value.length),start和end都会设置为value属性的长度。

6、常用案例代码

6.1 针对div(contenteditable="true")相关操作

(1) 获取用户选择内容

function saveSelection(){
  if(window.getSelection){
    /*主流的浏览器,包括chrome、Mozilla、Safari*/
    var sel = window.getSelection();
    if(sel.rangeCount > 0){
      return sel.getRangeAt(0);
    }
  }else if(document.selection){
    /*IE下的处理*/
    return document.selection.createRange();  
  }
  return null;
};

var selectedRange = saveSelection();  // 保存获取到的Range对象

注意:如果是在IE下需要获取内容,需要使用selection.text来获取。

实例(可以试试在不同浏览器下的执行,点击里面的按钮btn2):Demo

(2) 恢复光标位置

function restoreSelection() {   
  var selection = window.getSelection();   
  if (selectedRange) {   
    try {   
      selection.removeAllRanges();  /*清空所有Range对象*/ 
    } catch (ex) {
      /*IE*/   
      document.body.createTextRange().select();   
      document.selection.empty();   
    };
    /*恢复保存的范围*/    
    selection.addRange(selectedRange);   
  }   
}

(3)将光标移至文本最后

function selectAllText(elem){
  if(window.getSelection){
    elem.focus();
    var range = window.getSelection();
    range.selectAllChildren(elem);
    range.collapseToEnd();
  }else if(document.selection){
    var range = document.selection.createTextRange();
    range.moveToElementText(elem);
    range.collapse(false);
    range.select(); /*避免产生空格*/
  }
}

6.2 表单元素(input、textarea)相关操作

(1) 将光标置于表单元素的最后

function toTextEnd(elem){
  if(window.getSelection){
    elem.setSelectionRange(elem.value.length,elem.value,length);
    elem.focus()
  }else if(document.selection){
    /*IE下*/
    var range = elem.createTextRange();
    range.moveStart('character',elem.value.length);
    range.collapse(true);
    range.select();
  }
}

实例(点击里面的按钮btn10):Demo

(2) 选中所有文字

function selectAllText(elem){
  if(window.getSelection){
    elem.setSelectionRange(0,elem.value.length);
    elem.focus();
  }else if(document.selection){
    var range = elem.createTextRange();
    range.select();
  }
}

实例(点击里面的按钮btn11):Demo

(3)获取光标位置

function getCursorPosition(elem){
  if(window.getSelection){
    return elem.selectionStart;
  }else if(document.selection){
    elem.focus();
    var range = document.selection.createTextRange();
    range.moveStart('character',-elem.value.length);
    return range.text.length;
  }
  return elem.value.length;
}

(4)设置光标位置

function setCursorPosition(elem, position){
  if(window.getSelection){
    elem.focus();
    elem.setSelectionRange(position,position);
  }else if(document.selection){
    var range = elem.createTextRange();
    range.collapse(true);
    range.moveEnd('character',position);
    range.moveStart('character',position);
    range.select();
  }
}