HTML中编辑区域适用范围,创建可编辑区域

键盘输入分类

直接输入

输入的键直接落入可输入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示例

自定义编辑器

保证输入的复杂度与灵活性,一般选用普通标签而非文本域做输入容器。

普通标签可编辑

contenteditable标签属性

属性值如下:contenteditable=""

contenteditable="events"

contenteditable="caret"

// 纯文本输入

// 换行不会生成

,PC端或Android端使用'\n'判断,IOS使用inputType=== 'insertLineBreak'判断

// 复制黏贴不会带有格式

contenteditable="plaintext-only"

contenteditable="true" // 换行会生成

包裹

contenteditable="false"

user-modifyCSS属性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之前

通过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.typeNone: 当前没有选择。

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()

}

代码示例插入的内容只能是documentFragmentconst { 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()

}

}

}

}

预览

获取要预览元素的HTML,借助iframe.srcdoc属性进行预览。

若要对预览样式进行定制,需要对previewContent追加内联样式...

computed: {

showContent(){

return `

img,

video {

max-width: 100%;

}

.host {

width:100%;

overflow: hidden;

font-size: 28px;

text-align: justify;

word-break: break-all;

}

${this.previewHtml}

`

}

},

...

长度限制非中文状态下// 非中文状态下

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片段也是字符串,会带有样式。

拖拽上传图片

class="video-wrap"

@dragenter.stop.prevent

@dragover.stop.prevent

@drop.stop.prevent="onFileDrop"

>

点击或拖动上传视频

编码格式为h264且后缀为.mp4的视频

...

methods: {

...

onFileChange (e) {

console.log(e.target.files[0])

},

onFileDrop (e) {

const file = e.dataTransfer.files && e.dataTransfer.files[0]

console.log(file)

}

...

}

doc转HTMLvar 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时,元素节点必须要有文本内容,否则一堆世界未解之谜。

删除有样式的文本时(常见于插入回车),浏览器会自动生成追加样式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()从文档中删除所选择的内容

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值