前置知识
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;
1000px;
.nav {
display: flex;
button {
cursor: pointer;
}
&__img {
position: relative;
input {
100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
}
}
.row {
display: flex;
100%;
height: 300px;
}
.editor {
flex: 1;
position: relative;
margin-right: 20px;
padding: 10px;
outline: none;
border: 1px solid #000;
overflow-y: scroll;
img {
max- 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`,
`${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',
'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
}
}
结语
回复“加群”与大佬们一起交流学习~