Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
laizimo opened this issue Oct 21, 2017 · 0 comments
Open

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

laizimo opened this issue Oct 21, 2017 · 0 comments

Comments

@laizimo
Copy link
Owner

laizimo commented Oct 21, 2017

前言

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

正文

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

焦点

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

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

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

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

聚焦尾部

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

  • 首先,我们先获取窗口的选区(selection的概念,我们之前也有提到过)
  • 然后,我们需要将选取中的所有range块都清除掉(这样可以保证之前无论焦点处于任何的位置,都会先失去)
  • 然后,创建一个新的range块,这时range块的所有属性都是默认的
  • 最后,将这个range添加到selection中,这时,添加进去之后,range中的属性值并没有被定义,所以,整个range会保持在最后的位置。然后在使用focus函数,使得编辑块获得这个range

我们可以来看一下源码:

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. 最后,将这四个属性保存下来,因为我们之后也会使用到这个内容

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

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

  • 首先,我们会创建一个range对象
  • 然后,选区中所有的range都清除掉
  • 之后,我们会将保存下来的四个属性设置进入range对象
  • 最后,往选区中添加range

源码:

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:

  • queryCommandState: 这个函数返回的boolean类型的值,然后它会去测试当前这个state中是否具备这个标签。

    我们可以做个测试:

    document.execCommand('bold', false, null);
    const state = document.queryCommandState('bold');
    console.log(state);   //true

    这样我们就可以明白上面第一部分操作的原理:

    • 首先,遍历数据集中的命令
    • 如果命令所处的标签存在,则将这个命令放入到items数组中,方便之后的状态同步。
  • queryCommandValue:这个函数也是返回boolean类型的值。我们可以直接来做个测试:

    document.execCommand('formatBlock', false, '<h1>');
    const value = document.queryCommandValue('formatBlock');
    console.log(value);  //h1

    这里的不同点就是,无参数命令和有参数命令的区别了。类似于h1标签这种,需要我们自定义标签参数的值,往往就需要使用这部分的测试方式,所以,我们也会将获取到的value放入items集合中

最后,一步就是一个通信的问题了。我们之前一篇中,聊到如果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项目地址,谢谢支持

@laizimo laizimo changed the title 移动端富文本实践篇(三) 移动端富文本实践篇(二) Jan 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant