web-dev-qa-db-ja.com

マウスオーバーイベントオブジェクトがタッチスクリーンタッチによるものかどうかを識別する方法

事実上すべての現在のブラウザー(広範な githubのpatrickhlaukeからの詳細 、これは SO回答 に要約しました)、さらにいくつかの詳細- quirksModeから )、タッチスクリーンタッチはmouseoverイベントをトリガーします(ユーザーが他の場所をタッチするまで、タッチした場所にとどまる非表示の疑似カーソルを作成する場合があります)。

場合によっては、タッチ/クリックとマウスオーバーが異なることを行うことを目的とした場合に、これが望ましくない動作を引き起こすことがあります。

eventオブジェクトが渡されたマウスオーバーイベントに応答する関数の内部から、これが要素の外側から要素の内部に移動した移動カーソルからの「実際の」マウスオーバーであったかどうか、またはそれがタッチスクリーンタッチによるこのタッチスクリーンの動作が原因でしたか?

eventオブジェクトは同じように見えます。たとえば、Chromeでは、ユーザーがタッチスクリーンに触れたことによるマウスオーバーイベントにはtype: "mouseover"があり、タッチ関連としてそれを特定するものはありません。

イベントをtouchstartにバインドしてマウスオーバーイベントを変更し、次にイベントをtouchendにバインドしてこの変更を削除するというアイデアがありました。残念ながら、これは機能しません。イベントの順序がtouchstarttouchendmouseoverclickのように見えるためです(他の機能を台無しにせずに、正規化マウスオーバー関数をクリックしてクリックすることはできません)。


私はこの質問が以前に尋ねられることを期待していましたが、既存の質問はそれを完全にカットしません:

私が考えることができる最高のことは、touchstartwindow.touchedRecently = true;などのグローバルにアクセス可能な変数フラグを設定し、クリックしないで、500msのsetTimeoutの後にこのフラグを削除するタッチイベントを持つことです。これは醜いハックです。


注-タッチスクリーンとマウスのようなものを使用する多くのデバイスがあるため、タッチスクリーンデバイスにはマウスのようなロービングカーソルがないか、またはその逆であると想定しますできません画面の近くにカーソルを置いているときにカーソルを移動するペン、またはタッチスクリーンとマウスを使用するペン(タッチスクリーンラップトップなど)。私の回答の詳細 ブラウザがマウスオーバーイベントをサポートしているかどうかを検出するにはどうすればよいですか

注#2-これはjQueryの質問ではありませんではありません、私のイベントはRaphael.jsのパスから送られてきます。jQueryはオプションではなく、プレーンバニラブラウザーのeventオブジェクト。ラファエル固有のソリューションがある場合はそれを受け入れますが、それは非常にまれであり、raw-javascriptソリューションの方が優れています。

問題の複雑さを考えると、潜在的な解決策に関連する問題とEdgeのケースを詳しく説明する価値があると思いました。

問題:

1-デバイスおよびブラウザー間でのタッチイベントの異なる実装。一部で機能するものは、他の機能では機能しません。 patrickhlauke のリソースを一目見れば、タッチスクリーンをタップするプロセスが現在どのデバイスやブラウザでどのように処理されているかを理解することができます。

2-イベントハンドラーは、最初のトリガーに関する手掛かりを与えません。また、 eventオブジェクトは、マウスとのインタラクションによって送出されるマウスイベントと、タッチインタラクションによって送出されるマウスイベントの間で(ほとんどの場合、確かに)同一であると述べています。

