2021-07-20Movidea開発日記
https://gyazo.com/f8bac0032f9b9fc356ffc5b1c9a95184
---
マウスイベント周りの問題、前回Regroupの時はかなり複雑になってから自覚したのでしっかり理解することができなかったが、今回は理解できた
https://gyazo.com/eff9b3ce1c710834d545360b4c3fecd3
人間にとって好ましい、自然な、「意味の塊」は左のようなもの
グループのドラッグでの移動
範囲選択
選択範囲のドラッグでの移動
しかし現実にはブラウザの仕様によって
ドラッグでの移動のdropイベントはどちらの場合も共通
どの操作をしててもmousemoveやmouseupは呼び出される(追記: この解釈がそもそも間違いなのを後述)
Regroupの時はすべてCanvas要素の上で操作されることもあって全部mousedown開始だった
https://gyazo.com/85789af4ab3dcdc611e2080af074c7cb
バツ印は「mousedown後、mousemoveが発生せずに、mouseupが発生した時に行う処理」の意味
要するにクリック
そこでこうした
ハンドラーオブジェクトに「一塊の処理」をまとめて、mousedownのタイミングでどのハンドラーを使うかを決定
https://gyazo.com/c7c5e860137e7dc2d7aa033be8fca133
一見良さそうに見えるがダメ
複数のハンドラで共通して行わなければいけない処理がある
それぞれに書いたら漏れが出る
この問題を解決するために「もう一枚レイヤーを挟んで共通で行う処理をまとめて行う」という設計変更が行われた
mousedownの段階で処理が確定することを暗黙に仮定していた
そうでないケースが扱いにくくなってしまった
mousedownが来た直後にもう一つmousedownが来てマルチタッチになったら?
「付箋をグループにドロップすることでグループの中に入れたい」はドロップ時の状況による場合分け
それを踏まえて今回のこの状況をどう整理すればいいか
https://gyazo.com/eff9b3ce1c710834d545360b4c3fecd3
わかった気がしたがまたわからなくなった…
暗黙に状態が発生しているから、それを陽に管理したらいいのでは、と思ったのだった
現状のコードではmousedownでisDraggingフラグを立ててる
いや、ダメだな、現状の実装で既に「僕の理解」で書いた右の図と実際の振る舞いが違う
mouseDown, mouseMove, dragStart, dropの順で実行されてしまう
その結果フラグが立ちっぱなし
実際の挙動
https://gyazo.com/7f89d3da0b90f44e7fc1710c725600a8
今回の場合、上の処理の開始位置にある要素はDOM的に下の処理の要素に包含されてるので、mousedownを上にもつけて、そちらでつかんでstopPropagateする手がある
っていうかそれ以外の手がなさそう
要するに、ソースコードにコード上の包含関係に基づくレキシカルスコープがあるのと同様に、DOMにもその包含関係に基づくスコープがあるわけだ
そう考えるとmousedownでフラグを立てて、フラグが立ってる時だけmousemoveで特定の処理をするのは動的スコープで時間軸上の区間を切り取ってるのだな
それを図で表現するとこうなる
https://gyazo.com/3452e6e3fc8f0ba0929872154279528c
これを踏まえて今回必要なことを整理するとこうなる
https://gyazo.com/0c46bca6d85909389dd36c513b665706
今回はまだシンプルだから設計できたけど、これもう2〜3件増えたら図に描くことができなくなるよな
これを踏まえて今回どうするか
あわてて変なレイヤーを入れたりしない
あわてて分割しない
「選択範囲の移動」と「グループ内の付箋を外に出す」を実装してテストケースを書いてからリファクタリングする
---
2021-07-21
一晩寝て気づいたこと
選択時に選択対象の要素を移し替えるなら
選択範囲ができた後、選択範囲の外をクリックしたとき、元に戻さなければならない
つまりここにも状態が発生している
状態はテストの対象になるべき
今はローカル変数が状態を持っているが、これをグローバルにする
before
code:ts
let isDragging = false;
export const onMouseDown = (...) => {
isDragging = true;
...
};
export const onMouseMove = (...) => {
if (isDragging) {
...
}
};
export const onMouseUp = (...) => {
if (isDragging) {
...
isDragging = false;
}
};
after
code:ts
export const onMouseDown = (...) => {
updateGlobal((g) => {
...
g.mouseState = "selecting";
});
};
export const onMouseMove = (...) => {
const g = getGlobal();
if (g.mouseState === "selecting") {
...
}
};
export const onMouseUp = (...) => {
const g = getGlobal();
if (g.mouseState === "selecting") {
updateGlobal((g) => {
...
g.mouseState = "selecting"; // intentional bug, should be ""
});
}
};
test
code:test.ts
cy.get("#canvas").trigger("mousedown", 50, 100);
cy.getGlobal((g) => g.mouseState).should("to.eql", "selecting");
cy.get("#canvas").trigger("mouseup", 300, 400);
cy.getGlobal((g) => g.mouseState).should("to.eql", ""); // intentional fail
これでおかしな状態になってる時に検知できるようになった
故意のバグはテストがちゃんとこけることを確認してから修正した
昨日の「dragstartの前にmousedownが来ることに気付いてなかった」をテストケースで検証
code:test.ts
cy.testid("1").trigger("dragstart", "center");
cy.getGlobal((g) => g.mouseState).should("to.eql", "");
cy.get("#canvas").trigger("drop", 250, 250);
これでfailするかと思ったら、しない、なるほど
人間が操作する時には先にmousedownが発生する
テストケースではdragstartイベントを発行してるだけだからmousedownが発生しない
これを現実のイベントと同じようにテストするとこうなるか
code:test.ts
it("is not selecting", () => {
cy.testid("1").trigger("mousedown", "center");
cy.testid("1").trigger("mousemove", "center");
cy.testid("1").trigger("dragstart", "center");
cy.getGlobal((g) => g.mouseState).should("to.eql", "");
cy.get("#canvas").trigger("drop", 250, 250);
});
これは期待通りにfailする
で、stopPropagationして期待通りに動くことを確認done
ここまではいい。次は朝気づいた「選択後」の状態について
選択範囲のドラッグで解除されるべきでない
選択範囲外のmousedownでは解除されるべき
あえて図に描くとこう
https://gyazo.com/651a16356375b1045fad41bffd6b5827
うーむ🤔
https://gyazo.com/57c3b688037db501edbe090f08bd39d6
Aに解除コードを書くことはできる
しかし今後似たようなものが増えた時に書き漏らしそう
全部に自動的に解除コードをつけることはできない
Bで解除してはいけないから
自然言語でいうなら「選択範囲以外でのmousedownで解除」
この「以外」とは何か?
選択範囲にDOM的に包含されてる付箋でのmousedownは?
これは「DOMの重ね順序」(おっと、また新しいスコープだ)
DOMの重ね順序的に選択範囲は選択された付箋(の大部分)より手前にあるのでイベントをブロックするはず
https://gyazo.com/f77daf0dca10987a865497d17abc7775
Cのクリックで付箋にmousedownが発生しない
Dは選択範囲外クリックだから選択解除して良い
待てよ、ということは「選択されたものをdivに追加」ってやる時に別のdivが必要?
z-indexでいい?
いや、違うな、選択範囲を可視化するためのdivを選択範囲をまとめるために使う必要がない
「選択されてないもの」のopacityを下げることで選択されたものをハイライトすることにした
https://gyazo.com/954963b304cfd7aff577f27a97efc3f2
これはまだ状態の解除を実装してない
あー、ダメだ、選択範囲の表示をドラッグと選択されたオブジェクトを包むdivが分かれたら「選択範囲の表示」をドラッグしても選択されたオブジェクトが動かないじゃん…
複数個選択してドラッグで移動するところまではできたが「選択範囲の表示」が移動してない
https://gyazo.com/91dae056758bbe6355c7754dd1f24c47
できた
https://gyazo.com/f8bac0032f9b9fc356ffc5b1c9a95184
テストケースを書いた結果、期待した位置から縦方向に2ピクセルだけズレてることが明らかになった(苦笑
top:150pxのdivの(0, 2)をdragstartして、実際に発生するイベントは(150, 150)
Cypress環境でしか再現してなさそう...
code:test.ts
cy.getGlobal((g) => g.selected_items).should("to.eql", items);
cy.getGlobal((g) => items.map((id) => g.itemStoreid.position)).should( "to.eql",
[
]
);
cy.get("#selection-view").trigger("dragstart", 0, 2); // misterious 2px
cy.get("#canvas").trigger("drop", 100, 100);
cy.getGlobal((g) => items.map((id) => g.itemStoreid.position)).should( "to.eql",
[
]
);