結論
pointer-eventsは罠。
ひたすらz-indexを書き換えろ。
(そもそも、兄弟要素でD&Dとクリックを共存しようとすることが間違っているのでは?)
はじめに
先に結論を書いたため既に「はじめに」ではないが、先の結論を得るまでに2日くらい無駄に頭を悩ませたため備忘録の意味と、また、それらしい記事を遂に見つけられなかった故に後の人が見つけられるような記事となるように筆を執る事とした。
(頭を悩ませるその時間があったら素直に親子要素に書き換えればよかったのでは?)
前提状況
ファイルのアップロードをドラッグ&ドロップで実現したいと思う事自体はわりとよくある事に思う。
どうせD&Dする意味なんてアップロードしかないため、対象エリアは広いほどやりやすかろう、と画面全体をドロップゾーンにするようにposition:fixed、width:100vw、height:100vhな不可視(opacity:0)エリアを作ることとした。ファイルアップロード用のinputタグも触られたり見られたら嫌なので、透過しつつmargin-left:-500pxとでもしておこう。
さて、ファイルをD&Dするだけではしょうがないので、それはそれとしてコンテンツを書き始める。
ドロップゾーンと親子関係にするとネストが深くなって気持ち悪いからコンテンツは兄弟要素として作ろう(死亡フラグ)。
アップロードしたファイルをどう処理するかの指示を出すようなフォームも付けるとしようか。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>サンプル</title>
<link href="./index.css" rel="stylesheet" />
<script src="./index.js"></script>
</head>
<body>
<div id="drop-zone">
<form action="" method="post" enctype="multipart/form-data" onsubmit="alert('drop!')">
<!-- ファイルをドラッグ&ドロップする不可視エリア -->
<input type="file" name="file" id="file-input">
</form>
</div>
<div class="no-drop-zone">
<input type='checkbox' value="1">ファイルを良い感じに処理する選択肢1
<div>
コンテンツ1
</div>
<div>
コンテンツ2
</div>
</div>
</body>
</html>
#drop-zone {
height: 100vh;
width: 100vw;
position: fixed;
opacity: 0;
}
#file-input {
opacity: 0;
margin-left:-500px;
}
'use strict'
window.onload = () => {
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
dropZone.addEventListener('dragover', function (e) {
// イベント伝搬の停止
e.stopPropagation();
e.preventDefault();
}, false);
dropZone.addEventListener('dragleave', function (e) {
// イベント伝搬の停止
e.stopPropagation();
e.preventDefault();
}, false);
fileInput.addEventListener('change', function () {
alert('post process');
});
dropZone.addEventListener('drop', function (e) {
// イベント伝搬の停止
e.stopPropagation();
e.preventDefault();
//ドロップしたファイルを処理
const files = e.dataTransfer.files;
// ...
alert('post process');
}, false);
};
さて、この辺で一度、画面の感じを見てみるか。…ん?
あれ、チェックボックス触れないな? んん? よく触ってみるとコンテンツもマウスではアクティブにできないな?
聡い諸氏、というか検索した結果としてここに至った人たちならばお気付きかもしれないが、一番表側にD&D用のドロップゾーンがあるので、そいつに全マウスイベントが奪われてる。
このままではコンテンツ部分をマウス操作できぬ。
よく見つかる回答(失敗例)
全くダメな奴
pointer-events:none;にするとマウスイベントを透過してくれるよ。
→ D&Dも透過するのでファイルアップロードできない。
少し考えられたダメな奴
疑似要素hoverを使ってpointer-events:none;とautoを切り替えるといいよ。
→ マウスオーバーしている要素が高速で切り替わるので、文字列の選択やボタンにフォーカスを当てた際の見かけが不安定になる。
結論(再)
pointer-eventsは変えてはならない。z-indexを変える。
という事で、cssのみ以下のように更新する。
.no-drop-zone {
position: relative;
z-index: 1;
}
#drop-zone:hover~.no-drop-zone {
z-index: 3;
}
.no-drop-zone:hover {
z-index: 3;
}
#drop-zone {
z-index: 2;
height: 100vh;
width: 100vw;
position: fixed;
opacity: 0;
}
#file-input {
opacity: 0;
margin-left: -500px;
}
肝となるのは、上から2つ目と3つ目の『#drop-zone:hover~.no-drop-zone』『.no-drop-zone:hover』によるz-index指定。
自己保持回路的な考え方で、ドロップゾーンにホバーすると非ドロップゾーンが上に来て、そのままその状態を維持する、という内容。
ファイルをD&Dするときにはカーソルが非ドロップゾーン(というかウィンドウ)の外側なので、ドロップゾーンが最上位に来るという仕掛け。
pointer-eventsをnoneにするとhoverも解除されるっぽいので、pointer-eventsでは自己保持ができず高速スイッチングが発生する。
終わりに
結論が出てしまえば何も難しい事はしていないのだけれども、これが全く検索できなかった。
そもそもの敗因は親子ではなく兄弟で始めてしまったことなのだが、それがここまで話を拗らせる要因になるとは当初は思っていなかったので仕方がない。
ちなみに、さっきChatGPTに聞いたら特に紆余曲折なく(親子の場合の)正常に動くコードを吐き出した。
AIに完全敗北してて生きるのが辛い。
はやくAIが全ての仕事を奪ってほしい。
少しでも仕事を残すと結局ぼくみたいなヤツが無駄に仕事を増やすので、この世から完全に仕事を消し去ってくれ。
補足というか言い訳というか
親子要素だとイベントが子供要素に伝搬させる/させない制御がjs上でできるので、その辺をいじるとすんなりできるっぽい。
兄弟要素だと長兄(というかz-indexで上位にある要素)にしかマウスイベントが伝わらない。
きちんと把握していれば便利な違いだと思うのだけれど、HTMLとCSSを触ることなんて年に1,2回趣味で触る程度だから今回くらい痛い目をあと2回くらいみないと忘れる。