手入力禁止 (コピペ専用) のメールアドレス入力欄を作ってみる

世の中には、「入力ミス防止のためコピペ禁止」などと**ぬかしやがる**メールアドレス入力欄がある。
**マジふざけんな。**
手入力なんかしたら**typoの原因**になるだろ**馬鹿野郎。**
「確認のため2回入力してください」という指示もよくあるが、そもそもの記憶が間違っている場合は**何十回入力させようがそれで間違いを防げる可能性は低い**だろう。

メーラーなどの確実なソースからメールアドレスをコピペすることで、typoも記憶間違いも防ぎ、正しいメールアドレスを入力できる可能性を上げることができる。
そこで、今回は逆に**手入力禁止 (コピペ専用)** のメールアドレス入力欄を作ってみた。

:::note warn
メーラーからメールアドレスをコピペしても、例えば以下の問題を防ぐことはできず、完璧ではない。

* 本来入力したい自分のメールアドレスではなく、メールの相手のメールアドレスをコピーしてしまう
* 複数のメールアドレスを使い分けている人が、本来使いたいのと別のメールアドレスをコピーしてしまう
* コピーしたメールアドレスが、クリップボードの内容を自動で加工するソフトウェアにより意図と異なる文字列に変化する
:::

# メールアドレス入力欄への手入力を禁止する

まずはメールアドレスの入力欄を用意する。

```html:HTML
<input id="sample1" type="email" value="">
```