3-すべてのデバイスをカバーするこの問題の解決策は、現在のように短命である可能性があります W3Cの推奨事項では、タッチ/クリックイベントの処理方法については十分に詳しく説明していません( https://www.w3.org/TR/touch-events/ )。そのため、ブラウザーは引き続き異なるものになります。実装。また、タッチイベントの標準ドキュメントは過去5年間変更されていないようです。したがって、これはすぐには修正されません。 https://www.w3.org/standards/history/touch-events

4-理想的には、タッチイベントからマウスまでの時間が定義されていないため、ソリューションはタイムアウトを使用しないでくださいイベント、そして仕様を考えると、おそらくすぐにはありません。残念ながら、後で説明するように、タイムアウトはほとんど避けられません。


将来の解決策:

将来的には、解決策はおそらくマウスの代わりに使用することですPointer Events /タッチイベントはpointerTypehttps://developer.mozilla.org/en-US/docs/Web/API/Pointer_events )を提供するため、残念ながら、確立された標準に関してはまだ存在するため、ブラウザ間の互換性( https://caniuse.com/#search=pointer%20events )は貧弱です。


現時点でこれをどのように解決しますか

それを受け入れると:

  1. タッチスクリーンを検出できない( http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
  2. できたとしても、タッチ対応画面での非タッチイベントの問題はまだあります

その後、マウスイベント自体に関するデータのみを使用して、その発生元を特定できます。私たちが確立したように、ブラウザはこれを提供しないので、それを自分で追加する必要があります。これを行う唯一の方法は、マウスイベントとほぼ同時にトリガーされるタッチイベントを使用することです。

patrickhlauke リソースをもう一度見てみると、次のように説明できます。

  1. mouseoverの後には常にクリックイベントmousedownmouseupclickが続きます-常にこの順序で。 (時々他のイベントによって分けられる)。これは、W3Cの推奨事項 https://www.w3.org/TR/touch-events/ によってバックアップされています。
  2. ほとんどのデバイス/ブラウザでは、mouseoverイベントの前に常にpointerover、そのMSの対応物MSPointerOver、またはtouchstartが付きます。
  3. イベントの順序がmouseoverで始まるデバイス/ブラウザーは無視する必要があります。マウスイベントがタッチイベントの前にトリガーされたことを確認できません(---)タッチイベント自体がトリガーされました。

これを前提として、pointeroverMSPointerOver、およびtouchstartの間にフラグを設定し、いずれかのクリックイベントの間にフラグを削除できます。これは、いくつかのケースを除いて、うまく機能します。

  1. event.preventDefaultは、タッチイベントの1つで呼び出されます-クリックイベントが呼び出されないため、フラグが設定解除されることはありません。したがって、この要素の今後の本物のクリックイベントは、引き続きタッチイベントとしてマークされます。
  2. イベント中にターゲット要素が移動された場合。 W3C勧告の状態

ドキュメントのコンテンツがタッチイベントの処理中に変更された場合、ユーザーエージェントは、マウスイベントをタッチイベントとは異なるターゲットにディスパッチできます。


残念ながら、これは常にタイムアウトを使用する必要があることを意味します。私の知る限り、タッチイベントがevent.preventDefaultを呼び出したときに確立する方法も、タッチ要素がDOM内で移動され、別の要素でクリックイベントがトリガーされたときを理解する方法もありません。

これは魅力的なシナリオだと思うので、この回答は、推奨されるコード応答が含まれるように間もなく修正されます。とりあえず、@ ibowankenobiが提供する回答、または@Manuel Ottoが提供する回答をお勧めします。

7
Perran Mitchell

私たちが知っていることは:

ユーザーがマウスを使用しない場合

  • mouseoverは、touchendまたはtouchstart(ユーザーがタップしてホールドした場合)の直後に(800ms以内に)発生します。
  • mouseovertouchstart/touchendの位置は同じです。

ユーザーがマウス/ペンを使用する場合

  • mouseoverは、タッチイベントの前に発生します。そうでない場合でも、mouseoverの位置は、タッチイベントの位置の99%の時間と一致しません。

これらの点を念頭に置いて、スニペットを作成しました。これにより、リストされた条件が満たされた場合にイベントにフラグ_triggeredByTouch = true_が追加されます。さらに、この動作を他のマウスイベントに追加するか、_kill = true_を設定して、タッチによってトリガーされたマウスイベントを完全に破棄できます。

_(function (target){
    var keep_ms = 1000 // how long to keep the touchevents
    var kill = false // wether to kill any mouse events triggered by touch
    var touchpoints = []

    function registerTouch(e){
        var touch = e.touches[0] || e.changedTouches[0]
        var point = {x:touch.pageX,y:touch.pageY}
        touchpoints.Push(point)
        setTimeout(function (){
            // remove touchpoint from list after keep_ms
            touchpoints.splice(touchpoints.indexOf(point),1)
        },keep_ms)
    }

    function handleMouseEvent(e){
        for(var i in touchpoints){
            //check if mouseevent's position is (almost) identical to any previously registered touch events' positions
            if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
                //set flag on event
                e.triggeredByTouch = true
                //if wanted, kill the event
                if(kill){
                    e.cancel = true
                    e.returnValue = false
                    e.cancelBubble = true
                    e.preventDefault()
                    e.stopPropagation()
                }
                return
            }
        }
    }

    target.addEventListener('touchstart',registerTouch,true)
    target.addEventListener('touchend',registerTouch,true)

    // which mouse events to monitor
    target.addEventListener('mouseover',handleMouseEvent,true)
    //target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)
_

実際に試す:

_function onMouseOver(e){
  console.log('triggered by touch:',e.triggeredByTouch ? 'yes' : 'no')
}



(function (target){
        var keep_ms = 1000 // how long to keep the touchevents
        var kill = false // wether to kill any mouse events triggered by touch
        var touchpoints = []

        function registerTouch(e){
                var touch = e.touches[0] || e.changedTouches[0]
                var point = {x:touch.pageX,y:touch.pageY}
                touchpoints.Push(point)
                setTimeout(function (){
                        // remove touchpoint from list after keep_ms
                        touchpoints.splice(touchpoints.indexOf(point),1)
                },keep_ms)
        }

        function handleMouseEvent(e){
                for(var i in touchpoints){
                        //check if mouseevent's position is (almost) identical to any previously registered touch events' positions
                        if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
                                //set flag on event
                                e.triggeredByTouch = true
                                //if wanted, kill the event
                                if(kill){
                                        e.cancel = true
                                        e.returnValue = false
                                        e.cancelBubble = true
                                        e.preventDefault()
                                        e.stopPropagation()
                                }
                                return
                        }
                }
        }

        target.addEventListener('touchstart',registerTouch,true)
        target.addEventListener('touchend',registerTouch,true)

        // which mouse events to monitor
        target.addEventListener('mouseover',handleMouseEvent,true)
        //target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)_
_a{
  font-family: Helvatica, Arial;
  font-size: 21pt;
}_
<a href="#" onmouseover="onMouseOver(event)">Click me</a>
5
Manuel Otto

https://www.html5rocks.com/en/mobile/touchandmouse/ によると
シングルクリックの場合、イベントの順序は次のとおりです。

  1. タッチスタート
  2. タッチムーブ
  3. タッチエンド
  4. マウスオーバー
  5. マウスムーブ
  6. マウスダウン
  7. マウスアップ
  8. クリック

したがって、onTouchStart()で任意のブール値isFromTouchEvent = true;を設定し、onClick()でisFromTouchEvent = false;を設定して、onMouseOver()内でそれを確認できる場合があります。これは、リッスンしようとしている要素のすべてのイベントを取得することが保証されていないため、うまく機能しません。

4
Glen Pierce

通常、これに使用するいくつかの一般的なスキームがあります。そのうちの1つは、手動でsetTimeoutの原則を使用してプロパティをトリガーします。ここではこれについて説明しますが、まずタッチデバイスでtouchstart、touchmove、touchendを使用する理由を考え、destopでマウスオーバーを使用します。

ご存知のように、タッチイベントのいずれかでevent.preventDefault(これがtouchstartで機能するためにはイベントはパッシブではない必要があります)を呼び出すと、後続のマウスコールがキャンセルされるため、それらを処理する必要はありません。しかし、これがあなたが望むものではない場合、ここで私は時々使用します(私はあなたのdom操作ライブラリの「ライブラリ」、そしてあなたの要素として「elem」と呼びます):

setTimeoutを使用

library.select(elem) //select the element
.property("_detectTouch",function(){//add  a _detectTouch method that will set a property on the element for an arbitrary time
    return function(){
        this._touchDetected = true;
        clearTimeout(this._timeout);
        this._timeout = setTimeout(function(self){
            self._touchDetected = false;//set this accordingly, I deal with either touch or desktop so I can make this 10000. Otherwise make it ~400ms. (iOS mouse emulation delay is around 300ms)
        },10000,this);
    }
}).on("click",function(){
    /*some action*/
}).on("mouseover",function(){
    if (this._touchDetected) {
        /*coming from touch device*/
    } else {
        /*desktop*/
    }
}).on("touchstart",function(){
    this._detectTouch();//the property method as described at the beginning
    toggleClass(document.body,"lock-scroll",true);//disable scroll on body by overflow-y hidden;
}).on("touchmove",function(){
    disableScroll();//if the above overflow-y hidden don't work, another function to disable scroll on iOS.
}).on("touchend",function(){
    library.event.preventDefault();//now we call this, if you do this on touchstart chrome will complain (unless not passive)
    this._detectTouch();
    var touchObj = library.event.tagetTouches && library.event.tagetTouches.length 
        ? library.event.tagetTouches[0] 
        : library.event.changedTouches[0];
    if (elem.contains(document.elementFromPoint(touchObj.clientX,touchObj.clientY))) {//check if we are still on the element.
        this.click();//click will never be fired since default prevented, so we call it here. Alternatively add the same function ref to this event.
    }
    toggleClass(document.body,"lock-scroll",false);//enable scroll
    enableScroll();//enableScroll
})

SetTimeoutのない別のオプションは、mousoverがtouchstartのカウンターであり、mouseoutがtouchendのカウンターであると考えることです。したがって、以前のイベント(タッチイベント)はプロパティを設定します。マウスイベントがそのプロパティを検出した場合、イベントは発生せず、プロパティを初期値にリセットします。その場合、これらの線に沿った何かも行います:

setTimeoutなし

....
.on("mouseover",function(dd,ii){
                    if (this._touchStarted) {//touch device
                        this._touchStarted = false;//set it back to false, so that next round it can fire incase touch is not detected.
                        return;
                    }
                    /*desktop*/
                })
                .on("mouseout",function(dd,ii){//same as above
                    if(this._touchEnded){
                        this._touchEnded = false;
                        return;
                    }
                })
                .on("touchstart",function(dd,ii){
                    this._touchStarted = true;
                    /*some action*/
                })
                .on("touchend",function(dd,ii){
                    library.event.preventDefault();//at this point emulations should not fire at all, but incase they do, we have the attached properties
                    this._touchEnded = true;
                    /*some action*/
                });

多くの詳細を削除しましたが、これが主なアイデアだと思います。

3

あなたはそのためにmodernizrを使うことができます!これをローカルの開発サーバーでテストしたところ、動作しました。

if (Modernizr.touch) { 
  console.log('Touch Screen');
} else { 
  console.log('No Touch Screen');
} 

だから私はそこから始めますか?

2
Joseph Chambers