一个基于react的图片裁剪组件示例


Posted in Javascript onApril 18, 2018

开始

写了一年多vue,感觉碰到了点瓶颈,学习下react找找感觉。刚好最近使用vue写了个基于cropperJS的图片裁剪的组件,便花费了几个晚上的功夫用react再写一遍。代码地址

项目是使用create-react-app来开发的,省去了很多webpack配置的功夫,支持eslint,自动刷新等功能,使用前npm install并npm start即可。推荐同样是新学习react的人也用用看。

项目写的比较简陋,自定义配置比较差,不过也是完成了裁剪图片的基本功能,希望可以帮助到初学react和想了解裁剪图片组件的朋友。

组件的结构是这样的。

<!--Cropper-->

   <div>
   <ImageUploader handleImgChange={this.handleImgChange} getCropData={this.getCropData}/>
    <div className="image-principal">
     <img src={this.state.imageValue} alt="" className="img" ref="img" onLoad={this.setSize}/>
     <SelectArea ref="selectArea"></SelectArea>
    </div>
   </div>
<!--ImageUploader   -->
   <form className="image-upload-form" method="post" encType="multipart/form-data" >
    <input type="file" name="inputOfFile" ref="imgInput" id="imgInput" onChange={this.props.handleImgChange}/>
    <button onClick={this.props.getCropData}>获取裁剪参数</button>
   </form>
<!--SelectArea   -->
   <div className="select-area" onMouseDown={ this.dragStart} ref="selectArea" >
    <div className="top-resize" onMouseDown={ event => this.resizeStart(event, 'top')}></div>
    <div className="right-resize" onMouseDown={ event => this.resizeStart(event, 'right')}></div>
    <div className="bottom-resize" onMouseDown={ event => this.resizeStart(event, 'bottom')}></div>
    <div className="left-resize" onMouseDown={ event => this.resizeStart(event, 'left')}></div>
    <div className="right-bottom-resize" onMouseDown={ event => this.resizeStart(event, 'right')}></div>
    <div className="left-top-resize" onMouseDown={ event => this.resizeStart(event, 'left')}></div>
   </div>

ImageUploader & Cropper

ImageUploader主要做的就是上传图片,监听了input的change事件,并调用了父组件Cropper的的handleImgChange方法,该方法设置了绑定到img元素的imageValue,会使得img元素出发load事件。

handleImgChange = e => {
  let fileReader = new FileReader()
  fileReader.readAsDataURL(e.target.files[0])
  fileReader.onload = e => {
   this.setState({...this.state, imageValue: e.target.result})
  }
 }

load事件触发了Cropper的setSize方法,该方法可以设置了图片和裁剪选择框的初始位置和大小。目前裁剪选择框是默认设置是大小为图片的80%,中间显示。

setSize = () => {
  let img = this.refs.img
  let widthNum = parseInt(this.props.width, 10)
  let heightNum = parseInt(this.props.height, 10)
  this.setState({
   ...this.state,
   naturalSize: {
    width: img.naturalWidth,
    height: img.naturalHeight
   }
  })
  let imgStyle = img.style
  imgStyle.height = 'auto'
  imgStyle.width = 'auto'
  let principalStyle = ReactDOM.findDOMNode(this.refs.selectArea).parentElement.style
  const ratio = img.width / img.height
  // 设置图片大小、位置
  if (img.width > img.height) {
   imgStyle.width = principalStyle.width = this.props.width
   imgStyle.height = principalStyle.height = widthNum / ratio + 'px'
   principalStyle.marginTop = (widthNum - parseInt(principalStyle.height, 10)) / 2 + 'px'
   principalStyle.marginLeft = 0
  } else {
   imgStyle.height = principalStyle.height = this.props.height
   imgStyle.width = principalStyle.width = heightNum * ratio + 'px'
   principalStyle.marginLeft = (heightNum - parseInt(principalStyle.width, 10)) / 2 + 'px'
   principalStyle.marginTop = 0
  }
  // 设置选择框样式
  let selectAreaStyle = ReactDOM.findDOMNode(this.refs.selectArea).style
  let principalHeight = parseInt(principalStyle.height, 10)
  let principalWidth = parseInt(principalStyle.width, 10)
  if (principalWidth > principalHeight) {
   selectAreaStyle.top = principalHeight * 0.1 + 'px'
   selectAreaStyle.width = selectAreaStyle.height = principalHeight * 0.8 + 'px'
   selectAreaStyle.left = (principalWidth - parseInt(selectAreaStyle.width, 10)) / 2 + 'px'
  } else {
   selectAreaStyle.left = principalWidth * 0.1 + 'px'
   selectAreaStyle.width = selectAreaStyle.height = principalWidth * 0.8 + 'px'
   selectAreaStyle.top = (principalHeight - parseInt(selectAreaStyle.height, 10)) / 2 + 'px'
  }
 }

