UI Events¶
重要なユーザーインターフェースイベントとその扱い方を見ていく。
Mouse events¶
<https://javascript.info/mouse-events-basics> のノート。
マウスイベントは物理的なマウスから以外にも起こる可能性がある。PC だけとは限らない。
Mouse event types¶
主なマウスイベント:
Event |
Description |
---|---|
|
要素上でマウスボタンが押されたときに起こる。 |
|
要素上でマウスボタンが離されたときに起こる。 |
|
マウスポインターが要素に来たときに起こる。 |
|
マウスポインターが要素から離れたときに起こる。 |
|
マウスが要素上を動くたびに起こる。 |
|
左ボタンにより、要素上から |
|
短時間に同一要素上で二度の |
|
右ボタンを押すと起こる。 |
コンテキストメニューはキーからも表示される。右手側の Alt と Ctrl の間にあるキーだ。
右ボタンの場合には、メニューが表示されるのはボタンを離した瞬間だ。
Events order¶
上にもあるように、一つのマウスアクションから複数のマウスイベントが起こることがある。
例えば、左ボタンをクリックすると、まずボタンが押されたときに
mousedown
が起こり、それからボタンが離されたときにmouseup
とclick
が起こる。単一のアクションが複数のイベントを引き起こす場合、その順序は一定だ。
Modifiers: shift, alt, ctrl and meta¶
マウスイベント処理中にユーザーが押している修飾キーを参照することもできる。押されていれば値は true
だ:
event.shiftKey
event.altKey
event.ctrlKey
: Mac ユーザーに便宜を図るため、ふつうはevent.ctrlKey || event.metaKey
をテストする。
Coordinates: client{X,Y}
, page{X,Y}
¶
マウスイベントはカーソル位置を保持している。以前の章で見た二つの座標系に対応している。
event.clientX
,event.clientY
: ウィンドウ座標系event.pageX
,event.pageY
: ドキュメント座標系
Preventing selection on mousedown¶
マウスイベントを握りつぶすには、一般のイベントと同様にハンドラーが return
false
すればいい。ここでは dblclick
でテキスト選択が発生しないようにする例を挙げている。
囲み記事。テキスト選択そのものを処理するには copy
イベントハンドラーを対応する。
Tasks¶
Selectable list¶
まず、修飾キーの要件を無視して click
を実装する。CSS クラスを変更するメソッド各種には、これまでの演習で慣れている前提だ。この状態で選択解除とテキスト選択が解決できていない。
テキスト選択は囲み記事にある手法でも解決するが、これは場合によっては許されない。本当にマウスによる選択しか禁止しないのであれば、
mousedown
を潰す方法を採る。Ctrl キーを押しているときの振る舞いのほうが実装は容易だ。他の項目の状態を考慮しなくていい。
模範解答では
event.preventDefault()
を呼んでいない。
Moving the mouse: mouse{over,out}
, mouse{enter,leave}
¶
<https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave> のノート。
マウスが要素間を移動するときに発生するイベントについて。
Skipping elements¶
要素が複数配置されているところをマウスが比較的高速に移動する場合、開始要素から終了要素の間にある要素の上でこれらのイベントハンドラーが反応しないことが普通にある。それでも、mouseover
が発生した場合には、対になる mouseout
も必ず発生する。
Mouseout when leaving for a child¶
親子関係にある要素間では注意点が二つある:
親から子にマウスを移動させると、カーソルを親が含むにも関わらず、親に対する
mouseout
が発生する。イベントの bubbling が適用されるので、親から子に移動させると、親から出てまたすぐに戻ってきたようにも見えるかもしれない。
サンプルデモにおいて、ハンドラーは親要素にある。
勘違いしないように event.target
と event.relatedTarget
をチェックすること。あるいは、次に述べるイベントペアを対応すること。
Events mouseenter
and mouseleave
¶
次に mouseenter
, mouseleave
を見る。これらもペアで押さえる。先述のイベントハンドラーペアと似たものだが、親子関係の注意点二つが成り立たない。
要素内の移動、子孫への移動は考慮されない。
イベントの bubbling がない。
ということは、イベントハンドラーを親要素にだけ置いて、子要素すべての面倒を見る手法は採用できないということだ。
Event delegation¶
こちらのサンプルデモにも、ハンドラーは親要素にある。
八卦図のデモ二つ。前者はマウスの運動によってはセルというよりも中身のテキストだけがハイライトされてしまう。後者はその不具合を解決してセルしかハイライトされないようになっている。コードをよく分析すること。
共通点
TABLE
にonmouseover
,onmouseout
各ハンドラーを実装する。
改良点
現在ハイライト中の
TD
を保存しておく変数を設ける。onmouseover
を、event.target
がその現在ハイライト中のセルと変わっていなければ何も処理しないようにする。変わっていれば変数を更新し、改良前と同様の処理をする。onmouseout
はもう少し面倒になる。event.relatedTarget
をチェックし、さらにその親方向へチェックする。TD
を離れていくことが確定したら、ハイライト中セル変数をnull
とする。
Tasks¶
Improved tooltip behavior¶
現在表示中のツールチップ DOM 要素を保存しておく変数を設ける。
mouseover
,mouseout
ハンドラーをそれぞれdocument
に対して定義する。mouseover
では、まずツールチップ対応部分にマウスがいるかどうかを判定する。判定は以前のときのように
closest()
を利用する。引数はクラス名になる。ツールチップ要素を
DIV
として作成する方法は以前と同じ。生成後の要素には CSS クラス、中身、位置を指定するが、今回も位置が大事だ。ツールチップ要素の位置はマウスカーソル位置と対象要素の寸法から適当に決めていい。後から要件に従うように調整できる。
mouseout
では現在のツールチップ要素を存在すればremove()
する。そして、現在ツールチップ変数をリセットする。
“Smart” tooltip¶
さっきの例でマウスを高速で動かすと、この問題の意図が理解できる。
丁寧にもサンドボックスに単体テストが付属している。全部がパスするまでコードを書く。サンドボックスにコードが途中まで書かれているが、埋める場所は実は指定されているところだけではダメだ。
メソッド
trackSpeed()
を実装する。これがけっこう手が込んでいる。メソッド
onMouseOver()
およびonMouseOut()
内で対象要素にmousemove
のイベントハンドラーthis.onMouseMove
を着脱する。メソッド
onMouseMove()
はシンプルにマウスカーソル位置と時刻を更新する。座標はドキュメント座標系で持たせるのがコツのようだ。すなわちpage{X,Y}
を採用する。難しいと思ったのが速度の更新だ。タイマーで速度を計算するメソッドを仕込む。速度を更新する頻度を適切に決める方法が問われている。模範解答ではピクセルパーミリ秒という単位で速度と比較し、タイマーをクリアするようにしている。速度の算出方法はバカ正直に(ピタゴラスの定理で)移動距離を計算して、時間の差で除算する。
このタイマークリア直後の
this.call.over(this.elem)
により、コンストラクターで指定したツールチップ表示コードが発動する。
Drag’n’Drop with mouse events¶
<https://javascript.info/mouse-drag-and-drop> のノート。
仕様上は dragstart
, dragend
などのドラッグ&ドロップ用のイベントがある。しかし、それらは制限があったり、機能が弱かったりする。そこで、本章では
mousedown
, mousemove
, mouseup
でドラッグ&ドロップを実装する。
Drag’n’Drop algorithm¶
ブラウザー既定の挙動を取り除くため
dragstart
からイベント通知が拡がらないようにする。つまりreturn false
とする。mousedown
でドラッグの準備をする。ドラッグ対象要素の属性を変える。CSS が
position: absolute
とz-index: 1000
になるようにする。それに関係して対象要素をいったん
body
の子になるように移す。対象要素の座標を文書座標系で指定する。
mousemove
とmouseup
両ハンドラーを指定する。
mousemove
ハンドラーは座標更新処理しかしない。座標をきめ細かく取る。開始直後のマウスカーソル位置から対象要素がズレないように工夫する。本文では、対象要素座標系の原点とカーソル位置の変位を意識して位置を更新している。
mouseup
ハンドラーでドロップ処理およびクリナップをする。仕込んだ両ハンドラーを解除する。
これらのマウスイベントハンドラーを
document
に対して仕込むのが急所だ。
Correct positioning¶
ドラッグ開始時点のマウスポインターの座標を要素座標系に変換する。そして、ドラッグ中の要素の座標を、現在のマウス座標から上記座標の変位を加味して決める。
// mousedown
const rc = elem.getBoundingClientRect();
const shiftX = event.clientX - rc.left;
const shiftY = event.clientY - rc.top;
// mousemove
elem.style.left = `${event.pageX - shiftX}px`;
elem.style.top = `${event.pageY - shiftY}px`;
Potential drop targets (droppables)¶
今度はドロップ先の要素を特定することを考える。
ドラッグ中の要素がいちばん手前にあるため、一時的に
hidden = true
する。すると、絶好のelementFromPoint(clientX, clientY)
の応用状況となる。ドロップを受け入れることが可能な要素であるかどうかは、要素に CSS クラスを与えるなりなんなりすればいい。その上で
elem.closest()
により検索する。
Tasks¶
Slider¶
サッカーボールのコードをそのままパクるだけだと、スライダーが自由にドラッグしてしまう。これに拘束をかければ良い。とくに、y 座標の処理は不要。
与えられたサンドボックスコードは、すでに
DIV
がposition: relative
になっているので、サッカーボールのコードの配置関連コードは不要となる。
Drag superheroes around the field¶
要件 1 の急所は
document
にmousedown
ハンドラーを実装することと、マウスカーソル位置からclosest('.draggable')
で得られる要素をドラッグすることの二つ。要件 2 の縦スクロールが大きい場合の処理。次のものを利用する:
document.documentElement.clientHeight
dragElement.offsetHeight
window.scrollBy(0, scrollY)
要件 3 の横スクロール禁止。
document.documentElement.clientWidth
dragElement.offsetWidth
要件 4 は要件 2, 3 と一緒に実装する。
このデモではドラッグ可能要素の
position
をドラッグ中の間だけfixed
にする。座標計算をより容易にする意味がある。
Pointer events¶
<https://javascript.info/pointer-events> のノート。
マウスだけでなく、ペン、スタイラス、タッチスクリーンなど、ポインティングデバイス一般からの入力を処理する方法を見ていく。
The brief history¶
歴史的には、まずタッチスクリーンを対応する必要が生じたので、次のようなタッチイベントが導入された:
touchstart
touchend
touchmove
しかし、さらなるデバイスが登場したり、それらのイベントハンドラーを個別に書くのが面倒になったりしてくる。そこで本章で見ていく一連のイベントが導入された。これから書くスクリプトでは、マウスやタッチ固有のハンドラーではなく、ポインターハンドラーを書けばいい。
Pointer event types¶
ポインターイベントは、
mousemove
に対応するpointermove
といった具合に、マウスイベントと同様の名前が付けられている。それらに加え、ポインターイベントには三つの固有イベントが定義されている。基本的には既存コードの
mousexxxx
をpointerxxxx
に置換することでマウスもタッチなども動作すると期待してよい。ただし、CSS のいくつかの場所でtouch-action: none
を追加する必要があるかもしれない。
Pointer event properties¶
マウスイベントプロパティーと同じもの。
clientX
,target
, etc.pointerId
: イベントを発生させるポインターの IDpointerType
:"mouse"
,"pen"
,"touch"
のいずれかの文字列。isPrimary
: マルチタッチの場合の、優先的なポインターであるかどうか?デバイスによってはさらなるプロパティーが仕様で定められているが、ほとんどのデバイスがこれらを対応していない。したがって、めったに使われないプロパティーということだ。
Multi-touch¶
ユーザーがタッチスクリーンのある場所に触れた後、別の指をタッチスクリーンのどこかに置くと、次のようなことが起こる:
最初の指のタッチでは
isPrimary=true
であるpointerdown
と、何らかのpointerId
次以降の指(最初の指がまだ触れていると仮定)では
isPrimary=false
であるpointerdown
と各指に対して異なるpointerId
タッチしている複数の指を、それぞれの pointerId
を使って追跡することになる。ユーザーが指を動かしてから離すと、pointerdown
で得たのと同じ pointerId
を持つ pointermove
と pointerup
イベントが起こる。
このデモを PC とマウスで試しても面白くないことに注意。
Event: pointercancel
¶
イベント pointercancel
は、ポインターのやりとりが続いているときに発生するもので、その後、何かが起きてそれが中断され、さらなるポインターイベントが発生しないようにする。
例えばドラッグ&ドロップをポインターイベントで実装するなどすると、ブラウザーの既定の挙動が pointercancel
を発生させて妨害される。ここではその回避策を述べている。
まず、前章のマウスによるドラッグ&ドロップで述べた仕組みがポインターイベントでも成り立つことから、ドラッグ要素の
dragstart
ハンドラーにreturn false
させる。タッチデバイスの場合を考慮する。タッチ関連のブラウザーアクションはドラッグ&ドロップ以外にある。それらについても問題を回避するには、ドラッグ要素に対してCSS で
touch-action: none
と設定する。
改良版サッカーデモでは、ボールをドラッグしようとすると、ブラウザーが余計なことをしなくなることしかまだ確認できない。
Pointer capturing¶
ここでは elem.setPointerCapture(pointerId)
と
elem.releasePointerCapture(pointerId)
を述べている。Win32 API の
SetCapture(hWnd)
, ReleaseCapture()
のポインター版と解釈できる。
前章のスライダーバーの実装では document
に対してイベントハンドラーを定義していたが、これらの捕捉用メソッドをスライダーに対して利用すればスマートだ:
文書全体に対してハンドラーを追加・削除する必要がなくなり、コードがすっきりする。
文書内に他のポインターイベントハンドラーがある場合、ユーザーがスライダーをドラッグしている間にポインターがよその要素に行っても、そのイベントハンドラーが引き起こされることがなくなる。
Pointer capturing events¶
万全を期すために、残りの二つのポインター固有のイベントについても述べられている。
gotpointercapture
:elem.setPointerCapture()
が呼び出されたときに発生する。lostpointercapture
:elem.releasePointerCapture()
が明示的に呼び出されたときか、pointerup
やpointercancel
イベントにより自動的にポインター捕捉が解除されたときに発生する。
Keyboard: keydown
and keyup
¶
<https://javascript.info/keyboard-events> のノート。
冒頭にいい警告がある。やりたいことは、キーボードを使うことが本当に必要であるのかと。
Teststand¶
このデモは keydown
, keyup
イベントの概要と event.preventDefault()
のおさらい。既定の挙動を妨害すると、テキストボックスに文字が打ち出されなくなる。
Keydown
and keyup
¶
まずは event.key
と event.code
の違いを理解する。ひとまず前者を文字、後者を物理的キー(言い換えるとキーの位置)を表すものと解釈しておく。
event.key
は実際の文字(列)を値に取るか、文字がなければ特別な値を取る。event.code
は"KeyA"
,"Digit8"
,"Enter"
,"Tab"
などの文字列を値に取る。
本文では両者の違いについて細かく解説している。国によってキー配置が異なるから、
event.key
を採るか event.code
を採るかは、アプリケーションの目的による。
Auto-repeat¶
同じキーを長時間押し続けていると keydown
イベントが何度も繰り返し発生し、最後に``keydown`` が一度発生する。これを自動繰り返しという。自動繰り返しイベントでは event.repeat
の値は true
となっている。
Default actions¶
OS あるいはそれ以下のレベルで定められているショートカットキーによるコマンド起動以外は、JavaScript のいつもの方法で既定の挙動を妨害できる。
電話番号用に
INPUT
タグのkeydown
イベントハンドラーを書く例はわかりやすい。しかし、普通は別のイベントハンドラーを書くこと。
Legacy¶
この節に書いてあるイベントもプロパティーも旧式のものだ。今から書くコードでは採用しない。
Mobile Keyboards¶
仮想キーボードを使用する場合、e.key
は "Unindentified"
となるはずだ。
Tasks¶
Extended hotkeys¶
キー同時押しを判定するときには keydown
だけでなく keyup
の処理も必要となる。
可変個引数を取る関数の書き方を思い出す。
JavaScript の
Set
は扱いづらい。
Scrolling¶
<https://javascript.info/onscroll> のノート。
スクロールを監視するには
scroll
イベントを処理する。イベント
scroll
はwindow
とスクロール可能要素の両方で処理される。
Prevent scrolling¶
今まで見てきた UI イベントとは異なり、scroll
ハンドラーで
event.preventDefault()
を使っても、スクロールを妨害することはできない。このイベントはすでにスクロールが起こった後に発生するものだ。したがって、妨害するにはスクロールの原因となるイベント、たとえば PGUP や PGDN キーを押されたのを感知して
event.preventDefault()
を呼び出すなど、工夫する必要がある。
スクロールを許す方法はいろいろあるので、CSS の overflow:
プロパティーを利用するのが確実だ。
Tasks¶
Endless page¶
最後までスクロールするというのは、実際には閲覧者が文書の末端から何ピクセルか以上離れていないくらいの意味に解釈すること。
この例題はナンセンスなものではなく、現実にはよく使われるパターンだ(商品リストなど)。
イベントハンドラーは
window
に付与する。今回はスクロールすると
document.documentElement.getBoundingClientRect()
のtop
とbottom
が変動する。ウィンドウの高さは
document.documentElement.clientHeight
を見る。今はドキュメントの下部がそこから何ピクセルか以上離れていないときを知る必要がある。
const doc = document.documentElement; while(doc.clientHeight + 100 < doc.getBoundingClientRect().bottom){ document.body.add(now); }
Load visible images¶
ページが所定の位置にスクロールされてから画像などをロードする問題だ。
本問では関数 isVisible(elem)
を埋めるだけでいい。これは IMG
要素のクライアント領域の上下端の座標と document.documentElement.clientHeight
とを比較すればいい。
この比較の数値をオフセットするとプリロードの効果が得られる。
Comments¶
変位
shiftX
を自分で計算するのではなくevent.offsetX
を代わりに使うといいようだ。携帯電話で動かないという指摘が当然あるが、マウスではなく
pointerxxxx
イベントを使えばいいだろう。elem.elementFromPoint()
の仕様は MDN と本書とで違うように見えるが、矛盾していない。