当前位置: 首页 > Web前端 > HTML

富文本编辑器quill.js开发(三):光标和选择区

时间:2023-03-28 10:52:15 HTML

术语解释首先,我们需要了解一些术语以便更好的理解,如果你已经理解,可以跳过本节锚点(anchor)是选择的起点(与HTML中的锚链接不同)。当我们用鼠标选择一个区域时,锚点就是鼠标按下的那个点。当用户拖动鼠标时,锚点不会改变。Focus(焦点)选区的焦点是选区的终点。当您使用鼠标选择一个选择区域时,焦点是您释放鼠标时记录的点。当用户拖动鼠标时,焦点位置会发生变化。范围(range)范围是指文档的连续部分。范围包括整个节点,也可以包括节点的一部分,例如文本节点的一部分。用户通常只能选择一个范围,但有时用户可能会选择多个范围(例如,当用户按下控制按钮并选择多个区域时,Chrome中禁止此操作)。“Range”作为Range对象返回。也可以通过DOM创建、添加和删除范围对象。本词汇表来自MDNcontenteditablecontenteditable全局属性是一个枚举属性,指示元素是否应由用户编辑。如果是这样,浏览器修改其小部件以允许编辑。简单来说,如果我们想让一个div可以编辑,我们可以通过添加这个属性来实现。这是富文本编辑器最基本的结构。要完成富文本,首先要控制它的Cursor,浏览器提供选择对象和范围对象来操作光标。SelectionSelection对象表示用户选择的文本范围或插入符号的当前位置。它表示页面上的文本选择,可能跨越多个元素。文本选择是通过用户将鼠标拖到文本上创建的。我们可以使用APIwindow.getSelection()获取当前用户选择的文本。这是调用后的返回结果:部分属性说明anchorNode是只读的,返回选择起点所在的节点(Node)。anchorOffset只读返回一个数字,表示选区起点在anchorNode中的位置偏移量。如果anchorNode是文本节点,则返回从文本节点的第一个词开始到第一个被选中的词的词数(如果第一个词被选中,则偏移量为零)。如果anchorNode是一个元素,则返回选择中第一个节点之前的兄弟节点总数。(这些节点是anchorNode的子节点)isCollapsed只读返回一个布尔值,用于判断选择的起点和终点是否在同一位置。rangeCount只读返回此选择中包含的连续范围的数量。方法这里只是一些重要的方法getRangeAtvarselObj=window.getSelection();range=sel.getRangeAt(index)示例:letranges=[];sel=window.getSelection();for(vari=0;i这是一段巴拉巴拉这是另一段vars=window.getSelection();//一开始我们让他选择foo节点varrange=document.createRange();range.selectNode(foo);s.addRange(range);//一秒钟后我们取消选择foo节点,选择所有body节点setTimeout(()=>{s.removeAllRanges();varrange2=document.createRange();range2.selectNode(document.body);s.addRange(range2);},1000)效果展示:遇到到达contenteditable时元素,如果strong#foo元素是一个contenteditable元素:ThisisaparagraphofBarbara那么我们不能直接使用range.selectNode(foo);,和应该这样做:varrange=document.createRange();range.setStart(foo,0)range.setEnd(foo,1)//其中0、1代表子节点个数s.addRange(range);其中setStart和setEnd的第二个参数:如果起始节点类型是Text、Comment或CDATASection中的一种,则startOffset是指距起始节点的字符偏移量。对于其他Node类型的节点,startOffset是指起始节点到子节点的偏移量。或者使用selectNodeContentsAPI:varrange=document.createRange();range.selectNodeContents(foo)s.addRange(range);collapsecollapse方法可以将当前选择折叠到一个点。文档不会改变。如果选择的内容是可编辑的,并且焦点落在上面,光标会在那里闪烁。同样,这里也创建了一个例子Thisisaparagraphblahblah

vars=window.getSelection();varrange=document.createRange();range.selectNode(foo);s.addRange(range);setTimeout(()=>{s.collapse(foo,0);},1000)效果是1秒后,选区消失。让我们将contenteditable添加到p标签并尝试:;range.selectNodeContents(foo)s.addRange(range);setTimeout(()=>{s.collapse(foo,1);},1000)效果展示:RangeRange接口代表了一个文档片段,其中包含了a的一部分节点和文本节点。在上面的例子中,我们已经尝试使用Document.createRange方法来创建一个Range,Range对象也可以通过Selection对象的getRangeAt()方法或者Document对象的caretRangeFromPoint()方法来获取。Range(由document.createRange();创建)具有以下属性:{collapsed:true//指示Range的开始位置和结束位置是否相同的布尔值LevelnodeendContainer:document//包含Range结束的节点.endOffset:0//一个数字,表示Range结束点在endContainer中的位置。startContainer:document//包含范围开始的节点。startOffset:0//表示Range在startContainer中的起始位置的数字。}collapseRange.collapse()方法将Range折叠到边界点语法:range.collapse(toStart);toStart可以选择一个布尔值:true折叠到Range的起始节点,false折叠到结束节点。如果省略,则默认为false。在前面的Selection-collapse例子中,我们也可以通过这个API进行操作,达到同样的效果:();varrange=document.createRange();range.selectNodeContents(foo)s.addRange(range);setTimeout(()=>{range.collapse()//s.collapse(foo,1);},1000)在上一篇文章中,我尝试过使用selectNode()、selectNodeContents()、setEnd()、setStart()等方法,这里不再赘述。quill中的selection会根据nativeAPI获取信息并包装自己的一个对象:getRange(){constroot=this.scroll.domNode;//省略空值判断constnormalized=this.getNativeRange();//先来看这个函数if(normalized==null)return[null,null];//暂且不说}getRange函数是quill中获取选区的方法,normalized是基于nativeapi,通过一定的封装获取数据:getNativeRange(){constselection=document.getSelection();如果(选择==null||selection.rangeCount<=0)返回null;constnativeRange=selection.getRangeAt(0);如果(nativeRange==null)返回null;//上面四句使用nativeapi判断当前是否有选区//因为基本上rangeCount为1,所以可以直接通过getRangeAt(0)获取选区//这里的normalizeNative才是真正的native操作//nativeRange为当前constrange=this.normalizeNative(nativeRange);returnrange;}normalizeNativenormalizeNative(nativeRange){//判断选择是否在当前编辑器根元素中,是否被选中if(!contains(this.root,nativeRange.startContainer)||(!nativeRange.collapsed&&!contains(this.root,nativeRange.endContainer))){返回null;}//构造一个自定义对象来存储原生数据constrange={start:{node:nativeRange.startContainer,offset:nativeRange.startOffset,//起始元素的偏移量,但不是指从视觉点的偏移量视图的详细信息,请参见nativeRange.startContainer.data},end:{node:nativeRange.endContainer,offset:nativeRange.endOffset},native:nativeRange,};//开始遍历[range.start,range.end][range.start,range.end].forEach(position=>{//从原始位置获取Values:node=range.startContaineroffset=range.startOffsetlet{node,offset}=position;//当一个节点不是文本并且有子节点时//因为quill中有一些特殊的格式,比如图片,视频,表情符号等//这些特殊的格式在选择区域中占据不同的位置,例如:一张图片,看起来很大,但实际上偏移量只有1//同时,如果我们需要一些自定义的功能,这里的判断可能会影响选择区域,所以这里需要做一些特殊的判断while(!(nodeinstanceofText)&&node.childNodes.length>0){if(node.childNodes.length>offset){//判断node=node.childNodes[offset];偏移量=0;}elseif(node.childNodes.length===offset){node=node.lastChild;if(nodeinstanceofText){offset=node.data.length;}elseif(node.childNodes.length>0){//容器大小写offset=node.childNodes.length;}else{//嵌入案例offset=node.childNodes.length+1;}}else{休息;}}position.node=节点;position.offset=偏移量;//任务});返回范围;}最后返回一个自定义范围对象normalizedToRange,自定义对象包装后,会经过一个normalizedToRange方法getRange()的计算{constroot=this.scroll.domNode;//省略空值判断constnormalized=this.getNativeRange();//返回自定义范围if(normalized==null)return[null,null];constrange=this.normalizedToRange(normalized);返回[范围,归一化];}normalizedToRange://range的结构//constrange={//start:{//node:nativeRange.startContainer,//offset:nativeRange.startOffset,//起始元素的偏移量,但不代表它在视觉上看起来是Offset,详情参见nativeRange.startContainer.data//},//end:{node:nativeRange.endContainer,offset:nativeRange.endOffset},//native:nativeRange,//};//normalizedToRange(range){constpositions=[[range.start.node,range.start.offset]];//如果没有关闭,即游标状态,将末尾的数据添加到数组中if(!range.native.collapsed){positions.push([range.end.node,range.end.offset]);}//遍历数据,得到索引(从编辑框的第0位开始计算)constindexes=positions.map(position=>{//normalizeNative修改的值,和原来的相比,node和offset可能修改了const[node,offset]=position;//搜索对应的domconstblot=this.scroll.find(node,true);//通过它的api获取offset,可以查看https://github.com/quilljs/parchmentconstindex=blot.offset(this.scroll);//如果一个dom上的offset是0,那么当前索引就是dom的索引??if(offset===0){returnindex;}//LeafBlot是一个特例,属于子节点,属于parchment库if(blotinstanceofLeafBlot){returnindex+blot.index(node,offset);}//最后加上当前节点的长度returnindex+blot.长度();});//比较当前索引和编辑器的长度,使其不超过constend=Math.min(Math.max(...indexes),this.scroll.length()-1);//比较end和index,取最小值,作为起始值conststart=Math.min(end,...indexes);//通过原生API更新一个新的Range对象,传入参数start和lengthreturnnewRange(start,end-s开始);}执行顺序查看selection通过官方API,我们可以查看之前计算的数据:consteditorRef=useRef()editorRef.current?.getEditor()?.selection结果如图:其中lastRange对应normalizedToRange和lastNative是getNativeRange的返回(包装的本机数据)。总结本文主要介绍原生API:Selection和Range的作用及其属性和方法的说明,并通过这两个API在quill中进行介绍。API将受到怎样的影响,我们需要做出怎样的判断?总的来说,这两个API在富文本功能中基本不会遇到,所以大多数情况下,你只需要参考https://developer.mozilla了解即可。组织...https://github.com/quilljs/pa...https://github.com/quilljs/quill