はじめに
とあるWebアプリのダッシュボード画面を開発しているときに、ふと『AWSのマネジメントコンソール』みたいにマウスのドラッグ&ドロップでリストの表示領域をかえられたら便利!っと感じました。
早速、自前でリサイザブルなレイアウトを構築してみましたので紹介したいと思います。
出来上がりはこんな感じです!
まずはいつどおりに出来上がりのイメージからです。
こんな感じになりました。(今回はマウス操作が伴うので動画で紹介します)
コードはここです。
GitHub - Shikataramuno/resizable-div-layout at fixed-resizeArea
考え方
マウスのドラッグ&ドロップ操作に伴ってリサイズさせるので、
- ① ドラッグ&ドロップ操作を可能とするための要素
- ② その操作にともなってサイズを変化させる要素
が必要になります。 便宜的に①をリサイザー、②をリサイズエリアと呼びます。
リサイザーでドラッグイベントを補足してイベントオブジェクトのマウス座標から幅、高さを計算し、リサイズエリアのstyle
属性を更新することでリサイズします。
一番左側に幅方向にリサイズ可能なリサイズエリア1と、その右側の領域を上下に2分割し、それぞれに高さ方向にリサイズ可能なリサイズエリア2、3の合計三つのリサイズエリアを設定します。 それぞれのリサイズエリアの間に幅方向、高さ方向のリサイザーX、Yを配置します。
ダッシュボードの様な画面では、リサイズエリアにコンポーネントを配置していくことになります。
プロジェクトの構造
それでは、実装を見ていきましょう。まずはプロジェクトの作成からです。
プロジェクト名を resizable-div-layout
としてvue create
コマンドでプロジェクトを作成します。
作成時のオプションや可視性(publicやらprivateやら)を省略するための設定、 デバッグ文を出力できるようにするための設定などは以下の記事を参考にしてください。
上の考え方
で示した構造はResizableDivView.vue
というコンポーネントで定義します。
プロジェクトができたらsrc/component
以下に「リサイザブルコンポーネント」を作成します。
フォルダ構成はこんな感じになります。
resizable-div-layout │ babel.config.js │ package.json │ tsconfig.json │ tslint.json ├─node_modules ├─public └─src │ App.vue │ main.ts │ shims-tsx.d.ts │ shims-vue.d.ts │ tree.txt ├─assets │ logo.png └─components ResizableDivView.vue <--これが「リサイザブルコンポーネント」
ではResizableDivView.vue
の中身を見ていきましょう
HTMLの構造
ResizableDivView.vue
のtemplate
部分でリサイザー、リサイズエリアを定義していきます。
<template> <div id="resizeArea" ref="resizeArea" class="element"> <div id="element1" ref="element1" class="element">リサイズエリア1</div> <div id="resizerX" ref="resizerX" style="background: red;" draggable="true" @dragstart="xResizeStart" @dragend="xResizeEnd" @drag="xResize"> </div> <div id="rightElements" ref="rightElements"> <div id="element2" ref="element2" class="element">リサイズエリア2</div> <div id="resizerY" ref="resizerY" style="background: blue;" draggable="true" @dragstart="yResizeStart" @dragend="yResizeEnd" @drag="yResize"> </div> <div id="element3" ref="element3" class="element">リサイズエリア3</div> </div> </div> </template>
2行目でこのコンポーネントのトップ要素をresizeArea
という名前で定義しています。
その内部にリサイズエリア1、リサイザーXとその右側にrightElements
というdiv
要素を配置し、その内部にリサイズエリア2,3とリサイザーYを配置しています。こうすることで、リサイザーXの移動に伴ってリサイズエリア1とrightElements
の幅を更新すれば幅方向のリサイズを実現できるようになります。
次に5~8行目、13~16行目でリサイザーX、Yそれぞれのdiv
要素を定義しています。
ここではdiv
要素にdraggable
の属性をセットし、ドラッグイベントを捕捉できるようにし、ドラッグの開始、終了とドラッグ操作のイベントハンドラを登録しています。
振る舞い
次に実際にリサイズを行う振る舞いを解説していきます。
まず、コンポーネントのインターフェースからです。
@Component export default class ResizableDivView extends Vue { name: string = 'ResizableDivView'; padding: number = 5; margin: number = 0; border: number = 1; $refs!: { 'resizeArea': HTMLDivElement; 'rightElements': HTMLDivElement; 'element1': HTMLDivElement; 'element2': HTMLDivElement; 'element3': HTMLDivElement; 'resizerXY': HTMLDivElement; 'resizerX': HTMLDivElement; 'resizerY': HTMLDivElement; }; // 横方向のリサイズ changeWidth(x: number): void {...} // 縦方向のリサイズ changeHeight(y: number): void {...} // ドラッグ&ドロップのイベントハンドラー // resizerX イベント xResizeStart(e: MouseEvent): void {...} xResizeEnd(e: MouseEvent): void {...} xResize(e: MouseEvent): void {...} // resizerY イベント yResizeStart(e: MouseEvent): void {...} yResizeEnd(e: MouseEvent) {...} yResize(e: MouseEvent): void {...} // 初期表示 mounted(): void {...} }
まず、8~15行目のrefs
を使った各要素への参照の定義です。これは、drag
操作に伴って各要素のstyle
を更新するので、それぞれのelement
を直接的に参照するための定義です。
次に、26~28行目、31~33行目の定義です。これはリサイザーX,Yそれぞれのdrag
の開始、終了、マウス移動のイベントハンドラの定義になります。drag
操作に伴う幅、高さの再設定処理は19行目、22行目で定義しているchangeWidth
、changeHeight
のメソッドが該当し、それぞれdrag
のイベントハンドラから呼び出す構造としています。
ドラッグ開始、終了イベント
視覚的にドラッグ中であることを視認しやすくするために、ドラッグ操作中はリサイザーの背景色を切り替えています。その切替を開始、終了のイベントハンドラで処理しています。
xResizeStart(e: MouseEvent): void { const resizerX: HTMLElement = this.$refs.resizerX; resizerX.style.background = 'gray'; } xResizeEnd(e: MouseEvent): void { const resizerX: HTMLElement = this.$refs.resizerX; resizerX.style.background = 'red'; }
これはX方向のイベントハンドラです。リサイザーのdiv
要素への参照を取得し、style.backgroud
を更新して背景色を切り替えています。Y方向のイベントハンドラも同様です。
ドラッグ操作イベント
いよいよメインイベントの本題です。
drag
イベントに伴うリサイズ処理は以下の様になります。
幅方向のリサイズ
// 幅方向のリサイズ changeWidth(x: number): void { const resizeArea: HTMLDivElement = this.$refs.resizeArea; const element1: HTMLDivElement = this.$refs.element1; const resizerX: HTMLDivElement = this.$refs.resizerX; const rightElement: HTMLDivElement = this.$refs.rightElements; const element2: HTMLDivElement = this.$refs.element2; const resizerY: HTMLDivElement = this.$refs.resizerY; const element3: HTMLDivElement = this.$refs.element3; const adjust: number = element1.offsetLeft // element1 左端 + 2 * (this.border + this.padding) // element1 左右 border,padding + resizerX.offsetWidth; // resizerX 幅 const el1Width: number = x - adjust; element1.style.width = el1Width + 'px'; const rightElementWidth = resizeArea.offsetWidth - 4 * (this.border + this.padding) // resizeArea, element1 - el1Width - resizerX.offsetWidth - 1; rightElement.style.width = rightElementWidth + 'px'; } xResize(e: MouseEvent): void { if (e.pageX) { this.changeWidth(e.pageX); } }
31行目でマウスイベントからマウスのX座標を取り出しています。
このpageX
はdocument
全体の左端をX軸の原点とした座標(pixel)で表されます。
これをパラメタとしてchangeWidth
メソッドを呼び出します。(27行目)
changeWidth
メソッドではelement1
要素(リサイズエリア1)の幅とrightElements
要素の幅をそれぞれ計算しstyle.width
を更新しています。
それぞれの幅を次の様な計算で求めています。
element1 の幅
マウス座標から調整値としてelement1
の『左端の座標値』、『境界線の太さ』、『内部余白』、resizerX
の『幅』の合計値を差し引きelement1
の幅としています。(11~15行目)。 16行目でelement1
のstyle
を更新しています。rightElements の幅
基本的にelement1
の幅と考え方は同じです。
トップエリアのresizeArea
のoffsetWidth
からelement1
の幅と調整値を差し引いてrightElements
の幅を求めています(17~22行目)。
23行目でstyle
を更新しています。
補足
今回、各エリアとも外部余白(margin)は0としています。また、どの要素にもposition
属性を指定していません。従ってすべての要素でoffsetParent
はbody
要素となりoffsetLeft
で得られる値はpageX
と同じ原点からの座標値となります。
下図のイメージに示す様にマウス座標から上で述べた調整値を差し引いた値がelement1
の幅になります。
offsetWidth
は『境界線』と『内部余白』を含んだ幅になりますが、resizerX
はborder
、padding
ともstyleの設定値は0pxです。
高さ方向のリサイズ
// 高さ方向のリサイズ changeHeight(y: number): void { const rightElements: HTMLDivElement = this.$refs.rightElements; const element2: HTMLDivElement = this.$refs.element2; const resizerY: HTMLElement = this.$refs.resizerY; const element3: HTMLDivElement = this.$refs.element3; const adjust: number = element2.offsetTop // element2 上端 + 2 * (this.border + this.padding) // element2 上下 border,padding + resizerY.offsetHeight; // resizerY 高さ const el2height: number = y - adjust; element2.style.height = el2height + 'px'; const el3height: number = rightElements.offsetHeight - (el2height + 2 * (this.border + this.padding)) // element2 - resizerY.offsetHeight // resizerY - 2 * (this.border + this.padding); // element3 element3.style.height = el3height + 'px'; } yResize(e: MouseEvent): void { if (e.pageY) { this.changeHeight(e.pageY); } }
考え方は幅方向の場合と同じです。
最初にマウス座標からelement2
(リサイズエリア2)の高さを求め(8~13行目)、rightElements
の高さからelement2
とresizerYの高さと調整値を差し引いてelement3
の高さを求め(14~19行目)それぞれstyle.height
を更新しています。
出来上がり!
後はApp.vue
でHelloWorld
をResizableDivView
に変更してnpm run serve
で出来上がりです。
今回は全体の幅、高さは変えずに各リサイズエリアの面積を変える様なリサイズの仕方にしてみました。これとは別にリサイズエリア1だけ幅を変化させる。または、リサイズエリア2だけ高さを変え、後は幅を変えずに自動的にスクロールバーを付けるといったやり方もできると思います。
むしろこちらが主流な気がしますね!
っというわけでやってみましたので確認してみてください(コードは以下です)
GitHub - Shikataramuno/resizable-div-layout at autoadjust-resizeArea
謝辞
リサイザブルなレイアウトはダッシュボードなどのUI構築でとても重宝できるのでは?
と密にほくそ笑んでおります。
少しでも参考になったならば幸いです。最後までお付き合いいただき、ありがとうございました。