Cropper上还有一个getCropData方法,方法会打印并返回裁剪参数,

getCropData = e => {
  e.preventDefault()
  let SelectArea = ReactDOM.findDOMNode(this.refs.selectArea).style

  let a = {
   width: parseInt(SelectArea.width, 10),
   height: parseInt(SelectArea.height, 10),
   left: parseInt(SelectArea.left, 10),
   top: parseInt(SelectArea.top, 10)
  }
  a.radio = this.state.naturalSize.width / a.width

  console.log(a)
  return a
 }

SelectArea

重新放一遍selectArea的结构。要注意,.top-resize的cursor属性是 n-resize,而和left,right,bottom对应的分别是w-resize,e-resize,s-resize

<div className="select-area" onMouseDown={ this.dragStart} ref="selectArea" >
    <div className="top-resize" onMouseDown={ event => this.resizeStart(event, 'top')}></div>
    <div className="right-resize" onMouseDown={ event => this.resizeStart(event, 'right')}></div>
    <div className="bottom-resize" onMouseDown={ event => this.resizeStart(event, 'bottom')}></div>
    <div className="left-resize" onMouseDown={ event => this.resizeStart(event, 'left')}></div>
    <div className="right-bottom-resize" onMouseDown={ event => this.resizeStart(event, 'right')}></div>
    <div className="left-top-resize" onMouseDown={ event => this.resizeStart(event, 'left')}></div>
   </div>

selectArea的state值设为这样,selectArea保存拖拽选择框时的参数,resizeArea保存裁剪选择框时的参数,container为.image-principal元素,el为触发事件时的event.target。

this.state = {
   selectArea: null,
   el: null,
   container: null,
   resizeArea: null
  }

拖拽选择框

在.select-area按下鼠标,触发mouseDown事件,调用dragStart方法。

使用method = e => {}的形式可以避免在jsx中使用this.method.bind(this)

在这个方法中,首先保存按下鼠标时的鼠标位置,裁剪框与图片的相对距离和裁剪框的最大位移距离,接着添加事件监听

dragStart = e => {
  const el = e.target
  const container = this.state.container
  let selectArea = {
   posLeft: e.clientX,
   posTop: e.clientY,
   left: e.clientX - el.offsetLeft,
   top: e.clientY - el.offsetTop,
   maxMoveX: container.offsetWidth - el.offsetWidth,
   maxMoveY: container.offsetHeight - el.offsetHeight,
  }
  this.setState({ ...this.state, selectArea, el})
  document.addEventListener('mousemove', this.moveBind, false)
  document.addEventListener('mouseup', this.stopBind, false)
 }

moveBind和stopBind来自于

this.moveBind = this.move.bind(this)
 this.stopBind = this.stop.bind(this)

move方法,在鼠标移动中根据记录新的鼠标位置来计算新的相对位置newPosLeft和newPosTop,并控制该值在合理范围内

move(e) {
  if (!this.state || !this.state.el || !this.state.selectArea) {
   return
  }
  let selectArea = this.state.selectArea
  let newPosLeft = e.clientX- selectArea.left
  let newPosTop = e.clientY - selectArea.top
  // 控制移动范围
  if (newPosLeft <= 0) {
   newPosLeft = 0
  } else if (newPosLeft > selectArea.maxMoveX) {
   newPosLeft = selectArea.maxMoveX
  }
  if (newPosTop <= 0) {
   newPosTop = 0
  } else if (newPosTop > selectArea.maxMoveY) {
   newPosTop = selectArea.maxMoveY
  }
  let elStyle = this.state.el.style
  elStyle.left = newPosLeft + 'px'
  elStyle.top = newPosTop + 'px'
 }

stop方法,移除事件监听,清除state,避免方法错误调用

stop() {
  document.removeEventListener('mousemove', this.moveBind , false)
  document.removeEventListener('mousemove', this.resizeBind , false)
  document.removeEventListener('mouseup', this.stopBind, false)
  this.setState({...this.state, el: null, resizeArea: null, selectArea: null})
 }

裁剪选择框

跟拖拽一样,首先调用resizeStart方法,保存开始裁剪的鼠标位置,裁剪框的尺寸和位置,添加关于resizeBind和stopBind的事件监听,注意,由于react的事件机制特点,需要使用stopPropagation来禁止事件冒泡,事件监听的第三个参数使用false是无效的。

