結合セルがあるテーブルの各セルをドラッグで選択できるようにする。
スプシと同じように、選択範囲の中に結合したセルがあれば、そのサイズに合わせて選択範囲を拡張し、常に範囲が四角になるようにする。
##目次
-
考え方
2. mouse系イベントの活用
3. セルの列方向の位置を算出
4. watchプロパティの活用 -
実際のコード
2. mouse系イベントの活用
3. セルの列方向の位置を算出
4. watchプロパティの活用 - (参考)ドラッグ選択の処理を外部ファイルに移動
##考え方
###1. mouse系イベントの活用
ドラッグ選択を実現するためには、mousedown, mouseup, mousemoveイベントを使用する。
イベント | 内容 | ここでの用途 |
---|---|---|
mousedown | ポインティングデバイスのボタンが要素上で押されたときに発生 | 選択が開始したセルの情報を取得。開始フラグを立てる。 |
mousemove | 要素の上を移動したときに発生 | 現在選択している要素の情報を取得 |
mouseup | ポインティングデバイスのボタンが要素の上で離されたときに発生 | 終了フラグ |
イベントは各セルに仕込むのではなく、tableタグに仕込む。(セルが大量の場合に余計なメモリ消費を防ぐため)
ちなみに、よく目にするclickイベントはmousedownとmouseupを組み合わせて作られている。このため、mousedownなどのイベントを使えなくするとclickイベントも使えなくなる。
###2. セルの列方向の位置を算出 結合したセルを扱うため、セルの列番号と実際のセルの位置ズレが発生する。
このため、テーブルの状態をスキャンして、各セルの列方向の位置を求める。
###3. watchプロパティの活用 ドラッグに合わせて選択範囲を自動算出するため、Vue.jsのwatchプロパティを使う。
mousemoveイベントで選択中のセルが変化した場合に、watchで検知して、選択範囲を算出する。
**▼位置ズレの例** 例えば、(0, 0)位置のセルのcolspanが3の場合、(0, 1)セルの実際の位置は列番号3の位置となる。
##実際のコード ##mouse系イベントの活用 vue.jsでイベントを仕込むのは超簡単。タグの中に以下を記述。
@イベント名 = "メソッド名"
<table
@mousedown="mouseDown"
@mouseup="mouseUp"
@mousemove="mouseMove"
@click="clickCell"
>
個別に複数選択できるようにもしておくため@click="clickCell"
も設置しておく。
各イベントの処理は以下。methodの中に記述
mouseDown(e){
this.isDrag = true
this.startCell = {
rowIndex: e.target.parentNode.rowIndex,
cellIndex: e.target.cellIndex,
rowspan: e.target.rowSpan,
colspan: e.target.colSpan
}
},
mouseUp(e){
this.isDrag = false
},
mouseMove(e){
if(this.isDrag == true && e.target.tagName != "TABLE"){
this.endCell = {
rowIndex: e.target.parentNode.rowIndex,
cellIndex: e.target.cellIndex,
rowspan: e.target.rowSpan,
colspan: e.target.colSpan
}
}
}
引数のeにはイベントが発生した情報が入っている。
e.target.cellIndex
のようにして、欲しい情報を抜き出す。
isDrag
はドラッグ中を示すフラグ。このフラグがtrueの間のみmousemove
でデータを取得し続ける。
mousemoveの条件式のe.target.tagName != "TABLE"
は、ポインターの場所によって、targetがtdタグではなくtableタグを指してしまうことがあり、その場合を除外するため。
イベント内で使う全体共通の変数を3つ追加したため、これをdataプロパティに追加する。
data(){
return{
略
// Drag用
startCell:[],
endCell:[],
isDrag: false,
}
以上でマウスのイベントを検知して必要なデータを取得する処理が完了。
##セルの列方向の位置を算出 watchプロパティの中で使用するテーブルのセルの実際の列方向位置を算出するプログラムを作成する。
テーブルのデータを渡すと、各セルがどこの列位置になっているかを返す。
外部にjsファイルを作成し、後からvueにimportする。
const scanTable = (rows) =>
{
const arr = [];
const colIndices = []
for(var y=0; y < rows.length; y++)
{
var row = rows[y]
for(var x=0;x<row.table_cells.length;x++)
{
var cell = row.table_cells[x], xx = x, tx, ty;
for(;arr[y] && arr[y][xx]; ++xx);
for(tx = xx; tx < xx + (cell.colspan || 1); ++tx) {
for(ty = y; ty < y + (cell.rowspan || 1); ++ty)
{
if( !arr[ty] ) arr[ty] = []
arr[ty][tx] = 1
if ( !colIndices[y] ) colIndices[y] = []
colIndices[y][x] = xx
}
}
}
}
return colIndices
}
export { scanTable }
引数で配列情報を渡す(変数rowsに格納)と、処理結果をcolIndices
という変数で返す。
**▼実際の処理の例**
rows= [
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 2,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 3,
}
]
},
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 2,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 2,
}
]
},
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
}
]
},
]
これに作成したscanTableを実行すると、以下となる。
scanTable(rows)
//出力結果
[0, 2, 3]
[0, 1, 2]
[1, 2, 3]
##watchプロパティの活用 watchプロパティを使って、マウスのドラッグがあった場合に自動で選択範囲を算出する処理を作る。
まずは基点となるセル(rowIndexとcellIndex)を求める。
ドラッグは左上から右下のみでなく、右下から左上にいくパターンも考えられる。
// 基点となるrowIndexとcolIndex
let startRowIndex = this.startCell.rowIndex
if ( startRowIndex > this.endCell.rowIndex ){
startRowIndex = this.endCell.rowIndex
}
let startColIndex = this.colIndicies[this.startCell.rowIndex][this.startCell.cellIndex]
if ( startColIndex > this.colIndicies[this.endCell.rowIndex][this.endCell.cellIndex] ){
startColIndex = this.colIndicies[this.endCell.rowIndex][this.endCell.cellIndex]
}
let endRowIndex = this.startCell.rowIndex + this.startCell.rowspan - 1
if ( endRowIndex < this.endCell.rowIndex + this.endCell.rowspan - 1 ){
endRowIndex = this.endCell.rowIndex + this.endCell.rowspan - 1
}
let endColIndex = this.colIndicies[this.startCell.rowIndex][this.startCell.cellIndex] + this.startCell.colspan - 1
if ( endColIndex < this.colIndicies[this.endCell.rowIndex][this.endCell.cellIndex] + this.endCell.colspan - 1 ){
endColIndex = this.colIndicies[this.endCell.rowIndex][this.endCell.cellIndex] + this.endCell.colspan - 1
}
上記処理で、startRowIndex
、endRowIndex
、startColIndex
、endColIndex
の4つのデータを作成。
####セルを選択する
対象となるセルを算出し、選択中の配列(currentCells)に格納する処理。
//対象セルの抽出
const dragSelectedCells = () => {
for( let i = 0; i <= endRowIndex; i++ ){
for( let j = 0; j < this.rows[i].table_cells.length; j++ ){
//範囲拡張チェック
//colIndexの延長処理
if( i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| startRowIndex <= i && i + this.rows[i].table_cells[j].rowspan - 1 <= endRowIndex){
if( this.colIndices[i][j] < startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
startColIndex = this.colIndices[i][j]
dragSelectedCells()
}
if( this.colIndices[i][j] <= endColIndex && endColIndex < this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
endColIndex = this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
dragSelectedCells()
}
}
//rowIndexの延長処理
if( this.colIndices[i][j] <= startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| this.colIndices[i][j] <= endColIndex && endColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 <= endColIndex ){
if( i < startRowIndex && startRowIndex <= (i + this.rows[i].table_cells[j].rowspan - 1) ){
startRowIndex = i
dragSelectedCells()
}
if( i <= endRowIndex && endRowIndex < (i + this.rows[i].table_cells[j].rowspan - 1) ){
endRowIndex = i + this.rows[i].table_cells[j].rowspan - 1
dragSelectedCells()
}
}
if( startRowIndex <= i && i <= endRowIndex
&& startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] <= endColIndex ){
this.currentCells.push({
rowIndex: i,
cellIndex: j,
colIndex: this.colIndices[i][j],
rowspan: this.rows[i].table_cells[j].rowspan,
colspan: this.rows[i].table_cells[j].colspan,
})
}
}
}
}
dragSelectedCells()
テーブルのセルが選択範囲に該当するかを一つづつチェックしていく。
colspanやrowspanが1以上(結合してあるセル)を含む場合は、再起的にdragSelectedCells()
を実行することで、選択範囲を再計算する。
以上でドラッグ選択の記述が完了。
##フルコード
<template>
<div>
<p>〜TmpAddRow.vue〜</p>
<button @click="clear">選択解除</button>
<br>
<table
@mousedown="mouseDown"
@mouseup="mouseUp"
@mousemove="mouseMove"
@click="clickCell"
>
<template v-for="(tr, rowIndex) in rows">
<tr :key="rowIndex">
<template v-for="(cell, cellIndex) in tr.table_cells">
<td :key="cellIndex"
:class="{'is-active': isActive(rowIndex, cellIndex)}"
:rowspan="cell.rowspan || 1"
:colspan="cell.colspan || 1"
>
( {{rowIndex}} , {{cellIndex}} )
</td>
</template>
</tr>
</template>
</table>
<br>
<p>currentCells : {{currentCells}}</p>
</div>
</template>
<script>
import { scanTable } from "./scantable"
export default {
data(){
return{
currentCells:[],
// Drag
startCell:[],
endCell:[],
isDrag: false,
rows: [
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 2,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 3,
}
]
},
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 2,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 2,
}
]
},
{
"table_cells": [
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
},
{
"cell_type": "TD",
"rowspan": 1,
"colspan": 1,
}
]
},
]
}
},
methods:{
//セルに選択状態付与
isActive(rowIndex, cellIndex){
return this.currentCells.findIndex((elem) =>
elem.rowIndex == rowIndex && elem.cellIndex == cellIndex
) > -1
},
//クリックによる選択状態の変更
clickCell(event){
const cell = event.target
const tr = event.target.parentNode
if(this.isActive(tr.rowIndex, cell.cellIndex)){
const rmIndex = this.currentCells.findIndex((elem)=>
elem.rowIndex == tr.rowIndex && elem.cellIndex == cell.cellIndex
)
this.currentCells = [
...this.currentCells.slice(0, rmIndex),
...this.currentCells.slice(rmIndex + 1)
]
} else{
this.currentCells = [
...this.currentCells,
{
rowIndex: tr.rowIndex,
cellIndex: cell.cellIndex
}
]
}
},
//行内の要素(セル数)の最大値を取得する(行作成用)
getMaxCellNum(){
return this.rows.reduce((acc, tr) => {
if (acc < tr.table_cells.length){
return tr.table_cells.length
}else{
return acc
}
}, 0)
},
clear(){
this.currentCells = []
},
//ドラッグ選択
mouseDown(e){
console.log("mouseDown:",e)
this.isDrag = true
this.startCell = {
rowIndex: e.target.parentNode.rowIndex,
cellIndex: e.target.cellIndex,
rowspan: e.target.rowSpan,
colspan: e.target.colSpan
}
console.log("startcell:",this.startCell)
},
mouseUp(e){
console.log("mouseUp:",e)
this.isDrag = false
},
mouseMove(e){
if(this.isDrag == true && e.target.tagName != "TABLE"){
console.log("mouseMove:",e)
this.endCell = {
rowIndex: e.target.parentNode.rowIndex,
cellIndex: e.target.cellIndex,
rowspan: e.target.rowSpan,
colspan: e.target.colSpan
}
console.log("endCell:",this.endCell)
}
}
},
//dragによる選択範囲の算出
computed:{
colIndices(){
return scanTable( this.rows )
}
},
watch:{
endCell(){
this.currentCells = [this.startCell]
// 基点となるrowとcol
let startRowIndex = this.startCell.rowIndex
if ( startRowIndex > this.endCell.rowIndex ){
startRowIndex = this.endCell.rowIndex
}
let startColIndex = this.colIndices[this.startCell.rowIndex][this.startCell.cellIndex]
if ( startColIndex > this.colIndices[this.endCell.rowIndex][this.endCell.cellIndex] ){
startColIndex = this.colIndices[this.endCell.rowIndex][this.endCell.cellIndex]
}
let endRowIndex = this.startCell.rowIndex + this.startCell.rowspan - 1
if ( endRowIndex < this.endCell.rowIndex + this.endCell.rowspan - 1 ){
endRowIndex = this.endCell.rowIndex + this.endCell.rowspan - 1
}
let endColIndex = this.colIndices[this.startCell.rowIndex][this.startCell.cellIndex] + this.startCell.colspan - 1
if ( endColIndex < this.colIndices[this.endCell.rowIndex][this.endCell.cellIndex] + this.endCell.colspan - 1 ){
endColIndex = this.colIndices[this.endCell.rowIndex][this.endCell.cellIndex] + this.endCell.colspan - 1
}
//対象セルの抽出
const dragSelectedCells = () => {
for( let i = 0; i <= endRowIndex; i++ ){
for( let j = 0; j < this.rows[i].table_cells.length; j++ ){
//範囲拡張チェック
//colIndexの延長処理
if( i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| startRowIndex <= i && i + this.rows[i].table_cells[j].rowspan - 1 <= endRowIndex){
if( this.colIndices[i][j] < startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
startColIndex = this.colIndices[i][j]
dragSelectedCells()
}
if( this.colIndices[i][j] <= endColIndex && endColIndex < this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
endColIndex = this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
dragSelectedCells()
}
}
//rowIndexの延長処理
if( this.colIndices[i][j] <= startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| this.colIndices[i][j] <= endColIndex && endColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 <= endColIndex ){
if( i < startRowIndex && startRowIndex <= (i + this.rows[i].table_cells[j].rowspan - 1) ){
startRowIndex = i
dragSelectedCells()
}
if( i <= endRowIndex && endRowIndex < (i + this.rows[i].table_cells[j].rowspan - 1) ){
endRowIndex = i + this.rows[i].table_cells[j].rowspan - 1
dragSelectedCells()
}
}
if( startRowIndex <= i && i <= endRowIndex
&& startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] <= endColIndex ){
this.currentCells.push({
rowIndex: i,
cellIndex: j,
colIndex: this.colIndices[i][j],
rowspan: this.rows[i].table_cells[j].rowspan,
colspan: this.rows[i].table_cells[j].colspan,
})
}
}
}
}
dragSelectedCells()
}
}
}
</script>
<style lang="scss" scoped>
table{
width: 80%;
user-select: none;
th,td{
border: thin solid rgba(0, 0, 0, 0.12);
text-align: center;
color: gray;
}
th{
background: #ccc;
}
th, td{
//選択状態
&.is-active{
border: 1px double #0098f7;
}
}
}
button{
background: lightcoral;
padding: 5px 20px;
color: white;
border-radius: 50px;
}
</style>
**▼外部のjsファイル**
const scanTable = (rows) =>
{
const arr = [];
const colIndices = []
for(var y=0; y < rows.length; y++)
{
var row = rows[y]
for(var x=0;x<row.table_cells.length;x++)
{
var cell = row.table_cells[x], xx = x, tx, ty;
for(;arr[y] && arr[y][xx]; ++xx);
for(tx = xx; tx < xx + (cell.colspan || 1); ++tx) {
for(ty = y; ty < y + (cell.rowspan || 1); ++ty)
{
if( !arr[ty] ) arr[ty] = []
arr[ty][tx] = 1
if ( !colIndices[y] ) colIndices[y] = []
colIndices[y][x] = xx
}
}
}
}
return colIndices
}
export { scanTable }
##(参考)ドラッグ選択の処理を外部ファイルに移動 Vueファイルのwatchプロパティに記述している下記処理が長いので、こちらも外部ファイルに移動する。
ポイントは変数をプロパティ名を指定して渡すこと。
//対象セルの抽出
const dragSelectedCells = () => {
for( let i = 0; i <= endRowIndex; i++ ){
for( let j = 0; j < this.rows[i].table_cells.length; j++ ){
//範囲拡張チェック
//colIndexの延長処理
if( i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| i <= startRowIndex && startRowIndex <= i + this.rows[i].table_cells[j].rowspan - 1
|| startRowIndex <= i && i + this.rows[i].table_cells[j].rowspan - 1 <= endRowIndex){
if( this.colIndices[i][j] < startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
startColIndex = this.colIndices[i][j]
dragSelectedCells()
}
if( this.colIndices[i][j] <= endColIndex && endColIndex < this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 ){
endColIndex = this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
dragSelectedCells()
}
}
//rowIndexの延長処理
if( this.colIndices[i][j] <= startColIndex && startColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| this.colIndices[i][j] <= endColIndex && endColIndex <= this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1
|| startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] + this.rows[i].table_cells[j].colspan - 1 <= endColIndex ){
if( i < startRowIndex && startRowIndex <= (i + this.rows[i].table_cells[j].rowspan - 1) ){
startRowIndex = i
dragSelectedCells()
}
if( i <= endRowIndex && endRowIndex < (i + this.rows[i].table_cells[j].rowspan - 1) ){
endRowIndex = i + this.rows[i].table_cells[j].rowspan - 1
dragSelectedCells()
}
}
if( startRowIndex <= i && i <= endRowIndex
&& startColIndex <= this.colIndices[i][j] && this.colIndices[i][j] <= endColIndex ){
this.currentCells.push({
rowIndex: i,
cellIndex: j,
colIndex: this.colIndices[i][j],
rowspan: this.rows[i].table_cells[j].rowspan,
colspan: this.rows[i].table_cells[j].colspan,
})
}
}
}
}
dragSelectedCells()
###Vueファイル内の記述変更
上記記述を以下のように変更する。
this.currentCells = dragSelectedCells({
startRowIndex: startRowIndex,
endRowIndex: endRowIndex,
startColIndex: startColIndex,
endColIndex: endColIndex,
rows: this.rows,
colIndices: this.colIndices,
currentCells: this.currentCells,
})
渡す変数が多いのでごちゃごちゃしているが、記述量はシンプルになった。
また、外部から関数を読み込むのでimportに追記。
import { scanTable, dragSelectedCells } from "./scantable"
###jsファイルへの追記 関数を外部のjsファイルに移動する。
const dragSelectedCells = ({
startRowIndex,
endRowIndex,
startColIndex,
endColIndex,
rows,
colIndices,
currentCells,
}) => {
for( let i = 0; i <= endRowIndex; i++ ){
for( let j = 0; j < rows[i].table_cells.length; j++ ){
//範囲拡張チェック
//colIndexの延長処理
if( i <= startRowIndex && startRowIndex <= i + rows[i].table_cells[j].rowspan - 1
|| i <= startRowIndex && startRowIndex <= i + rows[i].table_cells[j].rowspan - 1
|| startRowIndex <= i && i + rows[i].table_cells[j].rowspan - 1 <= endRowIndex){
if( colIndices[i][j] < startColIndex && startColIndex <= colIndices[i][j] + rows[i].table_cells[j].colspan - 1 ){
startColIndex = colIndices[i][j]
dragSelectedCells({
startRowIndex,
endRowIndex,
startColIndex,
endColIndex,
rows,
colIndices,
currentCells,
})
}
if( colIndices[i][j] <= endColIndex && endColIndex < colIndices[i][j] + rows[i].table_cells[j].colspan - 1 ){
endColIndex = colIndices[i][j] + rows[i].table_cells[j].colspan - 1
dragSelectedCells({
startRowIndex,
endRowIndex,
startColIndex,
endColIndex,
rows,
colIndices,
currentCells,
})
}
}
//rowIndexの延長処理
if( colIndices[i][j] <= startColIndex && startColIndex <= colIndices[i][j] + rows[i].table_cells[j].colspan - 1
|| colIndices[i][j] <= endColIndex && endColIndex <= colIndices[i][j] + rows[i].table_cells[j].colspan - 1
|| startColIndex <= colIndices[i][j] && colIndices[i][j] + rows[i].table_cells[j].colspan - 1 <= endColIndex ){
if( i < startRowIndex && startRowIndex <= (i + rows[i].table_cells[j].rowspan - 1) ){
startRowIndex = i
dragSelectedCells({
startRowIndex,
endRowIndex,
startColIndex,
endColIndex,
rows,
colIndices,
currentCells,
})
}
if( i <= endRowIndex && endRowIndex < (i + rows[i].table_cells[j].rowspan - 1) ){
endRowIndex = i + rows[i].table_cells[j].rowspan - 1
dragSelectedCells({
startRowIndex,
endRowIndex,
startColIndex,
endColIndex,
rows,
colIndices,
currentCells,
})
}
}
if( startRowIndex <= i && i <= endRowIndex
&& startColIndex <= colIndices[i][j] && colIndices[i][j] <= endColIndex ){
currentCells.push({
rowIndex: i,
cellIndex: j,
colIndex: colIndices[i][j],
rowspan: rows[i].table_cells[j].rowspan,
colspan: rows[i].table_cells[j].colspan,
})
}
}
}
return currentCells
}
export { scanTable, dragSelectedCells }
再起処理の度に変数を渡す必要がある。
最後に外部ファイルに渡す関数を追加。
export { scanTable, dragSelectedCells }
以上。