【Web技术】421- 富文本原理介绍
前置知识
contenteditable 属性
<div contenteditable="true"></div><div contenteditable="true"> <p>这是可编辑的</p> <p contenteditable="false">这是不可编辑的</p></div>document.execCommand 方法
// document.execCommand(命令名称,是否展示用户界面,命令需要的额外参数)document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)// 加粗document.execCommand('bold', false, null);// 添加图片document.execCommand('insertImage', false, url || base64);// 把一段文字用 p 标签包裹起来document.execCommand('formatblock', false, '<p>');Selection 和 Range 对象
所以通常我们可以用 let range = window.getSelection().getRangeAt(0) 来获取选中的内容信息(getRangeAt 接受一个索引值,因为会有多个 Range,而现在只有一个,所以写0)。
看得一头雾水?????没关系,看下面两张图就懂了????:
这个知识点是很重要的,因为它让我们有了操纵光标的能力(比如插入内容之后设置光标的位置),不过这篇文章中我并没有去深入它,只是浅出????。
目标
起步
<template> <div class="xr-editor"> <!--按钮区--> <div class="nav"> <button>加粗</button> ... </div> <!--编辑区--> <div class="editor" contenteditable="true"></div> </div></template><!--全部样式就这些,这里就都先给出来了--><style lang="scss">.xr-editor { margin: 50px auto; width: 1000px; .nav { display: flex; button { cursor: pointer; } &__img { position: relative; input { width: 100%; height: 100%; position: absolute; left: 0; top: 0; opacity: 0; } } } .row { display: flex; width: 100%; height: 300px; } .editor { flex: 1; position: relative; margin-right: 20px; padding: 10px; outline: none; border: 1px solid #000; overflow-y: scroll; img { max-width: 300px; max-height: 300px; vertical-align: middle; } } .content { flex: 1; border: 1px solid #000; word-break: break-all; word-wrap: break-word; overflow: scroll; }}</style>加粗
<template> <div class="nav"> <button @click="execCommand">加粗</button> </div> ...</template><script>export default { name: 'XrEditor', methods: { execCommand() { document.execCommand('bold', false, null); } }};</script><template> <div class="nav"> <button @click="execCommand('bold')">加粗</button> </div> ...</template><script>export default { name: 'XrEditor', methods: { execCommand(name, args = null) { document.execCommand(name, false, args); } }};</script><button @click="execCommand('insertUnorderedList')">无序列表</button><button @click="execCommand('insertHorizontalRule')">水平线</button><button @click="execCommand('undo')">后退</button><button @click="execCommand('redo')">前进</button>段落
<template> <div class="xr-editor"> <div class="nav"> <button @click="execCommand('bold')">加粗</button> <button @click="execCommand('formatBlock', '<p>')">段落</button> </div> <div class="row"> <div class="editor" contenteditable="true" @input="print"></div> <div class="content">{{ html }}</div> </div> </div></template><script>export default { name: 'XrEditor', data() { return { html: '' }; }, methods: { execCommand(name, args = null) { document.execCommand(name, false, args); }, print() { this.html = document.querySelector('.editor').innerHTML; } }};</script>插入链接
<button @click="createLink">链接</button>createLink() { let url = window.prompt('请输入链接地址'); if (url) this.execCommand('createLink', url);}insertImgLink() { let url = window.prompt('请输入图片地址'); if (url) this.execCommand('insertImage', url);}插入图片
<button class="nav__img">插入图片 <!--这个 input 是隐藏的--> <input type="file" accept="image/gif, image/jpeg, image/png" @change="insertImg"></button>insertImg(e) { let reader = new FileReader(); let file = e.target.files[0]; reader.onload = () => { let base64Img = reader.result; this.execCommand('insertImage', base64Img); document.querySelector('.nav__img input').value = ''; // 解决同一张图片上传无效的问题 }; reader.readAsDataURL(file);}????至此,一个简易版的富文本就完成了(当然了 bug 也是有的????,不过并不妨碍我们理解),具体代码可以参考 npm 上的 pell 包,它已经是个极简版的了。
进阶
图片拉伸
1. 判断用户点击的是否是编辑区里面的图片
mounted() { this.editor = document.querySelector('.editor'); this.editor.addEventListener('click', this.handleClick);},methods: { handleClick(e) { if ( e.target && e.target.tagName && e.target.tagName.toUpperCase() === 'IMG' ) { this.handleClickImg(e.target); } }}2. 在点击的图片上创建个大小一样的 div
handleClickImg(img) { this.nowImg = img; this.showOverlay();}showOverlay() { // 添加蒙层 this.overlay = document.createElement('div'); this.editor.appendChild(this.overlay); // 定位蒙层和大小 this.repositionOverlay();},repositionOverlay() { let imgRect = this.nowImg.getBoundingClientRect(); let editorRect = this.editor.getBoundingClientRect(); // 设置蒙层宽高和位置 Object.assign(this.overlay.style, { position: 'absolute', top: `${imgRect.top - editorRect.top + this.editor.scrollTop}px`, left: `${imgRect.left - editorRect.left - 1 + this.editor.scrollLeft}px`, width: `${imgRect.width}px`, height: `${imgRect.height}px`, boxSizing: 'border-box', border: '1px dashed red' }); // 添加四个顶点拖拽框 this.createBox();},createBox() { this.boxes = []; this.addBox('nwse-resize'); // top left this.addBox('nesw-resize'); // top right this.addBox('nwse-resize'); // bottom right this.addBox('nesw-resize'); // bottom left this.positionBoxes(); // 设置四个拖拽框位置},addBox(cursor) { const box = document.createElement('div'); Object.assign(box.style, { position: 'absolute', height: '12px', width: '12px', backgroundColor: 'white', border: '1px solid #777', boxSizing: 'border-box', opacity: '0.80' }); box.style.cursor = cursor; box.addEventListener('mousedown', this.handleMousedown); // 顺便添加事件 this.overlay.appendChild(box); this.boxes.push(box);},positionBoxes() { let handleXOffset = `-6px`; let handleYOffset = `-6px`; [{ left: handleXOffset, top: handleYOffset }, { right: handleXOffset, top: handleYOffset }, { right: handleXOffset, bottom: handleYOffset }, { left: handleXOffset, bottom: handleYOffset }].forEach((pos, idx) => { Object.assign(this.boxes[idx].style, pos); });},3. 在四个顶点框上添加拖拽事件
handleMousedown(e) { this.dragBox = e.target; this.dragStartX = e.clientX; this.preDragWidth = this.nowImg.width; this.setCursor(this.dragBox.style.cursor); document.addEventListener('mousemove', this.handleDrag); document.addEventListener('mouseup', this.handleMouseup);},handleDrag(e) { // 计算水平拖动距离 const deltaX = e.clientX - this.dragStartX; // 修改图片大小 if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[3]) { // 左边的两个框 this.nowImg.width = Math.round(this.preDragWidth - deltaX); } else { // 右边的两个框 this.nowImg.width = Math.round(this.preDragWidth + deltaX); } // 同时修改蒙层大小 this.repositionOverlay();},handleMouseup() { this.setCursor(''); // 拖拽结束移除事件监听 document.removeEventListener('mousemove', this.handleDrag); document.removeEventListener('mouseup', this.handleMouseup);},setCursor(value) { // 设置鼠标样式 [document.body, this.nowImg].forEach(el => { el.style.cursor = value; });}操纵光标
所以我们需要具有控制光标的能力,具体操作就是在点击按钮之前我们可以先存储当前光标的状态,执行完命令或者在需要的时候后再还原或设置光标的状态即可。由于在 chrome 中,失去焦点并不会清除 Seleciton 对象和 Range 对象,所以就像我一开始说的我没怎么去了解????。。。这里就只简要展示两个方法给大家看下:
function saveSelection() { // 保存当前Range对象 let selection = window.getSelection(); if(selection.rangeCount > 0){ return sel.getRangeAt(0); } return null;};let selectedRange = saveSelection();function restoreSelection() { let selection = window.getSelection(); if (selectedRange) { selection.removeAllRanges(); // 清空所有 Range 对象 selection.addRange(selectedRange); // 恢复保存的 Range }}结语
回复“加群”与大佬们一起交流学习~
个人博客:http://www.pingan8787.com
微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。
目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!

浙公网安备 33010602011771号