resizeStart = (e, type) => {
  e.stopPropagation()
  const el = e.target.parentElement
  let resizeArea = {
   posLeft: e.clientX,
   posTop: e.clientY,
   width: el.offsetWidth,
   height: el.offsetHeight,
   left: parseInt(el.style.left, 10),
   top: parseInt(el.style.top, 10)
  }
  this.setState({ ...this.state, resizeArea, el})
  this.resizeBind = this.resize.bind(this, type)
  document.addEventListener('mousemove', this.resizeBind, false)
  document.addEventListener('mouseup', this.stopBind, false)
 }

裁剪的方法,将裁剪分为两种情况,一种是右侧,下侧和右下侧的拉伸。另一种是左侧,上侧和左上侧的拉伸。

第一种情况下,选择框的位置是不会变的,只有尺寸会变,处理起来相对简单。新的尺寸大小为原大小加上当前的鼠标的位置再减去开始拖拽处的鼠标的位置,如果宽度或者高度有一个超标了,则将尺寸设置为刚好到边界的大小。均为超标,设置为新的尺寸。

第二种情况下,选择框的位置和大小同时会变,要同时控制尺寸和位置不超出边界。

resize(type, e) {
  if (!this.state || !this.state.el || !this.state.resizeArea) {
   return
  }
  let container = this.state.container
  const containerHeight = container.offsetHeight
  const containerWidth = container.offsetWidth
  const containerLeft = parseInt(container.style.left || 0, 10)
  const containerTop = parseInt(container.style.top || 0, 10)
  let resizeArea = this.state.resizeArea
  let el = this.state.el
  let elStyle = el.style
  if (type === 'right' || type === 'bottom') {
   let length
   if (type === 'right') {
    length = resizeArea.width + e.clientX - resizeArea.posLeft
   } else {
    length = resizeArea.height + e.clientY - resizeArea.posTop
   }
   if (parseInt(el.style.left, 10) + length > containerWidth || parseInt(el.style.top, 10) + length > containerHeight) {
    const w = containerWidth - parseInt(el.style.left, 10)
    const h = containerHeight - parseInt(el.style.top, 10)
    elStyle.width = elStyle.height = Math.min(w, h) + 'px'
   } else {
    elStyle.width = length + 'px'
    elStyle.height = length + 'px'
   }
  } else {
   let posChange
   let newPosLeft
   let newPosTop
   if (type === 'left') {
    posChange = resizeArea.posLeft - e.clientX
   } else {
    posChange = resizeArea.posTop - e.clientY
   }
   newPosLeft = resizeArea.left - posChange
   // 防止过度缩小
   if (newPosLeft > resizeArea.left + resizeArea.width) {
    elStyle.left = resizeArea.left + resizeArea.width + 'px'
    elStyle.top = resizeArea.top + resizeArea.height + 'px'
    elStyle.width = elStyle.height = '2px'
    return
   }
   newPosTop = resizeArea.top - posChange
   // 到达边界
   if (newPosLeft <= containerLeft || newPosTop < containerTop) {
    // 让选择框到图片最左边
    let newPosLeft2 = resizeArea.left -containerLeft
    // 判断顶部会不会超出边界
    if (newPosLeft2 < resizeArea.top) {
     // 未超出边界
     elStyle.top = resizeArea.top - newPosLeft2 + 'px'
     elStyle.left = containerLeft + 'px'
    } else {
     // 让选择框到达图片顶部
     elStyle.top = containerTop + 'px'
     elStyle.left = resizeArea.left + containerTop - resizeArea.top + 'px'
    }
   } else {
    if (newPosLeft < 0) {
     elStyle.left = 0;
     elStyle.width = Math.min(resizeArea.width + posChange - newPosLeft, containerWidth) + 'px'
     elStyle.top = newPosTop - newPosLeft;
     elStyle.height = Math.min(resizeArea.height + posChange - newPosLeft, containerHeight) + 'px'
     return;
    }
    if (newPosTop < 0) {
     elStyle.left = newPosLeft - newPosTop;
     elStyle.width = Math.min(resizeArea.width + posChange - newPosTop, containerWidth) + 'px'
     elStyle.top = 0;
     elStyle.height = Math.min(resizeArea.height + posChange - newPosTop, containerHeight) + 'px'
     return;
    }
    elStyle.left = newPosLeft + 'px'
    elStyle.top = newPosTop + 'px'
    elStyle.width = resizeArea.width + posChange + 'px'
    elStyle.height = resizeArea.height + posChange + 'px'
   }
  }
 }

结束

