◆ ロングタップで PREVIEW / EDIT ボタンの表示切り替えれるようにした
◆ タッチ対応面倒くさい

前記事

このブログのコードブロックのプレビュー・編集機能ですが タブレット端末で使おうとしたら使えませんでした
ボタン表示条件が :hover なのでタップだと反応してくれません
Surface ならタップで出るんですけどね

タッチ操作でどういうときに表示しようかと考えると :hover をそのまま考えると触れてる間だけ表示して 2 本目の指でタッチするとボタンが押せる になりますが流石に使いづらすぎです
なので :hover からボタン押すたびに切り替わるような方針にしました
とは言っても触れるたびに切り替わって出たり消えたりも鬱陶しさがあります
メニュー画面出すものと考えると長押しかなって思ったのでロングタップでトグルということにしました

実装について

ただタッチ系操作ってイベントがあまりなくて touchstart, touchmove, touchend だけで あとは自力で判定するしかありません
タップ・ダブルタップ・ロングタップ・スワイプ・フリック…… これ全部自分で判定です
今回関係ないものまで作っておくのはかなり大変なのでとりあえずロングタップだけイベントを起こすようにしました

function enableLongTap(elem, move_thld = 20, tap_time = 800) {
let tid
let pos = { x: 0, y: 0 }

elem.addEventListener("touchstart", eve => {
const { clientX, clientY } = eve.targetTouches[0]
pos = { x: clientX, y: clientY }
clearTimeout(tid)
tid = setTimeout(() => elem.dispatchEvent(new Event("long-tap")), tap_time)
})

elem.addEventListener("touchmove", eve => {
const { clientX, clientY } = eve.targetTouches[0]
const moved = Math.sqrt((pos.x - clientX) ** 2 + (pos.y - clientY) ** 2)

if (moved > move_thld) {
clearTimeout(tid)
}
})

elem.addEventListener("touchend", eve => {
clearTimeout(tid)
})
}

touch 系操作のリスナを設定することになるので そのイベントが必要な要素だけに有効化します
有効化した要素でロングタップを検知すると long-tap イベントをディスパッチするので 普通に addEventListener で long-tap のリスナ設定すれば使えます

enableLongTap(div)

div.addEventListener("long-tap", eve => {
console.log("LONG TAP DETECTED")
})

オプション

ブログでは固定ですが 一応オプションに移動距離とタップ時間を設定できるようにしています

タップ時間

タップ時間はそのままロングタップとみなすまでの時間です
この時間が過ぎたタイミングでまだ手を離していなくてもイベントが起きます
当初は離したタイミングにしていたのですが それだとタッチ中にもう離していいのかわからないと思うことが多かったのでやめました
触れてる状態でイベントが起きてそれに対応する変化が起きたのを見てから安心して離せるほうがいいと思います

移動距離

移動距離についてですが タッチしながら指を動かした場合は基本はスワイプなどの別の操作になります
なので touchmove イベントは touchend と同じ扱いで良いと思っていたのですが 意図せず touchmove が起きることがありました

Android では初回のみ touchmove は起きづらくてちょっと動かしただけだと何もなくて明らかに動かしたと言えるくらい動いて初めて touchmove が起きます
動かしたつもり無いのにをいい感じに吸収してくれています
それに対して iOS の場合は感度が良すぎて動かしたつもりないのに touchmove が起きることが多かったです
1 秒くらい全く動かさないように集中が必要になるのも疲れるので多少の範囲は動いても無視するようにしました
それが移動距離の設定でここで指定した距離以内であれば touchmove イベントが起きてもタッチを継続してる扱いにします

このときの距離は px で計算するのでズームレベルによって 指が動いた距離が同じでも px は異なります
ズームアウトしてるときほど ちょっとの指の移動で多くの px を移動することになります
ただ目的の場所をタップするときにズームアウトして対象を小さい状態で操作することはあまりなくてどちらかというとズームインしてからタップすることが多いと思うのであまり問題にはしてません

タッチ対応判定難しい

タッチ環境はマイナーなものでほとんどは PC を想定しています
それなのにリスナつけるだけになるとはいえ タッチ操作できない端末でもタッチ用の有効化処理とかしたくないなと思って

if (window.ontapstart !== undefined) {
enableLongTap(elem)
}

のようにしていました

基本的にタッチ非対応の端末では undefined になっていて 対応していればその他の onclick などと一緒で null が入ってます

ただ Surface だとタッチが使えますがあくまで PC 扱いなのでここは undefined になっていてタッチ非対応として扱われてしまいます
navigator あたりの別プロパティで判定はできるものの Stackoverflow を探してみるとあらゆる端末で判定する機能は結構複雑な分岐で UA まで見てたりと そのチェック処理のほうが大変そうです

今回の場合は分岐しなくてもリスナが余分につくだけで害はないので常に enableLongTap は実行するようにしました