Vue.jsでライブラリを使わずに長押し機能を作る
スマートフォン・タブレットでよくありがちな「長押しで何かの動作をさせる」機能を Vue.js でライブラリを使わずに作ってみます。
ライブラリを使う場合は以下のスクラップに適当にまとめているので参考にできるかと。
環境
- Windows 10 21H2 (Build 19044.2130)
- iPhone SE 第 3 世代 - iOS 16.0
- iPad Pro 第 3 世代 - iPadOS 15.7
- Google Pixel 3a - Android 12
- Vue.js 2.7.10
- Nuxt.js 2.15.8
長押し機能を導入したかったプロジェクトで Nuxt を使っていたので環境一覧に入れていますが、Nuxt を利用していない環境でも利用可能です。
コード
コンポーネントとして利用できます。
<template>
<div
class="long-press-wrapper"
@pointerdown="startPress"
@pointerup="endPress"
@pointermove="endPress"
@pointercancel="endPress"
@click="endPress"
@contextmenu.prevent
>
<slot />
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
export default Vue.extend({
props: {
onLongPress: {
type: Function as PropType<() => void>,
required: true,
},
delay: {
type: Number,
default: 500,
},
},
data(): {
pressTimer: NodeJS.Timeout | null
} {
return {
pressTimer: null,
}
},
methods: {
startPress(): void {
this.pressTimer = setTimeout(() => {
this.onLongPress()
}, this.delay)
},
endPress(): void {
if (!this.pressTimer) return
clearTimeout(this.pressTimer)
this.pressTimer = null
},
},
})
</script>
<style scoped>
.long-press-wrapper {
user-select: none;
pointer-events: none;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-user-select: none;
}
</style>
<template>
<VLongPress :on-long-press="onLongPress" :delay="1000">
<img src="https://picsum.photos/300/100">
</VLongPress>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
methods: {
onLongPress(): void {
alert("long-pressed")
},
},
})
</script>
実際に試せる CodePen は以下から。画像を長押しするとアラートが出てくるはずです。
注意するべき点
この機能を実装する時に注意すべき事項として、「スマートフォン・タブレットの OS 機能としての長押し機能」と「スマートフォン・タブレットでのスクロール」があります。
スマートフォン・タブレットの OS 機能としての長押し機能
iOS や Android といったスマートフォン・タブレットの主要 OS では、OS として「長押し」に対して機能を設けています。マウスとキーボードで操作するパソコンとは異なり、左クリックの概念がないからですね。
iOS, Android ともに、文字列を長押しすると画像のようなポップアップが表示され、文字列をコピーしたり検索できます。
iPadOS | Android |
---|---|
Web ページとして長押しで何かをするといった機能を提供する場合、OS の長押し機能を無効化しないと Web ページ側の長押し機能が動かないか、Web ページ・OS 両方の長押し機能が動いてしまう恐れがあります。
したがって、Web ページ側で OS の長押し機能を無効化します。しかし、ブラウザによって対応が異なります。
以下に示すいくつかの CSS プロパティと JavaScript コードの組み合わせで無効化できます。リンク先は日本語の MDN です。
- CSS:
user-select: none
- CSS:
-webkit-user-select: none
- CSS:
-webkit-touch-callout: none
- CSS:
pointer-events: none
- JavaScript:
contextmenu.prevent
(contextmenu
イベントが発行されたときにpreventDefault()
でキャンセルすることを指しています)
これらに関する記事は昔から多くあるのですが、インターネット上の情報のみを参考にして実装すると、各種アップデートによって仕様が変わっており期待通りに動作しない恐れがあります。
ここでは、CodePen で適当なコードを書き、実機で検証しながら確認します。
実際に検証した結果が以下の通りです。(一応パソコンでの検証もしました)
OS ごとの環境情報は #環境 を参照ください。
「デフォルト」というのは、何も CSS や JavaScript を要素に対して割り当てないそのままの状態のことを指しています。
テキストの選択可否
ここでは、各テキスト要素を選択・長押しすることでテキストが選択できるかどうかを確認します。
ブラウザ | デフォルト | user-select |
-webkit-user-select |
-webkit-touch-callout |
pointer-events |
contextmenu.prevent |
---|---|---|---|---|---|---|
Windows Chrome 106.0.5249.119 | ○ | × | × | ○ | ○ | ○ |
Windows Firefox 106.0.1 | ○ | × | × | ○ | ○ | ○ |
iOS Safari | ○ | ○ | × | ○ | ○ | ○ |
iOS Chrome 106.0.5249.92 | ○ | ○ | × | ○ | ○ | ○ |
iOS Firefox 106.0 (20303) | ○ | ○ | × | ○ | ○ | ○ |
iPadOS Safari | ○ | ○ | × | ○ | ○ | ○ |
iPadOS Chrome 106.0.5249.92 | ○ | ○ | × | ○ | ○ | ○ |
iPadOS Firefox 105.1 (19787) | ○ | ○ | × | ○ | ○ | ○ |
Android Chrome 106.0.5249.126 | ○ | × | × | ○ | ○ | ○ |
Android Firefox 105.2 (20159) | ○ | × | × | ○ | △[1] | △[1:1] |
iOS / iPadOS Chrome での検証の際、当たり判定が大きすぎるのかテキスト部分を長押ししているのに画像部分を選択していることになって困りました…。img 要素を一時的に消すことで対応しました。
テキスト選択でのメニュー表示有無
ここでは、各テキスト要素を選択して右クリック・長押したときにテキストに対してのメニューが表示されるかを確認します。
ブラウザ | デフォルト | user-select |
-webkit-user-select |
-webkit-touch-callout |
pointer-events |
contextmenu.prevent |
---|---|---|---|---|---|---|
Windows Chrome 106.0.5249.119 | ○ | △[2] | △[2:1] | ○ | ○ | × |
Windows Firefox 106.0.1 | ○ | △[2:2] | △[2:3] | ○ | ○ | × |
iOS Safari | ○ | ○ | ×[3] | ○ | ○ | ○ |
iOS Chrome 106.0.5249.92 | ○ | ○ | ×[3:1] | ○ | ○ | ○ |
iOS Firefox 106.0 (20303) | ○ | ○ | ×[3:2] | ○ | ○ | ○ |
iPadOS Safari | ○ | ○ | ×[3:3] | ○ | ○ | ○ |
iPadOS Chrome 106.0.5249.92 | ○ | ○ | ×[3:4] | ○ | ○ | ○ |
iPadOS Firefox 105.1 (19787) | ○ | ○ | ×[3:5] | ○ | ○ | ○ |
Android Chrome 106.0.5249.126 | ○ | ×[3:6] | ×[3:7] | ○ | ○ | × |
Android Firefox 105.2 (20159) | ○ | ×[3:8] | ×[3:9] | ○ | ○ | ○ |
画像選択でのメニュー表示有無
ここでは、各画像要素を右クリック・長押ししたときに画像に対してのメニューが表示されるかを確認します。
ブラウザ | デフォルト | user-select |
-webkit-user-select |
-webkit-touch-callout |
pointer-events |
contextmenu.prevent |
---|---|---|---|---|---|---|
Windows Chrome 106.0.5249.119 | ○ | ○ | ○ | ○ | ○ | × |
Windows Firefox 106.0.1 | ○ | ○ | ○ | ○ | ○ | × |
iOS Safari | ○ | ○ | ○ | △[4] | × | ○ |
iOS Chrome 106.0.5249.92 | ○ | ○ | ○ | △[4:1] | × | ○ |
iOS Firefox 106.0 (20303) | ○ | ○ | ○ | △[4:2] | × | ○ |
iPadOS Safari | ○ | ○ | ○ | △[4:3] | × | ○ |
iPadOS Chrome 106.0.5249.92 | ○ | ○ | ○ | △[4:4] | × | ○ |
iPadOS Firefox 105.1 (19787) | ○ | ○ | ○ | △[4:5] | × | ○ |
Android Chrome 106.0.5249.126 | ○ | ○ | ○ | ○ | × | × |
Android Firefox 105.2 (20159) | ○ | ○ | ○ | ○ | ○ | × |
この検証結果をもとにすると、-webkit-user-select: none
と pointer-events: none
、contextmenu.prevent
を適用すればテキストでも画像でも長押し関連の OS 機能を無効化できそうです。
とはいえ、画像をコピー・ダウンロードしてほしくないからこれらを適用するという発想を安直にする場合はやめた方がよいと思います。どうせ開発ツールから解除できるので…。
…と思っていたのですが、pointer-events: none
を指定してしまうと pointerdown
イベントなども動かなくなるので、CSS で background-image: url()
を使って背景に画像を設定してしまうのが適切なのかなあと思います。
スマートフォン・タブレットでのスクロール
安直に実装しようとすると、「クリック・タップされたあと、数ミリ秒後にまだタップし続けてたら長押しってことにしよう!」と考えます。
対象要素が「スクロールする時に間違いなくタップしない」要素なら良いのですが、そうでない場合スクロールのたびに長押し機能が動作し、滅茶苦茶にイライラすることになります。(一敗)
スマートフォン・タブレットでは、スクロールする際に Web ページ上の任意の箇所をタップしてから指を上または下に動かします。
「タップした時」と「タップをやめた時」のイベントだけを使うと、「スクロールを始める時」にタップをし、「スクロールをやめた時」にタップをやめるので、長押しと同じ状態が起きます。
なので、「タッチして指を移動した」場合に長押しと判断しないよう、pointermove
イベントを受け取る必要があります。
その他
この記事では、昔からある mousedown
などの MouseEvent や touchstart
などの TouchEvent を使わずに pointerdown
など PointerEvent を利用しています。
PointerEvent を利用する理由として、純粋な記述量が少なくなるだけでなく、TouchEvent と MouseEvent は 両方同時発生しうる ためです。
2019 年初期ごろまでは Safari が PointerEvent をサポートしていなかった ので使うべきではなかったのですが、少なくとも 2022 年現在は主要ブラウザで利用可能なので、気にせず使ってしまって良いと考えています。
-
対象テキスト要素の長押しでは選択できないものの、ダブルタップでは選択できます。もちろん、「デフォルト」なら長押しでも選択できます。(Chrome ではダブルタップで選択できません) ↩︎ ↩︎
-
そもそも対象要素の選択ができないので、テキストに対するメニューは表示されませんがその要素上で右クリックできます(ページに対するメニューが表示される) ↩︎ ↩︎ ↩︎ ↩︎
-
そもそも対象要素の選択ができないので、要素に対するメニューすら開くことができません。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
非常に特殊な挙動をします。ほかの ○ のケースでは画像を長押しすると「画像の保存」や「画像のコピー」ができるメニューが開くのですが、このケースではそれは開きません。
その代わり、iPad のマルチタスク機能 が動作して、「画面から少し浮き上がって見える」ようになります。
参考までに、動画を撮影して Google ドライブにアップしておきました: vue-long-click-ipad-multi-task.mp4
なお、pointer-events
の場合はマルチタスク機能も動作しないようです。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Discussion