一個(gè)基于react的圖片裁剪組件示例
開(kāi)始
寫(xiě)了一年多vue,感覺(jué)碰到了點(diǎn)瓶頸,學(xué)習(xí)下react找找感覺(jué)。剛好最近使用vue寫(xiě)了個(gè)基于cropperJS的圖片裁剪的組件,便花費(fèi)了幾個(gè)晚上的功夫用react再寫(xiě)一遍。代碼地址
項(xiàng)目是使用create-react-app來(lái)開(kāi)發(fā)的,省去了很多webpack配置的功夫,支持eslint,自動(dòng)刷新等功能,使用前npm install并npm start即可。推薦同樣是新學(xué)習(xí)react的人也用用看。
項(xiàng)目寫(xiě)的比較簡(jiǎn)陋,自定義配置比較差,不過(guò)也是完成了裁剪圖片的基本功能,希望可以幫助到初學(xué)react和想了解裁剪圖片組件的朋友。
組件的結(jié)構(gòu)是這樣的。
<!--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}>獲取裁剪參數(shù)</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主要做的就是上傳圖片,監(jiān)聽(tīng)了input的change事件,并調(diào)用了父組件Cropper的的handleImgChange方法,該方法設(shè)置了綁定到img元素的imageValue,會(huì)使得img元素出發(fā)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事件觸發(fā)了Cropper的setSize方法,該方法可以設(shè)置了圖片和裁剪選擇框的初始位置和大小。目前裁剪選擇框是默認(rèn)設(shè)置是大小為圖片的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
// 設(shè)置圖片大小、位置
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
}
// 設(shè)置選擇框樣式
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上還有一個(gè)getCropData方法,方法會(huì)打印并返回裁剪參數(shù),
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的結(jié)構(gòu)。要注意,.top-resize的cursor屬性是 n-resize,而和left,right,bottom對(duì)應(yīng)的分別是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值設(shè)為這樣,selectArea保存拖拽選擇框時(shí)的參數(shù),resizeArea保存裁剪選擇框時(shí)的參數(shù),container為.image-principal元素,el為觸發(fā)事件時(shí)的event.target。
this.state = {
selectArea: null,
el: null,
container: null,
resizeArea: null
}
拖拽選擇框
在.select-area按下鼠標(biāo),觸發(fā)mouseDown事件,調(diào)用dragStart方法。
使用method = e => {}的形式可以避免在jsx中使用this.method.bind(this)
在這個(gè)方法中,首先保存按下鼠標(biāo)時(shí)的鼠標(biāo)位置,裁剪框與圖片的相對(duì)距離和裁剪框的最大位移距離,接著添加事件監(jiān)聽(tīng)
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來(lái)自于
this.moveBind = this.move.bind(this) this.stopBind = this.stop.bind(this)
move方法,在鼠標(biāo)移動(dòng)中根據(jù)記錄新的鼠標(biāo)位置來(lái)計(jì)算新的相對(duì)位置newPosLeft和newPosTop,并控制該值在合理范圍內(nèi)
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
// 控制移動(dòng)范圍
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方法,移除事件監(jiān)聽(tīng),清除state,避免方法錯(cuò)誤調(diào)用
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})
}
裁剪選擇框
跟拖拽一樣,首先調(diào)用resizeStart方法,保存開(kāi)始裁剪的鼠標(biāo)位置,裁剪框的尺寸和位置,添加關(guān)于resizeBind和stopBind的事件監(jiān)聽(tīng),注意,由于react的事件機(jī)制特點(diǎn),需要使用stopPropagation來(lái)禁止事件冒泡,事件監(jiān)聽(tīng)的第三個(gè)參數(shù)使用false是無(wú)效的。
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)
}
裁剪的方法,將裁剪分為兩種情況,一種是右側(cè),下側(cè)和右下側(cè)的拉伸。另一種是左側(cè),上側(cè)和左上側(cè)的拉伸。
第一種情況下,選擇框的位置是不會(huì)變的,只有尺寸會(huì)變,處理起來(lái)相對(duì)簡(jiǎn)單。新的尺寸大小為原大小加上當(dāng)前的鼠標(biāo)的位置再減去開(kāi)始拖拽處的鼠標(biāo)的位置,如果寬度或者高度有一個(gè)超標(biāo)了,則將尺寸設(shè)置為剛好到邊界的大小。均為超標(biāo),設(shè)置為新的尺寸。
第二種情況下,選擇框的位置和大小同時(shí)會(huì)變,要同時(shí)控制尺寸和位置不超出邊界。
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
// 防止過(guò)度縮小
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
// 到達(dá)邊界
if (newPosLeft <= containerLeft || newPosTop < containerTop) {
// 讓選擇框到圖片最左邊
let newPosLeft2 = resizeArea.left -containerLeft
// 判斷頂部會(huì)不會(huì)超出邊界
if (newPosLeft2 < resizeArea.top) {
// 未超出邊界
elStyle.top = resizeArea.top - newPosLeft2 + 'px'
elStyle.left = containerLeft + 'px'
} else {
// 讓選擇框到達(dá)圖片頂部
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'
}
}
}
結(jié)束
通過(guò)這些組件的編寫(xiě),感覺(jué)想要學(xué)好react,需要加深對(duì)this和事件模型的了解,這幾天在這上面踩了不少的坑。如果覺(jué)得這篇文章有幫助的話,歡迎star我的項(xiàng)目
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- React Native使用fetch實(shí)現(xiàn)圖片上傳的示例代碼
- React Native 圖片查看組件的方法
- react實(shí)現(xiàn)一個(gè)優(yōu)雅的圖片占位模塊組件詳解
- React中上傳圖片到七牛的示例代碼
- 探究react-native 源碼的圖片緩存問(wèn)題
- React+react-dropzone+node.js實(shí)現(xiàn)圖片上傳的示例代碼
- react native實(shí)現(xiàn)往服務(wù)器上傳網(wǎng)絡(luò)圖片的實(shí)例
- ReactNative實(shí)現(xiàn)圖片上傳功能的示例代碼
- React+ajax+java實(shí)現(xiàn)上傳圖片并預(yù)覽功能
- 基于Node的React圖片上傳組件實(shí)現(xiàn)實(shí)例代碼
相關(guān)文章
React中使用dnd-kit實(shí)現(xiàn)拖曳排序功能
在這篇文章中,我將帶著大家一起探究React中使用dnd-kit實(shí)現(xiàn)拖曳排序功能,由于前陣子需要在開(kāi)發(fā) Picals 的時(shí)候,需要實(shí)現(xiàn)一些拖動(dòng)排序的功能,文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06
基于visual studio code + react 開(kāi)發(fā)環(huán)境搭建過(guò)程
今天通過(guò)本文給大家分享基于visual studio code + react 開(kāi)發(fā)環(huán)境搭建過(guò)程,本文給大家介紹的非常詳細(xì),包括react安裝問(wèn)題及安裝 Debugger for Chrome的方法,需要的朋友跟隨小編一起看看吧2021-07-07
React使用TailwindCSS的實(shí)現(xiàn)示例
TailwindCSS是一個(gè)實(shí)用優(yōu)先的CSS框架,本文主要介紹了React使用TailwindCSS的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12
React Native 中實(shí)現(xiàn)確認(rèn)碼組件示例詳解
這篇文章主要為大家介紹了React Native中實(shí)現(xiàn)確認(rèn)碼組件示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
探討JWT身份校驗(yàn)與React-router無(wú)縫集成
這篇文章主要為大家介紹了JWT身份校驗(yàn)與React-router無(wú)縫集成的探討解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
react的ui庫(kù)antd中form表單使用SelectTree反顯問(wèn)題及解決
這篇文章主要介紹了react的ui庫(kù)antd中form表單使用SelectTree反顯問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
Remix集成antd和pro-components的過(guò)程示例
這篇文章主要為大家介紹了Remix集成antd和pro-components的過(guò)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

