最近在開發一個思惟導圖的庫blink-mind-react,在開發這個庫的過程當中,因爲思惟導圖區域須要能夠在視圖裏面自由移動,實現DragScroll效果。因此開發了這麼一個組件。css
先看最終要的效果react
經過按下鼠標移動能夠將思惟導圖拖動到視口中任何一個位置git
因爲思惟導圖控件有些許複雜,我寫了一個更簡單的demo, 僅僅拖動一個正方形色塊github
想要運行demo,在這個庫的目錄下運行瀏覽器
yarn storybook
複製代碼
這個組件的代碼在這裏bash
給要封裝的DragScroll組件取一個名字叫作DragScrollWidget,在拖動正方形色塊的例子中,demo的代碼以下:函數
import * as React from "react";
import { DragScrollWidget } from "../../src/component/common/DragScrollWidget";
export default class Demo2 extends React.Component {
render() {
return (
<DragScrollWidget>
{() => (
<div style={{ width: 100, height: 100, backgroundColor: "yellow" }} />
)}
</DragScrollWidget>
);
}
}
複製代碼
爲何咱們封裝的DragScrollWidget,示例代碼中傳給DragScrollWidget的children是一個函數而不是一個React.Element呢,寫成下面這樣呢?ui
<DragScrollWidget>
<div style={{ width: 100, height: 100, backgroundColor: "yellow" }} />
</DragScrollWidget>
複製代碼
這是由於在實現思惟導圖的過程當中一些特別的需求須要,暫且不表。this
下面分析DragScrollWidget內部是怎麼實現的,DragScrollWidget的render() 函數以下:spa
render() {
return (
<div
ref={this.viewBoxRef}
onMouseDown={this.onMouseDown}
className="drag-scroll-view"
>
<div style={this.state.widgetStyle} ref={this.bigViewRef}>
<div
className="drag-scroll-content"
ref={this.contentRef}
style={this.state.contentStyle}
>
{this.props.children(
this.setViewBoxScroll,
this.setViewBoxScrollDelta
)}
</div>
</div>
</div>
);
}
複製代碼
最外層的classname="drag-scroll-view"的div表示視口區域,
第二層的div 沒有classname , 他的ref是bigView 表示可拖動區域,這個區域的寬度和高度必須比視口區域的高寬都要大,不然就不存在拖動這回事了, 它最開始的size,暫時寫死成一個很大的size。在構造函數中將widgetStyle設置成一個很是大的size。
constructor(props: DragScrollWidgetProps) {
super(props);
this.state = {
widgetStyle: {
width: "10000px",
height: "10000px"
}
};
}
複製代碼
後面再說怎麼去根據視口區域大小和真正可拖拽的內容的實際大小來計算出合適的bigView區域的size
第三層的classname="drag-scroll-content"的div是爲了在咱們要拖動的內容的size 發生變化時候讓整個控件可以做出響應。具體怎麼響應如今也暫時不表。若是可拖拽內容的size固定不發生變化。那麼這個div 能夠忽略。 最裏面的函數調用表達式式爲了渲染該控件的children。至於this.setViewBoxScroll和this.setViewBoxScrollDelta是什麼暫時也不表。
裏面的三個ref函數代碼以下:
content: HTMLElement;
contentRef = ref => {
if (ref) {
this.content = ref;
this.contentResizeObserver.observe(this.content);
}
};
viewBox: HTMLElement;
viewBoxRef = ref => {
if (ref) {
this.viewBox = ref;
this.setViewBoxScroll(
this.viewBox.clientWidth,
this.viewBox.clientHeight
);
}
};
bigView: HTMLElement;
bigViewRef = ref => {
if (ref) {
this.bigView = ref;
}
};
複製代碼
目前只須要了解設置ref到class內部的viewBox,bigView,content變量便可,至於this.contentResizeObserver.observe(this.content)和setViewBoxScroll調用的目的是什麼後面會細說。
來看一下里面幾個css
.drag-scroll-view {
position: relative;
width: 100%;
height: 100%;
overflow: scroll;
}
.drag-scroll-content {
position: relative;
width: max-content;
}
複製代碼
很簡單就不作多解釋了。
下面分析當鼠標按下拖動的事件響應是怎麼實現的, 最外層的classname="drag-scroll-view"的div 綁定了一個onMouseDown事件
來看下這個onMouseDown函數
onMouseDown = e => {
// mouseKey 表示鼠標按下那個鍵才能夠進行拖動,左鍵或者右鍵
// needKeyPressed 爲了支持是否須要按下ctrl鍵,才能夠進行拖動
// canDragFunc是一個函數,它是爲了支持使用者以傳入函數的方式,這個函數的返回值表示當前的內容是否能夠被拖拽而移動
let { mouseKey, needKeyPressed,canDragFunc } = this.props;
if(canDragFunc && !canDragFunc())
return;
if (
(e.button === 0 && mouseKey === "left") ||
(e.button === 2 && mouseKey === "right")
) {
if (needKeyPressed) {
if (!e.ctrlKey) return;
}
this._lastCoordX = this.viewBox.scrollLeft + e.nativeEvent.clientX;
this._lastCoordY = this.viewBox.scrollTop + e.nativeEvent.clientY;
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
};
複製代碼
當鼠標按下時候給window 對象添加兩個事件mousemove和mouseup。
_lastCoordX和_lastCorrdY的做用是什麼呢?
// _lastCoordX和_lastCorrdY 是爲了在拖動過程當中 計算 viewBox的scrollLeft和scrollTop值用到
// _lastCoordX和_lastCorrdY 記錄下拖動開始時刻viewBox的scroll值和鼠標位置值
_lastCoordX: number;
_lastCoordY: number;
複製代碼
在mousemove過程當中就能夠根據當前鼠標的位置值和剛開始移動時刻位置的差值加上最開始移動時候viewBox的scroll值來計算出viewBox的新的scroll值
onMouseMove = (e: MouseEvent) => {
this.viewBox.scrollLeft = this._lastCoordX - e.clientX;
this.viewBox.scrollTop = this._lastCoordY - e.clientY;
};
複製代碼
鼠標鬆開時移除事件監聽
onMouseUp = e => {
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
};
複製代碼
這樣最基本的可拖動移動位置的控件便完成了。
最開始的時候bigView的size是一個很大的值,是由於這個值不是很重要,後面還須要根據可拖拽的內容的實際大小來進行計算, 計算的函數以下:
setWidgetStyle = () => {
if (this.content && this.viewBox && this.bigView) {
this.bigView.style.width =
(this.content.clientWidth + this.viewBox.clientWidth) * 2 + "px";
this.bigView.style.height =
(this.content.clientHeight + this.viewBox.clientHeight) * 2 + "px";
this.content.style.left = this.viewBox.clientWidth + "px";
this.content.style.top = this.viewBox.clientHeight + "px";
}
};
複製代碼
這個函數裏面把bigView的size 設置成(實際內容的size+視口區域的size)*2,
這樣作的目的是爲了當實際內容在某個方向上徹底離開視口空間時,可拖動區域在那個方向上便不能夠在拖動了,由於即便再拖動也看不到了,還不如讓它不能夠再拖動。
setWidgetStyle函數須要在this.content , this.viewBox , this.bigView 都不爲null的時候才能夠被調用,也就是 componentDidMount這個生命週期函數中被調用
componentDidMount(): void {
this.setWidgetStyle();
document.addEventListener("contextmenu", this.handleContextMenu);
}
componentWillUnmount(): void {
document.removeEventListener("contextmenu", this.handleContextMenu);
}
handleContextMenu = e => {
e.preventDefault();
};
複製代碼
由於咱們須要在這個控件中禁用瀏覽器自帶的右鍵菜單。
後面關於這個控件支持根據內容區域的實際大小的改變而作出響應,以及關於思惟導圖開發過程當中的特殊需求而設計的代碼,在後面的文章中繼續分析