0. INDEX
- 概要
- 前準備
- 実装
- あとがき
1. 概要
スマホ向けWebアプリ開発で、ロングタップ(押しっぱなし)的なトリガーがあると思い込んでいたが為にやらかしました。
という事で、スマホアプリのファイラー等でよく見かける、ロングタップをするとファイルを選択する という感じの事をWebアプリでもやりたくて、 ロングタップを判断するユーティリティ を作りゴリ押しました。
2. 前準備
ロングタップとは何か? という事をユーザーとシステムの目線で見て実装内容を考えてみます。
2.1. ユーザー目線のロングタップ操作
多分、こんな感じ。
- スマホ画面を指で触り始める
※指を動かさない(ドラッグしない)ってのもあるかもしれないですが、今回は割愛します - 触ったまま一定時間経ったら、長押し用のメニュー等が出てくる
2.2. システム目線のロングタップ操作
上記を踏まえて、こういう感じで実装できそうです。
-
touchstart
トリガーを捕まえて処理を開始する - 一定時間押しっぱなしだったと判断する為に、
setTimeout()
を仕掛ける … (1) - 一定時間押しっぱなしじゃなかったと判断する為に、
touchend
トリガーを仕掛ける … (2)
もうちょっと分解
- (1)が先に発動したら、ロングタップをした
- (2)が先に発動したら、ロングタップではなかった
2.3. つまりどーゆー事だっぺよ?(図解)
そういえば私は弊社の大バカ野郎担当なので、言葉だけではイマイチ理解できません。なのでお絵描き3をします。
3. 実装
前準備を元に実装しました。
3.1. 動作サンプル
https://jsfiddle.net/mahny/mg48pub3/11/
※JSFiddleの方はPCでも確認できるように「onMouseDown」の処理も入っています。
3.2. コード抜粋
function isHoldDown(targetElement, thresholdMsec = 1000) {
return new Promise((resolve) => {
const timerId = setTimeout(() => {
resolve(true);
removeListener();
}, thresholdMsec);
const touchendHandler = () => {
resolve(false);
removeListener();
};
const contextHandler = (event) => {
event.preventDefault();
}
const beforeTargetStyle = targetElement.style.userSelect;
const removeListener = () => {
clearTimeout(timerId);
targetElement.removeEventListener('touchend', touchendHandler);
targetElement.removeEventListener('contextmenu', contextHandler);
targetElement.style.userSelect = beforeTargetStyle;
};
targetElement.addEventListener('touchend', touchendHandler);
targetElement.removeEventListener('contextmenu', contextHandler);
targetElement.style.userSelect = 'none';
});
}
<h2>押しっぱなし判定習作</h2>
<div>
<button onTouchStart="test(event)">押しっぱなし!</button>
<div id="result-console">(判定結果)</div>
</div>
async function test(event) {
if (await isHoldDown(event.target)) {
console.log('ロングタップ');
} else {
console.log('普通のタップ');
}
}
3.3. コード解説
このコードは、onTouchStart
をトリガーにするなど、ユーザーの指が既に画面に触れている事を前提としています。
以降、isHoldDown()
について解説します。
インターフェース周り
function isHoldDown(targetElement, thresholdMsec = 1000) {
return new Promise((resolve) => {
// ~(略)~
}
}
- ユーティリティとしては、単純にtrue/falseで判定したかったので、Promiseを使っています
- その為、呼び出し側は async/await を使う作りになっている
- サンプルだしエラー処理もいらなかったので、
reject
を入れていませんが、必要なら追加する事
ハンドラ周り
// ロングタップであると判定する為のハンドラ
const timerId = setTimeout(() => {
resolve(true);
removeListener();
}, thresholdMsec);
// ロングタップではないと判定する為のハンドラ
const touchendHandler = () => {
resolve(false);
removeListener();
};
// コンテキストメニューが出てくるのを抑制する為のハンドラ
const contextHandler = (event) => {
event.preventDefault();
}
- 重要なのは、
setTimeout
とtouchendHandler
で以下の処理を担います-
setTimeout
-
touchend
が発生しなかったので、タップし続けた⇒ロングタップである ので、resolve(true)
で返しています - 呼び出し元で
await
を使っていれば、戻り値としてtrue
が得られますね
-
-
touchendHandler
-
setTimeout
が発生する前に指を離してしまったので、ロングタップではない。resolve(false)
で返しています - 同様に、呼び出し元で
await
を使っていれば、戻り値としてfalse
が得られますね
-
-
contextHandler
- 単に、コンテキストメニューを封じたかったので、
contextmenu
トリガーに紐づけるハンドラとして定義しています。
スマホ専用サイトの場合はcontextmenu
自体が無いので不要です
- 単に、コンテキストメニューを封じたかったので、
-
トリガー周り、他
const beforeTargetStyle = targetElement.style.userSelect;
const removeListener = () => {
clearTimeout(timerId);
targetElement.removeEventListener('touchend', touchendHandler);
targetElement.removeEventListener('contextmenu', contextHandler);
targetElement.style.userSelect = beforeTargetStyle;
};
targetElement.addEventListener('touchend', touchendHandler);
targetElement.removeEventListener('contextmenu', contextHandler);
targetElement.style.userSelect = 'none';
-
ロングタップではないと判断する為の
touchend
トリガーにハンドラを定義しています -
beforeTargetStyle
とcontextmenu
は、ロングタップ時にコンテキストメニューが出てきてしまうのを抑制する為に定義しています
4. あとがき
という事で、思い込みで安請け合いをしたら苦労した話…ではなく、ロングタップを判断するユーティリティ作った話でした。
そろそろ、標準でロングタップトリガーを準備してくれてもいいと思うわけですが、テキスト選択等のコンテキストメニューとぶつかりやすそうで二の足を踏んでいるのでしょうか。
良い子の皆様におかれましては、安請け合い等をして泥沼にハマらない事を祈っております。
-
弊社にはこのような営業はおりません。全ては私の中にいる悪魔(サービス精神旺盛)の所業であり、ただの自爆です。 ↩
-
マンガは 社畜ちゃん台詞メーカー で作りました。
https://blog.oukasoft.com/OS/scenemake/ ↩ -
お絵描き後、先に書いていたテキストは要らねーじゃんと思ったり思わなかったりしましたω ↩