[`beforeinput` イベント](https://developer.mozilla.org/ja/docs/Web/API/Element/beforeinput_event) で `preventDefault()` を用いることで、ほとんどの入力をキャンセルできる。
ただし、IME を用いた入力 ([`inputType`](https://developer.mozilla.org/ja/docs/Web/API/InputEvent/inputType) が `"insertCompositionText"` の場合) はキャンセルできないので、入力内容を記録しておいて [`input`](https://developer.mozilla.org/ja/docs/Web/API/Element/input_event) イベントで書き戻すことで対処する。

:::note info
`input` 要素の `value` プロパティは、`beforeinput` イベントの処理時は**変更前**の値、`input` イベントの処理時は**変更後**の値を表す。
:::

```javascript:JavaScript: 全ての入力を禁止する
const sample1 = document.getElementById("sample1");
let prevValue = null;
sample1.addEventListener("beforeinput", function(e) {
  e.preventDefault();
  if (e.inputType === "insertCompositionText") prevValue = sample1.value;
});
sample1.addEventListener("input", function(e) {
  if (e.inputType === "insertCompositionText") {
    sample1.value = prevValue;
    prevValue = null;
  }
});
```

イベントオブジェクトの [`inputType` プロパティ](https://developer.mozilla.org/ja/docs/Web/API/InputEvent/inputType)により、どのような入力が行われたかを確認できる。
値と意味のリストは、[Input Events Level 2](https://w3c.github.io/input-events/#interface-InputEvent-Attributes) にある。
値の最初の部分 (接頭辞) により、大まかに以下の種類にわけることができる。

|値の最初の部分|入力操作|
|---|---|
|`insert`|テキストの追加や上書き|
|`delete`|テキストの削除|
|`history`|アンドゥ・リドゥ|
|`format`|スタイルの設定|

今回は、以下の仕様にする。

* テキストが貼り付けられたら、(挿入ではなく) テキスト全体をそのテキストに設定する
* テキストが削除されたら、(一部ではなく) テキスト全体を削除 (空文字列に設定) する
* その他の操作は無視 (禁止) する

今回は、以下の種類を「テキストの貼り付け」であるとみなす。
これらの種類において、イベントオブジェクトの [`data` プロパティ](https://developer.mozilla.org/ja/docs/Web/API/InputEvent/data)で貼り付けられた文字列を取得できる。

:::note warn
テキストではなく画像がクリップボードに入っている状態で貼り付け操作を行った場合など、`data` プロパティが `null` である場合もある。
:::

|種類|意味|
|---|---|
|`insertFromYank`|キルバッファからの貼り付け|
|`insertFromDrop`|ドロップによる貼り付け|
|`insertFromPaste`|クリップボードからの貼り付け|
|`insertFromPasteAsQuotation`|引用としてクリップボードから貼り付け|

また、`delete` で始まるすべての種類を「テキストの削除」であるとみなす。

実装例を以下に示す。

```javascript:JavaScript: 貼り付けのみを受け付ける
const pasteOperations = new Set([
  "insertFromYank", "insertFromDrop",
  "insertFromPaste", "insertFromPasteAsQuotation",
]);
const sample1 = document.getElementById("sample1");
let prevValue = null;
sample1.addEventListener("beforeinput", function(e) {
  e.preventDefault();
  if (e.inputType === "insertCompositionText") prevValue = sample1.value;
  if (pasteOperations.has(e.inputType)) {
    // 貼り付け
    if (typeof e.data === "string") sample1.value = e.data;
  } else if (e.inputType.substring(0, 6) === "delete") {
    // 削除
    sample1.value = "";
  }
});
sample1.addEventListener("input", function(e) {
  if (e.inputType === "insertCompositionText") {
    sample1.value = prevValue;
    prevValue = null;
  }
});
```

# 手入力されようとしたら警告を出す

今回の入力欄に手入力がされようとしたら、単に無視するのではなく警告を出したい。
警告を出す方法の一つとして、バリデーション失敗 (入力されたデータが設定した条件を満たさない) 時の警告を利用する方法がある。
この方法は、[`setCustomValidity()`](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement/setCustomValidity) メソッドでエラーメッセージを設定し、[`reportValidity()`](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement/reportValidity) メソッドでエラーメッセージを表示することで実現できる。

ただし、`setCustomValidity()` でエラーメッセージを空でない文字列に設定すると、入力が常に無効であるとみなされる。
今回の実装では、手入力は無視するため、手入力を試みても入力されているデータは無効にならない。
そのため、実際には無効ではないにもかかわらず無効であるとみなされ、問題になる可能性がある。
無効判定が行われる「直前」に発火し、無効状態の解除に使えるようなイベントは見つからなかった。

そこで、今回は入力されているデータが空の場合のみエラーメッセージを設定し、警告を出す仕様にした。
データが空の場合は、貼り付けによる入力が行われる前であるため、警告によりユーザーに手入力ではなく貼り付けをするよう指示を出す意味が大きいと考えられる。
さらに、メールアドレスの入力が必須である場合は、データが空のときはどうせ無効なので、エラーメッセージを設定により無効状態になっていても大きな問題にならない。
有効な入力 (貼り付け) および削除が行われたとき、無効状態を解除するため、エラーメッセージを空文字列に設定する。

以下に実装例を示す。

```javascript:JavaScript: 手入力されようとしたら警告を出す
const pasteOperations = new Set([
  "insertFromYank", "insertFromDrop",
  "insertFromPaste", "insertFromPasteAsQuotation",
]);
const sample1 = document.getElementById("sample1");
let prevValue = null;
sample1.addEventListener("beforeinput", function(e) {
  e.preventDefault();
  if (e.inputType === "insertCompositionText") prevValue = sample1.value;
  if (pasteOperations.has(e.inputType)) {
    // 貼り付け
    if (typeof e.data === "string") sample1.value = e.data;
    sample1.setCustomValidity("");
  } else if (e.inputType.substring(0, 6) === "delete") {
    // 削除
    sample1.value = "";
    sample1.setCustomValidity("");
  } else if (e.inputType.substring(0, 6) === "insert") {
    // 手入力
    if (sample1.value === "") {
      // データが空の場合、警告を出す
      sample1.setCustomValidity("入力ミス防止のため、コピペで入力してください。");
      sample1.reportValidity();
    }
  }
});
sample1.addEventListener("input", function(e) {
  if (e.inputType === "insertCompositionText") {
    sample1.value = prevValue;
    prevValue = null;
  }
});
```

# デモ

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="GRagaGw" data-user="mike_cat" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mike_cat/pen/GRagaGw">
  paste-only email area test</a> by MikeCAT (<a href="https://codepen.io/mike_cat">@mike_cat</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<details><summary>ソースコード</summary>

```html:HTML
<form method="POST" action="https://example.com/" id="theform">
  <p>
    メールアドレスを入力してください。<br>
    <strong>入力ミス防止のため、手入力はせず、コピペしてください。</strong><br>
    <input name="email" type="email" value="" size="50" required>
  </p>
  <p>
    <input type="submit" value="送信">
  </p>
</form>
```

```javascript:JavaScript
const theform = document.getElementById("theform");
theform.addEventListener("submit", function(e) {
  e.preventDefault();
  alert(theform.email.value);
});

const pasteOperations = new Set([
  "insertFromYank", "insertFromDrop",
  "insertFromPaste", "insertFromPasteAsQuotation",
]);
let prevValue = null;
theform.email.addEventListener("beforeinput", function(e) {
  e.preventDefault();
  if (e.inputType === "insertCompositionText") prevValue = theform.email.value;
  if (pasteOperations.has(e.inputType)) {
    // 貼り付け
    if (typeof e.data === "string") theform.email.value = e.data;
    theform.email.setCustomValidity("");
  } else if (e.inputType.substring(0, 6) === "delete") {
    // 削除
    theform.email.value = "";
    theform.email.setCustomValidity("");
  } else if (e.inputType.substring(0, 6) === "insert") {
    // 手入力
    if (theform.email.value === "") {
      // データが空の場合、警告を出す
      theform.email.setCustomValidity("入力ミス防止のため、コピペで入力してください。");
      theform.email.reportValidity();
    }
  }
});
theform.email.addEventListener("input", function(e) {
  if (e.inputType === "insertCompositionText") {
    theform.email.value = prevValue;
    prevValue = null;
  }
});
```

</details>

試してみるのに便利なよう、コピー元となる適当なメールアドレスを用意した。

```text
webmaster@example.com
```

# おわりに

今回は、入力ミス防止のため手入力を禁止し、コピペによる入力のみを受け付けるメールアドレスの入力欄を作ってみた。
これは、世の中に出回っている**コピペ禁止のメールアドレス入力欄に対するアンチテーゼ**として作成した、**技術的な実験**である。
実際に運用するウェブサイトに、このような入力欄を設置することを推奨するものではない。
とはいえ、設置することを禁止はしないので、設置するかの判断は**自己責任で**行ってほしい。

:::note warn
今回の実装では、「手入力」と「パスワード管理ソフトやキーボードマクロツールなどによる自動入力」を区別することができない。
このような受け付ける操作を限定する入力欄を設置し、ユーザーに使用させることは、ユーザーの体験を損ねる可能性がある。
このような入力欄を設置するかを検討する際は、これを含むデメリットも考慮した上で、慎重に判断してほしい。
:::