一个基于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 相关文章推荐
splice slice区别
Oct 09 Javascript
Discuz! 6.1_jQuery兼容问题
Sep 23 Javascript
JavaScript 继承详解 第一篇
Aug 30 Javascript
简约JS日历控件 实例代码
Jul 12 Javascript
JS小功能(checkbox实现全选和全取消)实例代码
Nov 28 Javascript
javascript弹出窗口实现代码
Nov 12 Javascript
javascript获取以及设置光标位置
Feb 16 Javascript
VUE2实现事件驱动弹窗示例
Oct 21 Javascript
详解Webpack实战之构建 Electron 应用
Dec 25 Javascript
vue-cli2.x项目优化之引入本地静态库文件的方法
Jun 19 Javascript
详解关于element el-button使用$attrs的一个注意要点
Nov 09 Javascript
React中使用Vditor自定义图片详解
Dec 25 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/11/19 PHP
php 不使用js实现页面跳转
2014/02/11 PHP
php实现socket推送技术的示例
2017/12/20 PHP
PHP常见加密函数用法示例【crypt与md5】
2019/01/27 PHP
IE autocomplete internet explorer's autocomplete
2007/06/30 Javascript
用Mootools获得操作索引的两种方法分享
2011/12/12 Javascript
Jquery 动态生成表格示例代码
2013/12/24 Javascript
兼容主流浏览器的JS复制内容到剪贴板
2014/12/12 Javascript
js中for in语句的用法讲解
2015/04/24 Javascript
apply和call方法定义及apply和call方法的区别
2015/11/15 Javascript
基于MVC+EasyUI的web开发框架之使用云打印控件C-Lodop打印页面或套打报关运单信息
2016/08/29 Javascript
js获取Get值的方法
2016/09/29 Javascript
Jquery和Js获得元素标签名称的方法总结
2016/10/08 Javascript
bootstrap datepicker插件默认英文修改为中文
2017/07/28 Javascript
10分钟彻底搞懂Http的强制缓存和协商缓存(小结)
2018/08/30 Javascript
浅析微信扫码登录原理(小结)
2018/10/29 Javascript
VUE简单的定时器实时刷新的实现方法
2019/01/20 Javascript
Egg Vue SSR 服务端渲染数据请求与asyncData
2019/11/24 Javascript
axios如何取消重复无用的请求详解
2019/12/15 Javascript
小程序富文本提取图片可放大缩小
2020/05/26 Javascript
python实现排序算法
2014/02/14 Python
Python入门篇之编程习惯与特点
2014/10/17 Python
Python字符串处理函数简明总结
2015/04/13 Python
在Django的session中使用User对象的方法
2015/07/23 Python
对python周期性定时器的示例详解
2019/02/19 Python
pandas读取CSV文件时查看修改各列的数据类型格式
2019/07/07 Python
django formset实现数据表的批量操作的示例代码
2019/12/06 Python
Python netmiko模块的使用
2020/02/14 Python
基于CSS3特效之动画:animation的应用
2013/05/09 HTML / CSS
迪拜航空官方网站:flydubai
2017/04/20 全球购物
严选全球尖货,立足香港:Bonpont宝盆
2018/07/24 全球购物
德国最大的婴儿用品网上商店:Kidsroom.de(支持中文)
2020/09/02 全球购物
AJAX都有哪些有点和缺点
2012/11/03 面试题
医药营销专业个人自荐信
2013/09/29 职场文书
消防安全检查制度
2014/02/04 职场文书
刑事附带民事起诉状
2015/05/19 职场文书