技术标签: 可编辑div 光标 富文本 前端 React 焦点
最近项目中h5端要实现图文上传,而且还要支持用户用户输入的格式,例如换行啥的,那么使用输入控件保存输入内容,图片上传控件就不合适了,因为很难知道用户的输入样式。
如果使用一些现有的富文本编辑器,貌似又不是很划算,所以综合考虑使用div来自己实现一个就是比较理想的方案了。
先来考虑一下,如果使用div来实现简单的富文本编辑器,需要解决哪些问题?
首先,div默认是不可编辑的,需要设置它可以编辑,这个很简单只需要使用
contentEditable="true"
其次,我们需要在div指定的位置插入图片,指定位置就是光标所在的位置,那么我们插入图片的时候,需要知道光标的位置,网上查了下应该是需要getSelection API W3Cschool上面直接有简单的用法,感兴趣的童鞋可以去看看。
获取当前光标选中位置
let selection = window.getSelection();
let range = selection.getRangeAt(0);
那么知道了光标位置,怎么插入图片?
let img = document.createElement("img");
img.src = json.url;
//最大宽度为手机宽度减去20
img.style.maxWidth = (width-20) + 'px';
range.insertNode(img);//选中位置插入图片
这就完了?
当然还没完,实现中遇到了另一个问题,那就是焦点的变化。如果我想插入图片,我就首先需要上传图片,但是当我点击上传图片的按钮的时候,焦点就已经发生了变化,这个时候获取的选中位置就是你点击的位置!
那么该如何解决这个问题呢?
仔细分析,点击的时候,焦点变化的时候,我们需要记录下上次焦点的位置,上次焦点的位置在哪里?在可编辑的div中,焦点变化的时候,就是可编辑div失去焦点的时候!
那么是否可以监听div失焦这个事件呢?答案当然是的!
onBlur={() => {
let selection = window.getSelection();
range = selection.getRangeAt(0);
}}
给div实现onBlur监听就可以了,是不是很简单?
接下来看一下完整的代码
import React, {Component} from "react";
import "../../assets/css/topic/DivEdit.css";
import "../../assets/common/second-common.css";
let range;
let width = document.documentElement.clientWidth;
//头部栏
class DivEdit extends Component {
constructor(props) {
super(props);
let typeArr = [".png", ".jpg", ".jpeg", ".bmp", ".gif"];
this.state = {
id:'',
inputValueHtml: '',
fileType: typeArr,
showTips:true
}
}
//光标定位在可编辑div的开始的位置
setStartFocus() {
document.querySelector('#my-question-define-edit').focus();
}
//光标定位在内容的尾端
setEndFocus() {
let srcObj = document.querySelector('#my-question-define-edit');
let selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(srcObj);
selection.removeAllRanges();
selection.addRange(range);
range.setStart(srcObj, 1);
range.setEnd(srcObj, 1);
}
// 调用input 选择文件
triggerSelect() {
return document.getElementById('topic-add-img').click();
}
//处理上传文件
handleChange(file) {
// 判断文件类型进行上传
if (window.typeMatch(this.state.fileType, file.name)) {
// 上传文件
this.saveFile(file);
} else {
Toast.fail("文件类型不符,请重新选择", 3);
}
}
// 上传文件
saveFile(file) {
//你们自己处理上传的处理 //当成功的时候
let img = document.createElement("img");
img.src = json.url;
img.style.maxWidth = (width-20) + 'px';
if(range) {
range.insertNode(img);
}else{
this.setStartFocus();
let selection = window.getSelection();
range = selection.getRangeAt(0);
range.insertNode(img);
}
}
componentDidMount() {
let question=this.props.question;
this.setState({
inputValueHtml: '<p>' + question + '</p><br/><br/><br/>',showTips:!(question&&question!=='')
},
() => {
if(question&&question!==''){
this.setEndFocus();
}
});
}
getHtml(){
return this._editDiv.innerHTML;
}
render() {
return (
<div className={'add-my-question-first-edit'} >
<div id={'my-question-define-edit'}
onFocus={() => {
this.setState({showTips:false});
}}
onBlur={() => {
let selection = window.getSelection();
range = selection.getRangeAt(0);
}}
onClick={() => {
let selection = window.getSelection();
range = selection.getRangeAt(0);
}}
ref={(editDiv) => this._editDiv = editDiv}
className={'add-my-question-edit'}
contentEditable="true"
dangerouslySetInnerHTML={
{__html: this.state.inputValueHtml}}/>
{
this.state.showTips?
<span style={
{
position: 'absolute',
top:10,
left:10,
marginTop:'1.333rem',
fontSize:17,color:'#E8E8E8'
}}>输入您的问题...</span>:null
}
<div className={'add-bottom-menu'}>
<img style={
{width: 20, position: 'absolute', left: 10}}
src={require("../../assets/images/topic_topic_show.png")}
alt='箭头'/>
<div className={'topic-add-pic'}
onClick={() => {
//调起上传图片
this.triggerSelect();
}}>
<img style={
{width: 20}}
src={require("../../assets/images/topic_upload_pic.png")}
alt='上传图片'/>
<span style={
{marginLeft: 10, fontSize: 15}}>上传图片</span>
</div>
</div>
<div style={
{visibility: 'hidden'}}>
<input
type="file"
id={'topic-add-img'}
style={
{display: "none"}}
onChange={e => this.handleChange(e.target.files[0])}
/>
</div>
</div>
);
}
}
export default DivEdit;
样式表
.add-my-question-first-edit {
display: flex;
flex-direction: column;
flex: 1;
}
.add-my-question-first-edit .add-my-question-edit{
flex: 1;
margin-top: 1.333rem;
/*margin-bottom: 1.333rem;*/
font-size: 0.453rem;
background-color: #fff;
padding: 10px 10px 1.867rem;
}
.add-my-question-first-edit .add-bottom-menu{
position: fixed;
bottom: 0;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 1.067rem;
background-color: #fff;
border-top: #E8E8E8 solid 1px;
}
.add-my-question-first-edit .add-bottom-menu .topic-add-pic{
display: flex;
flex-direction: row;
align-items: center;
align-self: center;
}
更多内容,欢迎同步关注作者公众号二维码!
程序员内功修炼手册 主要发布计算机基础、设计模式、计算机网络基础知识,同时重点关注大前端知识
Android、iOS、web前端、Flutter、React Native等,想学习大前端知识的速度来吧,一起学习、一起成长!
文章浏览阅读122次。还是A+BTime Limit: 2000/1000 MS (Java/Others)Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 24568Accepted Submission(s): 11729Problem Description读入两个小于10000的正整数A和B,计算A+B。...
文章浏览阅读419次。HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息。FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。BASIC:仅记录请求的方法,URL以及响应状态码和执行时间。NONE:不记录任何日志信息,这是默认值。配置Feign日志有两种方式;方式二:java代码实现。注解中声明则代表某服务。方式一:配置文件方式。_feign 日志设置
文章浏览阅读155次。将容器管理的持久性 Bean 用于面向服务的体系结构本文将介绍如何使用 IBM WebSphere Process Server 对容器管理的持久性 (CMP) Bean的连接和持久性逻辑加以控制,使其可以存储在非关系数据库..._javax.ejb.objectnotfoundexception: no such entity!
文章浏览阅读1.5k次。基础java练习题一、递归实现跳台阶从第一级跳到第n级,有多少种跳法一次可跳一级,也可跳两级。还能跳三级import java.math.BigDecimal;import java.util.Scanner;public class Main{ public static void main(String[]args){ Scanner reader=new Scanner(System.in); while(reader.hasNext()){ _java 递归例题
文章浏览阅读1.5k次,点赞6次,收藏6次。目录1.串应用- 计算一个串的最长的真前后缀题目描述输入输出样例输入样例输出题解2.字符串替换(string)题目描述输入输出样例输入样例输出题解3.可重叠子串 (Ver. I)题目描述输入输出样例输入样例输出题解4.字符串操作(string)题目描述输入输出样例输入样例输出题解1.串应用- 计算一个串的最长的真前后缀题目描述给定一个串,如ABCDAB,则ABCDAB的真前缀有:{ A, AB,ABC, ABCD, ABCDA }ABCDAB的真后缀有:{ B, AB,DAB, CDAB, BCDAB_对存储在string数组内的所有以字符‘a’开始并以字符‘e’结尾的单词做加密处理。
文章浏览阅读68次。西安交通大学/算法设计与问题求解/树与二叉树/MOOC_算法设计与问题求解西安交通大学
文章浏览阅读1.6k次。问题:在Vue项目中出现如下错误提示:[Vue warn]: Computed property "totalPrice" was assigned to but it has no setter. (found in <Anonymous>)代码:<input v-model="totalPrice"/>原因:v-model命令,因Vue 的双向数据绑定原理 , 会自动操作 totalPrice, 对其进行set 操作而 totalPrice 作为计..._computed property "totalprice" was assigned to but it has no setter.
文章浏览阅读60次。十分暴力而简洁的解决方式:读取P和T的位置并自动生成唯一正确答案,将题给测点与之对比,不一样就给我爬!_basic 1003 case 1
文章浏览阅读422次。原标题:详解将Web项目War包部署到Tomcat服务器基本步骤详解将Web项目War包部署到Tomcat服务器基本步骤1 War包War包一般是在进行Web开发时,通常是一个网站Project下的所有源码的集合,里面包含前台HTML/CSS/JS的代码,也包含Java的代码。当开发人员在自己的开发机器上调试所有代码并通过后,为了交给测试人员测试和未来进行产品发布,都需要将开发人员的源码打包成Wa..._/opt/bosssoft/war/medical-web.war/web-inf/web.xml of module medical-web.war.
文章浏览阅读3k次,点赞3次,收藏13次。# -*- coding: utf-8 -*-# 简述:这里有四个数字,分别是:1、2、3、4#提问:能组成多少个互不相同且无重复数字的三位数?各是多少?def f(n):list=[]count=0for i in range(1,n+1):for j in range(1, n+1):for k in range(1, n+1):if i!=j and j!=k and i!=k:list.a..._python求从0到9任意组合成三位数数字不能重复并输出
文章浏览阅读1k次,点赞3次,收藏2次。<el-table-column prop="studentSex" label="性别" :formatter="sex"></el-table-column>然后就在vue的methods中写方法就OK了methods: { sex(row,index){ if(row.studentSex == 1){ return '男'; }else{ return '女'; }..._elementui table 性别
文章浏览阅读1.1k次。java文件操作之移动文件到指定的目录_java中怎么将pro.txt移动到design_mode_code根目录下