通过这些组件的编写,感觉想要学好react,需要加深对this和事件模型的了解,这几天在这上面踩了不少的坑。如果觉得这篇文章有帮助的话,欢迎star我的项目

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
改善你的jQuery的25个步骤 千倍级效率提升
Feb 11 Javascript
输入框的字数时时统计—关于 onpropertychange 和 oninput 使用
Oct 21 Javascript
javascript的创建多行字符串的7种方法
Apr 29 Javascript
javascript中拼接HTML字符串的最快、最好的方法
Jun 07 Javascript
JQuery实现动态适时改变字体颜色的方法
Mar 10 Javascript
JavaScript实现向setTimeout执行代码传递参数的方法
Apr 16 Javascript
JavaScript精炼之构造函数 Constructor及Constructor属性详解
Nov 05 Javascript
jQuery 移动端拖拽(模块化开发,触摸事件,webpack)
Oct 28 Javascript
JS如何生成一个不重复的ID的函数
Dec 25 Javascript
原生js实现轮播图的示例代码
Feb 20 Javascript
jQuery布局组件EasyUI Layout使用方法详解
Feb 28 Javascript
基于代数方程库Algebra.js解二元一次方程功能示例
Jun 09 Javascript
JS实现对json对象排序并删除id相同项功能示例
Apr 18 #Javascript
Angular ng-animate和ng-cookies用法详解
Apr 18 #Javascript
JS实现的base64加密解密操作示例
Apr 18 #Javascript
JS实现简单获取最近7天和最近3天日期的方法
Apr 18 #Javascript
详解Node使用Puppeteer完成一次复杂的爬虫
Apr 18 #Javascript
jQuery滚动条美化插件nicescroll简单用法示例
Apr 18 #jQuery
Angular 如何使用第三方库的方法
Apr 18 #Javascript
You might like
十天学会php之第五天
2006/10/09 PHP
用PHP开发GUI
2006/10/09 PHP
PHP 文件类型判断代码
2009/03/13 PHP
PHP开发负载均衡指南
2010/07/17 PHP
php blowfish加密解密算法
2016/07/02 PHP
php实现微信公众号创建自定义菜单功能的实例代码
2019/06/11 PHP
表单填写时用回车代替TAB的实现方法
2007/10/09 Javascript
javascript 写类方式之一
2009/07/05 Javascript
JavaScript 内置对象属性及方法集合
2010/07/04 Javascript
js 字符串转化成数字的代码
2011/06/29 Javascript
jquery打开直接跳到网页最下面、最低端实现代码
2013/04/22 Javascript
多个datatable共存造成多个表格的checkbox都被选中
2013/07/11 Javascript
js+HTML5实现视频截图的方法
2015/06/16 Javascript
javascript随机显示背景图片的方法
2015/06/18 Javascript
jquery实现两边飘浮可关闭的对联广告
2015/11/27 Javascript
jQuery实现的表头固定效果实例【附完整demo源码下载】
2016/08/01 Javascript
JS中script标签defer和async属性的区别详解
2016/08/12 Javascript
CSS+jQuery实现简单的折叠菜单
2016/12/20 Javascript
vue2.0 与 bootstrap datetimepicker的结合使用实例
2017/05/22 Javascript
layui表格数据复选框回显设置方法
2019/09/13 Javascript
Vue axios与Go Frame后端框架的Options请求跨域问题详解
2020/03/03 Javascript
用jQuery实现抽奖程序
2020/04/12 jQuery
Vue实现鼠标经过文字显示悬浮框效果的示例代码
2020/10/14 Javascript
python控制台显示时钟的示例
2014/02/24 Python
python如何实现数据的线性拟合
2019/07/19 Python
Python实现一个带权无回置随机抽选函数的方法
2019/07/24 Python
Python内建序列通用操作6种实现方法
2020/03/26 Python
html2canvas生成的图片偏移不完整的解决方法
2020/05/19 HTML / CSS
Canon佳能美国官方商店:购买数码相机、数码单反相机、镜头和打印机
2016/11/15 全球购物
Bally澳大利亚官网:瑞士奢侈品牌
2018/11/01 全球购物
公务员学习习总书记“三严三实”思想汇报
2014/09/19 职场文书
简易离婚协议书范本
2014/10/24 职场文书
2014年老干部工作总结
2014/11/21 职场文书
2015年销售部工作总结范文
2015/04/27 职场文书
2016年先进班集体事迹材料
2016/02/26 职场文书
评测 | 大屏显示带收音机的高端音箱,JBL TUNE2便携式插卡音箱实测
2021/04/24 无线电