JavaScriptだけでスワイプを実装
BootStrapなどがjQueryの依存を減らしているのに呼応してjQueryを使わずJavaScriptだけで動くスワイプ機能を実装してみました。touchだけでなく、マウスイベントにも対応してみましたが、マウスイベントの方はブラウザの外に持って行った際にうまく検知できなかったり、ドラッグ&ドロップの機能と相性が悪いようで若干不安定です(ドラッグ&ドロップと認識されてしまうと思ったように稼働しません)。
ページの最後に稼働サンプルも用意しましたので試してみてください。
仕組み
マウスクリックかタップを検知した際、初期位置を記憶するとともに、別に用意してある要素に元の要素のクローンを作成します。
スワイプ動作が始まると、元の要素は非表示にして、かわりにpositionをabsoluteにしたクローン要素を表示させて、マウスの稼働とともに移動させます。
マウスか画面から指が離れた際に、それまでに移動した左右の稼働量から判定して、右スワイプイベント、左スワイプイベント、なにもしないのうちから挙動を選択します。
最後にクローン要素を削除して、非表示にしていた元の要素を再表示させます。
使い方
使い方です。まず、スワイプさせたい要素に任意のIDを付けてください。
あとはIDと、左右側のスワイプが成立した際のイベント用のfunctionをクラスのコンストラクタに渡してインスタンスを作成するだけです。
swipe = new Swipe('div-swipe-id',functionOnLeft,functionOnRight);
スワイプ用の線画を始める境界となる移動量、スワイプを成立となる移動量はクラス内で調整可能です。
マウス操作時は、ブラウザにドラッグ&ドロップと判定されてしまうとうまく動きません。デフォルトでドラッグ&ドロップが有効になっている画像や選択文字列、Draggable属性にtrueの入った要素で注意が必要です。
具体的には次のように(/)マークがついている時はドラッグ&ドロップモードになっています。
コードの紹介
コードは次のようになっています。
swipe.js
class Swipe {
/* スワイプクラス */
MOVE_OFFSET = 10; /* 動きを検知するための最小移動値 px */
MOVE_OFFSET_MINUS = this.MOVE_OFFSET * -1;
MOVE_BORDER = 70; /* イベントを起こす境界 px */
MOVE_BORDER_MINUS = this.MOVE_BORDER * -1;
strDirection = ""; /* 現在の方向を記憶 */
numXPosition = -1; /* 位置を記憶 */
numscrollY = 0; /* スクロール位置を記憶 */
elSource = null; /* スワイプのターゲットとなる要素 */
elCloneWrapper = null; /* クローンの入れ物 */
blnMouseDown = false;
blnInit = false;
/* コンストラクタ */
/* 引数 */
/* strTargetId:スワイプターゲットのID */
/* funcLeft:スワイプ左検知時に実行する関数 */
/* funcRight:スワイプ右検知時に実行する関数 */
constructor(strTargetId, funcLeft, funcRight) {
/* 対象要素を取得 */
this.elSource = document.getElementById(strTargetId);
this.funcLeft = funcLeft;
this.funcRight = funcRight;
if (this.elSource) {
/* クローン用の外枠を作成 */
this.elCloneWrapper = document.createElement("div");
this.elCloneWrapper.style.display = "block";
this.elCloneWrapper.style.position = "absolute";
this.elCloneWrapper.style.top =
String(
this.elSource.getBoundingClientRect().top +
document.scrollingElement.scrollTop
) + "px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
document.scrollingElement.scrollTop
) + "px";
this.elCloneWrapper.style.height = this.elSource.clientHeight + "px";
this.elCloneWrapper.style.width = this.elSource.clientWidth + "px";
this.elCloneWrapper.style.minHeight = this.elSource.clientHeight + "px";
this.elCloneWrapper.style.minWidth = "0px";
this.elCloneWrapper.style.overflow = "hidden";
this.elCloneWrapper.style.whiteSpace = "nowrap";
document.body.appendChild(this.elCloneWrapper);
/* イベントを設定 */
/* そのまま書くとイベント内でthisが呼び出し元になってしまうので、bindでthisを固定 */
this.elSource.addEventListener(
"touchstart",
this.onSwipeStart.bind(this)
); /* 指が触れたか検知 */
this.elSource.addEventListener("touchmove", this.onSwipeMove.bind(this)); /* 指が動いたか検知 */
this.elSource.addEventListener("touchend", this.onSwipeEnd.bind(this)); /* 指が離れたか検知 */
/* マウス用 */
this.elSource.addEventListener("mousedown", this.onMouseStart.bind(this));
window.addEventListener("mousemove", this.onMouseMove.bind(this));
window.addEventListener("mouseup", this.onMouseEnd.bind(this));
window.addEventListener("mouseleave", this.onMouseEnd.bind(this)); /* ウインドウから外れた時も終了 */
this.blnInit = true;
}
}
/* スワイプ中のイベント */
onSwipeMove(event) {
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
let blnVisible = false;
let numMove = this.numXPosition - this.getXPosition(event);
/* スワイプ検知 */
if (this.MOVE_BORDER < numMove) {
this.strDirection = "left";
} else if (numMove < this.MOVE_BORDER_MINUS) {
this.strDirection = "right";
} else {
this.strDirection = "";
}
/* 小さな動きを検知しない */
if (this.MOVE_OFFSET < numMove) {
blnVisible = true;
} else if (numMove < this.MOVE_OFFSET_MINUS) {
blnVisible = true;
} else {
blnVisible = false;
}
if (blnVisible) {
this.elCloneWrapper.style.display = "block"; /* クローンを表示 */
this.elSource.style.visibility = "hidden"; /* 元の要素を非表示に(枠は維持) */
if (numMove < 0) {
/* 右移動 */
/* クローンの幅がページのサイズを超すとページが伸びてしまうので、ラッパーのサイズを減らす */
let numCurrentX =
this.elSource.getBoundingClientRect().right -
window.pageXOffset -
numMove;
let numAdj = document.body.clientWidth - numCurrentX;
if (numAdj < 0) {
if (0 < this.elSource.getBoundingClientRect().width + numAdj) {
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width + numAdj) +
"px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
} /* 幅が0以下になる時は処理しない */
} else {
/* 通常移動でOK */
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width) + "px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
}
} else {
/* 左移動 */
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width) + "px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
}
} else {
this.elCloneWrapper.style.display = "none"; /* クローンを非表示 */
this.elSource.style.visibility = "visible"; /* 元のテーブル表示 */
}
}
/* マウス版(move) */
onMouseMove(event) {
if (this.blnMouseDown == false) {
return;
}
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
let blnVisible = false;
let numMove = this.numXPosition - this.getXPositionMouse(event);
/* スワイプ検知 */
if (this.MOVE_BORDER < numMove) {
this.strDirection = "left";
} else if (numMove < this.MOVE_BORDER_MINUS) {
this.strDirection = "right";
} else {
this.strDirection = "";
}
/* 小さな動きを検知しない */
if (this.MOVE_OFFSET < numMove) {
blnVisible = true;
} else if (numMove < this.MOVE_OFFSET_MINUS) {
blnVisible = true;
} else {
blnVisible = false;
}
if (blnVisible) {
this.elCloneWrapper.style.display = "block"; /* クローンを表示 */
this.elSource.style.visibility = "hidden"; /* 元の要素を非表示に(枠は維持) */
if (numMove < 0) {
/* 右移動 */
/* クローンの幅がページのサイズを超すとページが伸びてしまうので、ラッパーのサイズを減らす */
let numCurrentX =
this.elSource.getBoundingClientRect().right -
window.pageXOffset -
numMove;
let numAdj = document.body.clientWidth - numCurrentX;
if (numAdj < 0) {
if (0 < this.elSource.getBoundingClientRect().width + numAdj) {
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width + numAdj) +
"px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
} /* 幅が0以下になる時は処理しない */
} else {
/* 通常移動でOK */
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width) + "px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
}
} else {
/* 左移動 */
this.elCloneWrapper.style.width =
String(this.elSource.getBoundingClientRect().width) + "px";
this.elCloneWrapper.style.left =
String(
this.elSource.getBoundingClientRect().left +
window.pageXOffset -
numMove
) + "px";
}
} else {
this.elCloneWrapper.style.display = "none"; /* クローンを非表示 */
this.elSource.style.visibility = "visible"; /* 元のテーブル表示 */
}
}
/* スワイプ開始時のイベント */
onSwipeStart(event) {
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
/* クローンの外枠の位置を初期状態に */
/* 動的に高さが変わる対象だったらここでサイズを検知する */
this.elCloneWrapper.style.height =
this.elSource.getBoundingClientRect().height + "px";
/* 幅を初期化 */
this.elCloneWrapper.style.width = this.elSource.clientWidth + "px";
/* 高さを取得 */
this.elCloneWrapper.style.top =
String(
this.elSource.getBoundingClientRect().top +
document.scrollingElement.scrollTop
) + "px";
/* 左を基準に位置を取得 */
this.elCloneWrapper.style.left =
String(this.elSource.getBoundingClientRect().left) + "px";
/* クローン作成 */
this.elCloneWrapper.appendChild(this.elSource.cloneNode(true));
this.numscrollY = document.scrollingElement.scrollTop; /* scrollY記憶 */
this.numXPosition = this.getXPosition(event); /* 位置記憶 */
this.strDirection = ""; /* 左右の状態を初期化 */
}
/* マウス版(start) */
onMouseStart(event) {
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
this.blnMouseDown = true;
this.elCloneWrapper.style.height =
this.elSource.getBoundingClientRect().height + "px";
this.elCloneWrapper.style.width = this.elSource.clientWidth + "px";
this.elCloneWrapper.style.top =
String(
this.elSource.getBoundingClientRect().top +
document.scrollingElement.scrollTop
) + "px";
this.elCloneWrapper.style.left =
String(this.elSource.getBoundingClientRect().left) + "px";
this.elCloneWrapper.appendChild(this.elSource.cloneNode(true));
this.numscrollY = document.scrollingElement.scrollTop; /* scrollY記憶 */
this.numXPosition = this.getXPositionMouse(event); /* 位置記憶 */
this.strDirection = ""; /* 左右の状態を初期化 */
/* そのままだと文字列選択範囲のドラック&ドロップに陥りやすいので、伝播を止める。 */
/* ただし中にinput要素がある場合はここで止めるとフォーカスが当たらないので注意 */
/* event.preventDefault(); */
}
/* スワイプ終了時のイベント */
onSwipeEnd(event) {
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
let blnSwipeAct = false;
this.elCloneWrapper.style.display = "none"; /* クローンの外枠を非表示 */
this.elSource.style.visibility = "visible"; /* 元のテーブル表示 */
this.elCloneWrapper.removeChild(this.elCloneWrapper.firstChild); /* クローン削除 */
/* スワイプ終了時に左右どちらかの判定が入っていたらイベント実行 */
if (this.strDirection == "right") {
try {
blnSwipeAct = true;
this.funcRight();
} catch (e) {
/* 下流のイベントでのエラーを無視 */
console.log(e);
}
} else if (this.strDirection == "left") {
try {
blnSwipeAct = true;
this.funcLeft();
} catch (e) {
/* 下流のイベントでのエラーを無視 */
console.log(e);
}
}
/* スワイプで上下の位置がずれた場合に修正 */
if (blnSwipeAct) {
window.scroll(0, this.numscrollY);
}
}
/* マウス版(end) */
onMouseEnd(event) {
if (this.blnMouseDown) {
/* マウスの時はここでも判定が必要 */
let numMove = this.numXPosition - this.getXPositionMouse(event);
/* スワイプ検知 */
if (this.MOVE_BORDER < numMove) {
this.strDirection = "left";
} else if (numMove < this.MOVE_BORDER_MINUS) {
this.strDirection = "right";
} else {
this.strDirection = "";
}
} else {
/* マウスクリックを検知していなかった際は、クリア指示 */
/* ウインドウ外へマウスカーソルがいったりして挙動がおかしくなった時に混乱を少なくする */
this.strDirection = "";
}
this.blnMouseDown = false;
if (!this.blnInit) {
console.log("swipe init faied");
return;
}
let blnSwipeAct = false;
this.elCloneWrapper.style.display = "none"; /* クローンの外枠を非表示 */
this.elSource.style.visibility = "visible"; /* 元のテーブル表示 */
if (this.elCloneWrapper.hasChildNodes())
this.elCloneWrapper.removeChild(this.elCloneWrapper.firstChild); /* クローン削除 */
/* スワイプ終了時に左右どちらかの判定が入っていたらイベント実行 */
if (this.strDirection == "right") {
try {
blnSwipeAct = true;
this.funcRight();
} catch (e) {
/* 下流のイベントでのエラーを無視 */
console.log(e);
}
} else if (this.strDirection == "left") {
try {
blnSwipeAct = true;
this.funcLeft();
} catch (e) {
/* 下流のイベントでのエラーを無視 */
console.log(e);
}
}
/* スワイプで上下の位置がずれた場合に修正 */
if (blnSwipeAct) {
window.scroll(0, this.numscrollY);
}
}
/* 横方向の座標を取得 */
getXPosition(event) {
return event.touches[0].pageX;
}
/* 座標取得マウス版 */
getXPositionMouse(event) {
return event.pageX;
}
}
稼働サンプル
実際に動かしてみたサンプルです。冒頭でも書きましたが、マウスイベントだとウインドウの外にいった場合の検知の問題や、ドラッグ&ドロップとの相性の問題で、挙動が不安定なのでもう少し改善が必要だと思います。