ドラッグ&ドロップでブラウザのデフォルト動作と闘ったメモ
ブラウザの長押しドラッグを殺して mobile-drag-drop だけ動かしたい
Vue アプリをタッチデバイスで動かしてドラッグ&ドロップするためにmobile-drag-dropを導入した。すんなり動いたけど、ドラッグ対象の要素がそのまま指の動きについてくる場合と、飛び出してセンタリングしてついてくる場合がある。どうやら前者は mobile-drag-drop によるもので、後者は長押しで発動するブラウザのデフォルト挙動らしい。どちらが良いかは場合によりそうだけど、今回は mobile-drag-drop の方だけ生かそうと思った。
試行錯誤
長押し判定のフック処理かと思ったが..
HTML 標準 ではタッチ長押しのイベントらしきものがなかった。contextmenu が長押しで発動する場合はあるけど、関係なさそう。
ドラッグ判定の始点は touchstart のようだから、フックして試行錯誤してみたけど..
function onTouchStart(event) {
if (event.target === draggable要素) {
event.preventDefault();
}
}
意図通りにブラウザの長押しドラッグは発動しなくなったが、この要素のタッチを起点とするスクロールもできなくなった。デフォルト動作のスクロールは必要なので殺したくない。
touch-action の設定かな?
じゃあtouch-actionで制御できるのかな?と思って以下を試してみた。
スクロールさせたい親要素: {
touch-action: pan-x pan-y;
}
設定は効いているっぽいが、残念ながら長押しドラッグは普通に出てくる。
VueUse の onLongPress で何か分かるか
VueUse にonLongPressという機能があるので、何か切り口になればと思って試してみた。onLongPress 自体はすんなり動くが、ブラウザの長押しドラッグも同時に発動する。modifiers に prevent: true を付けたら何か起きそうな気もしたけど期待外れ。
結論(上手くできた方法)
試行錯誤中に関係ないバグも出てきて迷子になりかけたけど、結果的に以下の方法で上手くいった。
function onDragStart(event) {
if (!event.isTrusted) {
// mobile-drag-dropによる発火
event.dataTransferの設定;
} else {
// 長押しタッチによる発火
event.preventDefault();
}
}
ブラウザによって発火したイベントは isTrusted=true に、mobile-drag-drop によるイベントは isTrusted=false になることに気付いたので、dragstart イベントの入口でそれを見て、ブラウザ発火の場合はデフォルト挙動を止めた。これで長押しドラッグは出てこなくなる。
一方で mobile-drag-drop の場合は event.dataTransfer を設定してデフォルト挙動を動かす。一時は dragstart を丸ごと preventDefault()してドラッグ処理を自作しようかと思いつめたけど楽に済んだ。
isTrusted 属性を本来の意味では使ってないので完全に非推奨な方法だと思うけど、今回の目的には上手くハマった気がする。
マウスでもドラッグする場合はマウスの down/up も拾って、例えば以下のように書く必要がある。
function onDragStart(event) {
if (!event.isTrusted || isMouseDown) {
// マウス or mobile-drag-dropによる発火
event.dataTransferの設定;
} else {
// 長押しタッチによる発火
event.preventDefault();
}
}
let isMouseDown = false;
function onMouseDown() {
isMouseDown = true;
}
function onMouseUp() {
isMouseDown = false;
}
余談
もしかして mobile-drag-drop に限らず polyfill のタッチイベントってみんなスクリプト生成で isTrusted=false になってるのかな? 結構広く使われてそうだけど、isTrusted 属性の運用とどう折り合いがついてるんだろうか..
Discussion