键盘输入分类
直接输入
输入的键直接落入可输入DOM元素,为直接输入。
E.g.英文输入。
间接输入
输入的键值不会直接落入可输入DOM元素,有一个中间态,为间接输入。
E.g.中文输入。
区分中英文输入
因为任何输入都会触发input
,而输入中文的时候才触发compositionstart
和compositionend
,可以以此来区分中英文输入。
e.keyCode
在中英文下不同的表现
e.keyCode
在英文模式下输入,能获取正确的键值;e.keyCode
在中文模式下输入,键入任何值都输出229
;
Windows
将所有未识别的设备输入都设置为VK_PROCESSKEY 229
,浏览器的 event.keyCode
复用了这一规范,因此在中文输入过程中,无论按下什么按键,返回的event.keyCode
永远是229
。
输入的事件监听
因为任何输入都会触发input
,而输入中文的时候才触发compositionstart
和compositionend
,可以以此来区分中英文输入。
监听input
事件时,输入值时,e.data
有值,删除值时,e.data === undefined
,可以以此判断输入、删除。
compositionstart
、compositionupdate
和compositionend
只能通过window.addEventListener('')
监听,on*
监听无效。
光标位置codepen
示例
https://codepen.io/mihuartuan...
自定义编辑器
保证输入的复杂度与灵活性,一般选用普通标签而非文本域做输入容器。
普通标签可编辑
contenteditable
标签属性
属性值如下:
contenteditable=""
contenteditable="events"
contenteditable="caret"
// 纯文本输入
// 换行不会生成<div>,PC端或Android端使用'\n'判断,IOS使用inputType=== 'insertLineBreak'判断
// 复制黏贴不会带有格式
contenteditable="plaintext-only"
contenteditable="true" // 换行会生成<div>包裹
contenteditable="false"
user-modify
CSS属性
user-modify
可以在移动端使用,以及,只需要兼顾webkit内容的桌面网页项目。
-webkit-user-modify: read-only; // 普通元素的默认状态
-webkit-user-modify: read-write; //可以输入富文本
-webkit-user-modify: write-only;
-webkit-user-modify: read-write-plaintext-only // 只可以输入纯文本
两种方式对比
contenteditable
和user-modify
的旧版本浏览器支持性差contenteditable
是归属于W3C标准,全浏览器支持;而user-modify
浏览器有自己的实现,非标准,使用需要追加浏览器前缀-webkit-
、-moz-
...;contenteditable
和user-modify
都可以实现对拷贝的富文本过滤格式;
插入内容 && 关闭选择范围
基本知识
两个概念选择selection
、范围range
range
只有置于selection
中才起作用
const selection = getSelection()
const range = selection.getRangeAt(0) // 获取光标位的选中范围
const range1 = new Range() // 自定义选中范围,使用时需要selection.addRange(range1)
selection
是管理range
的集合,除了Firfox
中rangeCount > 1,其它浏览器的实现,selection
最多只有一个range
range
是文本选择范围的起点和终点
range
文本选择规则
通过
range.setStart(node, offset)
、range.setEnd(node, offset)
设置范围,根据node节点的类型nodeType
不同分属不同的情况- node为文本节点
nodeType === 3
,偏移量offset
为文本中的位置。 - node为元素节点
nodeType === 1
,偏移量offset
为指定元素子节点node.childNodes
的位置 - 其中范围起点、终点的
node
允许不同节点 - 其中范围起点、终点的定位位于偏移量
offset
之前
- node为文本节点
- 通过
console.log(range)
即可查看选中的文本;静默调用toString()
方法返回内容; - 通过
range.startContainer
、range.startOffset
查看当前范围的起点归属元素及偏移量 - 通过
range.endContainer
、range.endOffset
查看当前范围的终点归属元素及偏移量 - 通过
range.insertNode(node)
,在范围的起始处将node插入文档 - 通过
range.extractContents()
、range.deleteContents
从文档中删除范围内容 - 通过
range.surroundContents(node)
,自定义元素节点包裹选择的范围,选择的范围若有元素节点,元素节点必须闭合 - 通过
selection.empty()
可以清空选择
使用selection.addRange(range)
添加范围时,如果选择已存在,则首先使用selection.removeAllRanges()
将其清空。然后添加范围。否则,除Firefox
外的所有浏览器都将忽略新范围。 其中,通过range.setStart
、range.setEnd
调整范围的情况,不必考虑清空selection
Selection
类型
selection.type
- None: 当前没有选择。
- Caret: 选区已折叠(即 光标在字符之间,并未处于选中状态)。
- Range: 选择的是一个范围。
设置光标位置为某元素后
// 方式1. 只支持Android、PC
range.setStart(baseNode, 1)
range.setEnd(baseNode, 1)
range.collapse()
// 方式2. 只支持Android、PC
range.setEndAfter(baseNode)
selection.collapseToEnd()
// 方式3.
selection.setPosition(node, offset)
// 方式4. 支持IOS,仅用于通过range.extractContents()提取的documentFrag文本
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
// 方式4. 支持全平台,IOS不可用于通过range.extractContents()提取的documentFrag文本
selection.extend(baseNode, 1)
selection.collapseToEnd()
// 光标定位文本后:通过range.extractContents()提取的documentFrag文本
try {
// 安卓端
selection.extend(cloneNode, 1)
selection.collapseToEnd()
} catch (e) {
// Iphone端
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
}
代码示例
插入的内容只能是documentFragment
const { selection, range } = this.lastSelection
this.editableEle.focus()
const textNode = range.startContainer
range.setStart(textNode, range.endOffset)
range.setEnd(textNode, range.endOffset)
const spanNode1 = document.createTextNode(' ')
const spanNode2 = document.createElement('span')
spanNode2.className = 'tag'
spanNode2.innerHTML = '#'
let frag = document.createDocumentFragment(), lastNode = spanNode2
frag.appendChild(spanNode1)
frag.appendChild(spanNode2)
range.insertNode(frag)
// IPhone下有时候会报错,采用下方代码替代
selection.extend(lastNode, 1)
selection.collapseToEnd()
IOS下,在标签后紧跟着添加节点,selection.extend(node, 1)关闭范围报错解决方案
whetherEndTag (prefixer) {
if (!!prefixer && !(prefixer.trim())) {
// 半角、全角空格
const selection = getSelection()
const range = selection.getRangeAt(0)
const node = range.startContainer
if (node.parentNode && node.parentNode.className === 'tag') {
range.setStart(node, range.endOffset - 1)
range.setEnd(node, range.endOffset)
const cloneNode = range.extractContents()
range.setStartAfter(node.parentNode, selection.endOffset)
range.setEndAfter(node.parentNode, selection.endOffset)
range.collapse(true)
range.insertNode(cloneNode)
// 安卓、IOS不兼容
try {
// 安卓端
selection.extend(cloneNode, 1)
selection.collapseToEnd()
} catch (e) {
// Iphone端
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
}
}
}
}
预览
<iframe class="previewContaner" :srcdoc="previewContent" @load="loaded"></iframe>
获取要预览元素的HTML
,借助iframe.srcdoc
属性进行预览。
若要对预览样式进行定制,需要对previewContent
追加内联样式
...
computed: {
showContent(){
return `
<style>
img,
video {
max-width: 100%;
}
.host {
width:100%;
overflow: hidden;
font-size: 28px;
text-align: justify;
word-break: break-all;
}
</style>
<div class="host">${this.previewHtml}</div>
`
}
},
...
长度限制
非中文状态下
// 非中文状态下
onKeyupListener (e) {
this.check_charcount(e)
},
onKeydownListener (e) {
this.check_charcount(e)
},
check_charcount (e, max = 100) {
if(e.which != 8 && this.editableEle.textContent.length > max) {
e.preventDefault()
}
}
中文状态:纯文本
// 中英文,在input、compositionEnd事件中调用——纯文本
...
data () {
return {
CNEnd: true
}
}
...
compositionstart (e) {
this.CNEnd = false
},
compositionend (e) {
this.CNEnd = true
this.limitInput(e)
}
...
limitInput(event) {
let _words = this.editableEle.textContent
let _this = this.editableEle
if (this.CNEnd) {
let num = _words.length
if (num >= 100) {
num = 100
if (_this.spillOver) {
event.target.innerText = this.fullContent
} else {
event.target.innerText = _words.substring(0, 100)
_this.spillOver = true
this.fullContent = _words.substring(0, 100)
}
Toast('100字以内。')
} else {
_this.spillOver = false
this.fullContent = ''
}
const sel = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.editableEle)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
} else if (this.fullContent) {
// 目标对象:超过100字时候的中文输入法
// 原由:虽然不会输入成功,但是输入过程中字母依然会显现在输入框内
// 弊端:谷歌浏览器输入法的界面偶尔会闪现
event.target.innerText = this.fullContent
this.CNEnd = true
}
}
中文状态:富文本
区别:在于fullContent的取值。
limitInput(event) {
let _words = this.editableEle.textContent
let _this = this.editableEle
if (this.CNEnd) {
let num = _words.length
if (num >= 100) {
if (_this.spillOver) {
event.target.innerHTML = this.fullContent
} else {
const selection = getSelection()
const range = selection.getRangeAt(0)
const lastNode = range.startContainer.parentNode
lastNode.textContent = lastNode.textContent.slice(0, lastNode.textContent.length - (num - 100))
event.target.innerHTML = this.editableEle.innerHTML
_this.spillOver = true
this.fullContent = this.editableEle.innerHTML
}
Toast('コンテンツは100語以内でお願いします。')
} else {
_this.spillOver = false
this.fullContent = ''
}
const sel = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.editableEle)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
this.cacheCursorPos()
} else if (this.fullContent) {
// 目标对象:超过100字时候的中文输入法
// 原由:虽然不会输入成功,但是输入过程中字母依然会显现在输入框内
// 弊端:谷歌浏览器输入法的界面偶尔会闪现
event.target.innerHTML = this.fullContent
this.CNEnd = true
}
},
处理复制的内容
...
function paste (event) {
const paste = (event.clipboardData || window.clipboardData).getData('text');
const selection = window.getSelection();
if (!selection.rangeCount) return false;
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(paste));
event.preventDefault();
}
...
document.addEventListener("paste", paste);
...
- 通过
event.preventDefault()
禁止默认的拷贝事件 - 通过
selection
手动的插入想要拷贝的内容 event.clipboardData.getData('text')
获取拷贝的文本数据(图片、视频获取不到)
Note: 不要相信用户输入,这里的处理复制的内容,还要排除用户复制HTML
片段。因为复制过来的HTML
片段也是字符串,会带有样式。
拖拽上传图片
<label
class="video-wrap"
@dragenter.stop.prevent
@dragover.stop.prevent
@drop.stop.prevent="onFileDrop"
>
<div class="tip-wrap">
<i class="el-icon-upload"></i>
<div class="text-tip">
<p class="title-tip">点击或拖动上传视频</p>
<small class="small-tip">编码格式为h264且后缀为.mp4的视频</small>
</div>
<input class="video-input" @change="onFileChange" type="file">
</div>
</label>
...
methods: {
...
onFileChange (e) {
console.log(e.target.files[0])
},
onFileDrop (e) {
const file = e.dataTransfer.files && e.dataTransfer.files[0]
console.log(file)
}
...
}
doc转HTML
https://github.com/mwilliamso...
var mammoth = require("mammoth");
...
const reader = new FileReader()
reader.onload = function (e) {
const file = e.target.result;
mammoth.convertToHtml({
arrayBuffer: file
})
.then(function(result){
var html = result.value;
console.log(html)
})
.done();
};
reader.readAsArrayBuffer(form[0].files[0])
网络状态
在Windows
环境Chrome
浏览器中测试:
- 关闭WiFi,
online
、offline
事件并不会被触发 - 调整
Network
的网络阻塞模式,online
、offline
事件倒是会被触发
所以,online
、offline
事件不可用 navigator.onLine
情况同上navigator.connection.addEventListener('change', () => {})
事件在WiFi开关的过程中会触发,但,无法知道切换的方向性,即无法知道是联网、断网状态。
var connection = navigator.connection;
var type = connection.effecetiveType;
function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}
connection.addEventListener('change', updateConnectionStatus);
!!!世界未解之谜
node.nextSibling.nodeType === 3
当获取元素节点的兄弟文本节点node.nextSibling
时,元素节点必须要有文本内容,否则一堆世界未解之谜。
删除有样式的文本时(常见于插入回车),浏览器会自动生成<font>追加样式
clearFontTag () {
// 当删除时,浏览器自动添加font标签加样式
const fontTag = this.editableEle.querySelector('font')
if (fontTag) {
const newNode = document.createTextNode(fontTag.textContent)
this.editableEle.replaceChild(newNode, fontTag)
const { selection } = this.lastSelection
selection.extend(newNode, 1)
selection.collapseToEnd()
this.cacheCursorPos()
}
}
在带有样式的标签后回车,下一行浏览器自动带样式
- 使用
contenteditable="plaintext-only"
创建可编辑区时,在带有样式的标签后回车,换行后仍在标签内 - 使用
contenteditable="true"
创建可编辑区时,,在带有样式的标签后回车,浏览器自动在新行添加样式标签。
解决方案
clearNewlineSideEffect () {
const { range } = this.lastSelection
const node = range.startContainer
const baseNode = isAndroid ? node.parentNode : node
if (baseNode.nodeType === 1 && baseNode.className === 'tag' && !/^#/.test(baseNode.textContent)) {
const frag = document.createDocumentFragment()
const textNode = document.createTextNode(baseNode.textContent)
const brNode = document.createElement('br')
frag.appendChild(textNode)
frag.appendChild(brNode)
baseNode.parentNode.replaceChild(frag, baseNode)
}
},
知识点补充
selection
文本选择
selection
也可以实现range
部分功能的范围选择
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)
等同于对设置range.setStart(anchorNode, anchorOffset)
、range.setEnd(focusNode, focusOffset)
- 通过
selection.collapse(node, offset)
等同于对同一node设置range.setStart(node, 0)
、range.setEnd(node, offset)
- 通过
selection.setPosition(node, offset)
等同于对同一node设置同一偏移量range.setStart(node, offset)
、range.setEnd(node, offset)
或range.setStart(node, offset)
、range.collapse(true)
- 通过
selection.deleteFromDocument()
从文档中删除所选择的内容
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。