laizimo / zimo-article

:books:博客——源于实践,乐于分享,欢迎Star~
1.06k stars 95 forks source link

移动端富文本实践篇(二) #42

Open laizimo opened 6 years ago

laizimo commented 6 years ago

前言

至上一篇基础常识讲完之后,这次我们将开启新的篇章。本篇我们会来讲述你在操作时需要去增加的监听事件,焦点控制,按钮的状态同步等问题。同时,还需要完成的是当标题栏聚焦时,你需要去控制按钮的禁止点击,例如插入图片按钮等。所以综合而言,本篇还是相对比较重要的。接下来,我们会就上述提到的点进行一一讲述。如果你喜欢我的文章,欢迎评论,欢迎Star~。欢迎关注我的github博客

正文

首先,我们第一步来讲一下焦点控制的问题。那么,你会认为焦点控制只是简单的使用focus函数聚焦这么简单吗?当然不是。一旦,你这样操作了,你会发现一个问题,你的焦点永远在起始处,往往会造成不良的用户体验。这里我们需要明白的第一点——焦点控制

焦点

我们知道焦点其实就是一个range。每个range都会又一个collapsed去判断,前后是否重叠。一旦,前后重叠了,这个range就会称为焦点。所以,我们如果想去控制焦点,就必须学会控制range块。

回到上一篇的range介绍,我们知道range具备4个常用属性:startContainer、startOffset、endContainer、endOffset等4个属性。我们只要学会如何去控制这4个属性,我们就能将range块变出任意的样子。

那么,我们可以先从「聚焦功能」说起:

这里,我们所谓的聚焦是可以区分为两种,一种是保证聚焦到尾部,另一种就是聚焦到用户输入的位置。

聚焦尾部

之前,我们已经说过,focus函数使得整个编辑块聚焦,但是它是使得焦点聚焦在起始处,而我们现在需要将整个焦点聚焦到末尾,我们源码中的逻辑可以这样:

我们可以来看一下源码:

focus: function(){   //聚焦
    const _self = this;
    const range = document.createRange();
    range.selectNodeContents(_self.cache.editor);
    range.collapse(false);
    const select = window.getSelection();
    select.removeAllRanges();
    select.addRange(range);
    _self.cache.editor.focus();
}

这里,我们创建了一个range,然后将这个range的节点内容设置为编辑块,之后使用collapse来使得它的先后合并。同时,我们需要去获得选区,将选区中的range都清除掉,再将新创建的range对象添加到选区对象中。最后,使编辑块聚焦。

这时,你去测试一下,你就会发现焦点会自动聚焦到尾部去

聚焦还原

之前,我们也讲述过还有一种焦点控制的方式——聚焦到原先用户输入的位置

那么,我们需要如何去完成这一个功能呢?我们首先需要去保存用户输入的焦点。

我们可以先来看一下源码:

saveRange: function(){
    const _self = this;
    const selection = window.getSelection();
    if(selection.rangeCount > 0){
        const range = selection.getRangeAt(0);
        const { startContainer, startOffset, endContainer, endOffset} = range;
        _self.currentRange = {
            startContainer: startContainer,
            startOffset: startOffset,
            endContainer: endContainer,
            endOffset: endOffset
        };
    }
}

这里的saveRange方法就是我们在源码中用来保存Range的方法。其实,它的原理非常的简单:

  1. 从选区中去获得第一个range块。(注意:因为ctrl键可以保证一个选取有多个range块)
  2. 然后将range块中的四个属性提取出来startContainer、startOffset、endContainer、endOffset。
  3. 最后,将这四个属性保存下来,因为我们之后也会使用到这个内容

既然你看到了保存焦点时的原理,那么,相信还原焦点的原理你应该也已经清楚一点了吧。

接下来,我们就来看一下还原焦点的过程:

源码:

reduceRange: function(){
    const _self = this;
    const { startContainer, startOffset, endContainer, endOffset} = _self.currentRange;
    const range = document.createRange();
    const selection = window.getSelection();
    selection.removeAllRanges();
    range.setStart(startContainer, startOffset);
    range.setEnd(endContainer, endOffset);
    selection.addRange(range);
}

至此,我们已经将富文本中需要去控制焦点的部分内容分析完了。之后,我们先来看一下按钮的状态同步

状态同步

何为状态同步?你或许还没有一个比较清晰的概念。那么,我们给定一个场景,来帮助大家理解一下:

最初,你会按下加粗按钮之后,输入部分的内容会加粗;但是,当你这时发现之前有个地方的内容,需要修改,这时你会点击那个部分进行修改。这时问题来了:在没点击之前,你的加粗按钮是高亮显示的,而点击之后,你首先要确定那个位置是否具备加粗,然后去控制按钮的高亮问题。这就是我们之后需要处理的问题——状态同步

我们可以先简单的阐述一下状态同步的原理:

我们只需要去获得当前焦点处所含有的标签就可以了。因为我们所插入的bold、italic等都是通过execCommand的命令插入的。同样,document也提供了API让我们来获取当前焦点处的标签。我们可以看一下源码中的这个方法:

getEditItem: function(evt = {}){
    const _self = this;
    const { STATE_SCHEME, CHANGE_SCHEME } = _self.schemeCache;
    if(evt.target && evt.target.tagName === 'A'){
        _self.cache.currentLink = evt.target;
        const name = evt.target.innerText;
        const href = evt.target.getAttribute('href');
        window.location.href = CHANGE_SCHEME + encodeURI(name + '@_@' + href);
    }else{
        if(e.which == 8){
            AndroidInterface.staticWords(_self.staticWords());
        }
        const items = [];
        _self.commandSet.forEach((item) => {
            if(document.queryCommandState(item)){
                items.push(item);
            }
        });
        if(document.queryCommandValue('formatBlock')){
            items.push(document.queryCommandValue('formatBlock'));
        }
        window.location.href = STATE_SCHEME + encodeURI(items.join(','));
    }
}

这里的源码内容有点复杂,因为我们还有其他的一些情况需要考虑,所以这里我们可以来提取一部分进行分析:

const items = [];
_self.commandSet.forEach((item) => {
    if(document.queryCommandState(item)){
        items.push(item);
    }
});
if(document.queryCommandValue('formatBlock')){
    items.push(document.queryCommandValue('formatBlock'));
}
window.location.href = STATE_SCHEME + encodeURI(items.join(','));

这个部分就是实际去获取标签的部分,我们可以先来了解两个API:

最后,一步就是一个通信的问题了。我们之前一篇中,聊到如果js与webView之间进行交互时,可以通过url劫持的方式来完成。我们将这个URL头进行定义,相对应这种特殊的URL头,webView会做相应的处理。

因为URL中添加参数时,都需要将值进行URL编码。所以,我们需要做一个编码的过程。

那么,至此我们提取出来的代码部分讲完了。我们回过头来再去分析一下原来的代码。

有些特殊情况或许你的考虑到:

首先,来看一下修改链接的,我们并不需要去进行状态的同步。所以,我们需要确定点击时,判断这个节点元素是否是A标签,我们可以看一下源码:

if(evt.target && evt.target.tagName === 'A'){
    _self.cache.currentLink = evt.target;
    const name = evt.target.innerText;
    const href = evt.target.getAttribute('href');
    window.location.href = CHANGE_SCHEME + encodeURI(name + '@_@' + href);
}

因为,我们需要修改链接,所以需要将当前这个链接的节点保留下来,方便之后的修改;同时,我们也需要向webview传递链接的name和url的信息。使用的方式——URL劫持。

之后,我们需要去考虑的是一些键位,比方说回车操作,删除操作。它们本身也不会去通知webview对其进行监听。键位的话,我们可以考虑按键时的键位code来进行特殊键位的判断,如下:

_self.cache.editor.addEventListener('keyup', (evt) => {
    if(evt.which == 37 || evt.which == 39 || evt.which == 13 || evt.which == 8){
        _self.getEditItem(evt);
    }
}, false);

这里我们对删除键、回车键、左尖括号『<』,右尖括号『>』,做了监听,然后当用户按下这几个键时,都会调用getEditItem的方法。

状态同步的问题我们就聊那么多。之后我们来看一下我们设置的监听事件。

设置监听

直接先放上源码来让大家看一下:

bind: function(){
    const _self = this;

    document.addEventListener('selectionchange', _self.saveRange, false);

    _self.cache.title.addEventListener('focus', function(){
        AndroidInterface.setViewEnabled(true);
    }, false);

    _self.cache.title.addEventListener('blur', () => {
        AndroidInterface.setViewEnabled(false);
    }, false);

    _self.cache.editor.addEventListener('blur', () => {
        _self.saveRange();
    }, false);

    _self.cache.editor.addEventListener('click', (evt) => {
        _self.saveRange();
        _self.getEditItem(evt);
    }, false);

    _self.cache.editor.addEventListener('keyup', (evt) => {
        if(evt.which == 37 || evt.which == 39 || evt.which == 13 || evt.which == 8){
            _self.getEditItem(evt);
        }
    }, false);

    _self.cache.editor.addEventListener('input', () => {
        AndroidInterface.staticWords(_self.staticWords());
    }, false);
}

直接按照顺序阐述下去吧!!

selectionchange事件,则是检测选区的变化,因为选区发送变化的时候。往往指定是焦点的变化。

每次焦点发生变化时,都需要去保存当前的range,以便于还原焦点。

focus和blur事件,其实就是需要去控制底栏按钮的可用性。因为我们的界面上面有标题栏,标题栏是不允许插入图片、插入链接、字体操作的。所以这里通过对象映射的方式,提醒webView去禁止底栏显示。

同样的,对于编辑块来说,需要监听blur事件,然后保存原来的焦点。

接下来,就是我们之前所说的点击事件的监听了。首先,点击编辑块时,需要你去保存焦点,同时同步这个位置的状态,调用getEditItem方法。

最后,需要去监听一个输入事件,因为我们需要去同步字体的数量,每当用户输入时,我们就要调用staticWords方法来同步字体的数目。

总结

最后,我们本篇文章的内容都已经分析完了。当然,你也可以细细理解我们在这里说做的所有操作,可以说这篇内容解决了我们在开发富文本编辑器时,大部分的问题。同时,也内涵了我们的思考。希望你能喜欢我们这个项目,同时帮助你的进步。

最后,如果你对我写的有疑问,可以与我讨论。如果我写的有错误,欢迎指正。你喜欢我的博客,请给我关注Star~呦。大家一起总结一起进步。欢迎关注我的github博客。同时也希望你关注我们的项目,github项目地址,谢谢支持