富文本编辑器中引用(BLOCKQUOTE)的问题

昨天说到我要自己写一个富文本编辑器,到目前为止,前6个功能已经完成了,但就是第7个引用功能,让我搞了一下午,下面记录一下开发过程。(下文中的w = window)

引用(BLOCKQUOTE )与前6个功能相比,同样有官方的命令document.execCommand('indent'),但问题就出在,各个浏览器对这个命令的实现方式真是乱七八糟,又一次深深的体会到富文遍编辑器这个坑,果然是谁写谁知道。

firefox的实现是最标准的也是最简便的,当上面生成引用的命令执行后,会生成一个BLOCKQUOTE元素,之后做的操作都会在这个元素之中,除非又执行了一次execCommand命令,文本界面与生成的dom结构就是下面这样:

chrome和safari的dom结构相同:

为什么说firefox的实现是最标准的也是最简便的呢?因为逻辑上引用是只能引用一次的,也就是不能嵌套的,比如:

呵呵,天气真好。

我引用了这段文字,表明这不是我说的,我是借别人的话来表达自己的想法,但是你会引用一段别人引用别人的话么==。但是在三大浏览器中,引用都是可以嵌套的==。而且如果不对引用嵌套加以限制,引用区块过多会把输入位置顶到编辑器外面去。所以现在的逻辑就是:
如果光标的位置处于引用范围内,则阻止创建新的引用病取消当前引用,反之则创建引用,也就是这样的:
{<5>}

前一篇文章说过检测当前文本被赋予了哪些样式可以使用queryCommandState 方法,但是在引用区域中,document.queryCommandState(‘indent’)始终返回false==。这就是为什么firefox的实现方式最方便的原因,在firefox下,只要检测光标位置的父元素是不是BLOCKQUOTE就好了,但在chrome和safari下,因为每行都是一个BLOCKQUOTE,特殊输入情况还会出现div与BLOCKQUOTE轮番嵌套的情况,这样显然只判断父节点是不行的,特别蛋疼。
所以要对当前光标上的元素递归遍历起父节点,如果有BLOCKQUOTE则说明这行是在引用状态的,则调用document.execCommand('outdent')来取消引用,这里要注意,引用的用法与之前的execCommand不同,document.execCommand(‘indent’)只有创建引用的功能。

function findquote(node){
    var find = false;
    (function core(node){
      var pnode = node.parentElement;
      if(node.nodeName === 'BLOCKQUOTE'){
        find = true;
      }else if((pnode.nodeName === 'DIV' && pnode.className === 'editor') || pnode.nodeName === 'BODY'){
      }else{
        core(pnode);
      }
    })(node) // 第一次传入递归的node参数,由外层函数传入
    return find;
  }

写了个函数,这个函数之后还会用到,第一次传入递归的node参数是下面的quote。

quote = w.getSelection().getRangeAt(0).commonAncestorContainer;

quote的作用就是获取当前光标位置的共同父元素,从这个元素开始递归遍历。

现在可以找到某一个光标的位置是否在引用中了,但这只完成了一半,万一用户想对某一选区范围取消引用呢,所以还要判断用户选择的某一选区是否在BLOCKQUOTE中。

要知道某一选区是否在引用中,首先要确定被选中的都有哪些元素。这个开始花了好长时间,后来还是被我想到了解决办法,w.getSelection().getRangeAt(0)返回值原型中有一个方法:cloneContents,它可以复制当前选区中的文档片段,他的返回值就是选区中的dom结构,也就是下面这样:

现在有了被选的dom结构,确定是否在引用中就很简单了,下面这个函数就可以确定选区中的元素是否在引用范围内,是则返回true,传入的selectednode参数就是刚刚获取的文档片段:w.getSelection().getRangeAt(0).cloneContents().children

  function findquotefromchild(selectednode){
    var find = false;
    for(var i=0; i<selectednode.length; i++){
      if(selectednode[i].nodeName === 'BLOCKQUOTE' && selectednode[i].innerHTML !== ''){
        find = true;
        break;
      }
    }
    return find;
  }

但又出现问题了,上面获取文档片段的代码在safari中就是不通过,其实解决办法很简单,不过要先吐槽下safar的开发者工具:只能说真tm难用,完全跟chrome dev tools不在一个档次的。safari中出现的问题原因其实很简单,我通过w.getSelection().getRangeAt(0).cloneContents()获取选区中的文档片段,他的返回值是没有length属性的,所以我要先取得它的children属性,这样就能知道有几个元素了,但是逗逼safari里没有这个属性==,所以只能用childNodes属性了。

现在我们已经能知道一个光标的位置或者一个选区是否在引用范围内了。接着根据返回值却来确定到底是添加引用还是取消引用了。现在问题已经解决大部分了,还有个小功能,就是要根据光标当前的位置决定顶用按钮的状态是弹出还是按下,有了前面的基础,现在这个就很简单了,监听keyup事件,事件回调中调用上面的两个判断函数,就能知道当前位置是否在引用中了,这样也就能切换按钮状态了。在这里要注意,一定要监听keyup事件,而不是keypress,keyup会在按键被释放时出触发,这时光标已经移动过,这时获取的状态是光标移动过的状态,而keypress会在按键按下时触发,这时获取的状态是按键之前的,当光标在有引用与无引用中切换时无法正确反映当前按钮状态。

好了,现在引用功能已经基本完成了,目前还没有碰到什么问题,等碰到了再解决吧。

另外,还有一个昨天的遗留问题。当页面加载完成后,先按功能按钮,再点击输入区域,刚在启用的功能并没有生效,比如我在页面加载后先按加粗按钮,再点击输入区域,这时文字并没有加粗,还是普通状态。分析了下,可能是要输入区域有焦点是功能才能生效,试了一下果然没错,所以在每次按钮点击后,先对输入区域调用一下focus(),之后再执行功能代码,这样就能生效啦。
但在Safari中又出现了一个问题,每次focus后,光标都会移到输入范围的开头,这明显是不符合常理的,仔细想了下,问题应该处在blur事件这块。每次点击按钮时,输入框会触发blur事件,再次focus就会自动改变光标的位置。这样解决办法就有了:阻止blur事件的发生。给按钮添加mousedown事件,回调中调用e.preventDefault();阻止事件的进一步发生,这样点击按钮输入框就不会blur啦,问题圆满解决。