📚

HTMLのdialogタグを使ってみた

2023/10/04に公開

https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog

dialogのユーザーエージェントスタイル

これはChromeの例。

dialog {
    display: none;
    position: absolute;
    inset-inline-start: 0px;
    inset-inline-end: 0px;
    width: fit-content;
    height: fit-content;
    background-color: canvas;
    color: canvastext;
    margin: auto;
    border-width: initial;
    border-style: solid;
    border-color: initial;
    border-image: initial;
    padding: 1em;
}

特に気を付ける必要があるのは、position:absolutemargin:auto
スタイルがうまく当たらない、JSで思ったように操作できない(後述)ときはデフォルトのスタイルを疑おう(戒め)。

dialogは2種類ある

showModal()で表示したときと、show()open属性で表示したときで要素の扱いが変わるらしい。

show()やopenで表示した場合
特に特別な仕様はない。

showModal()で表示した場合

  1. Escapeキーでモーダルを閉じることができる。
  2. ::backdropというCSS疑似要素が使える。
  3. 最上位レイヤー(top layer)に表示される。
  4. 3によって、最後に開かれたモーダル以外は操作不能になる。

最上位レイヤーについてはcolissで。
https://coliss.com/articles/build-websites/operation/css/what-is-the-top-layer.html

JavaScriptで操作したい

個人的に困ったのが、ESCで閉じられたときにイベントを発火する方法。
モーダルが閉じるときには方法にかかわらずcloseイベントが発行されるため、モーダルを開いたときに「closeイベントが発火したときの処理」を書いておくことでボタンを押されたときと同一の処理ができる。

モーダルを開く

function openModal(target) {
  let _element = document.getElementById(target);
  _element.showModal();// モーダルを表示
  
  // モーダルのid名で処理を分岐する
  if (target === "dialog-help") {
    _element.addEventListener("close", () => {
	// モーダルが閉じられたときの処理
    });
  }
}

モーダルを閉じる

// ボタンが押されたときにモーダルを閉じる
function closeModal(e) {
  e.target.offsetParent.close();
}

ドラッグしてみる

参考記事

https://qiita.com/kotazuck/items/0465250d6e026983fa50

実践

ドラッグするために下記のようなdialogを用意した。

当然、青色の部分を掴んでドラッグすると移動するようにしたい。

HTML

<dialog id="dialog-help" class="dialog">
  <!-- 青色の部分(dialog_header) -->
  <div class="dialog_header" draggable="true">
    <div class="dialog_header--icon">?</div>
    <h2 class="dialog_header--ttl">ヘルプ</h2>
    <button class="dialog_header--close" @click="closeModal">close</button>
  </div>
  <!-- ダイアログの内容 -->
  <div class="dialog_content">
    <h3 class="dialog_content--ttl u-mb10">U+1F4BB</h3>
    <div class="dialog_content--img"><img src="./../img/logo.png" alt="" width="40" height="40" /></div>
    <button v-if="!_playing.playing" class="dialog_btn -submit" autofocus="autofocus" @click="closeModal">はじめる</button>
    <button v-else class="dialog_btn -submit" @click="endPlaying">やめる</button>
    <button class="dialog_btn" @click="openModal('dialog-play')">あそびかた</button>
    <button class="dialog_btn" @click="openModal('dialog-about')">このサイトについて</button>
  </div>
</dialog>

JavaScript

  function draggableModal() {
  // https://qiita.com/kotazuck/items/0465250d6e026983fa50
  const Modals = document.getElementsByTagName("dialog");
  [...Modals].forEach((element) => {
    let _target = element.getElementsByClassName("dialog_header")[0];
    let _mouse_in_modal = { x: 0, y: 0 }; // モーダルのどこをつかんで移動を開始したか保存する用

    function MouseDown(evt) {
      // mouseのとき || touchのとき
      let _pointerX = evt.clientX || evt.touches[0].clientX;
      let _pointerY = evt.clientY || evt.touches[0].clientY;
      _mouse_in_modal.y = element.offsetTop - _pointerY;
      _mouse_in_modal.x = element.offsetLeft - _pointerX;
      if (evt.dataTransfer) {
        evt.dataTransfer.setDragImage(document.createElement("div"), 0, 0);
      }
    }
    function MouseMove(evt) {
      if (evt.x === 0 && evt.y === 0) return;

      let _pointerX = evt.clientX || evt.touches[0].clientX;
      let _pointerY = evt.clientY || evt.touches[0].clientY;

      const top = _pointerY + _mouse_in_modal.y;
      const left = _pointerX + _mouse_in_modal.x;

      element.style.margin = `0px`; // 1.座標を画面左上基準にする
      // 2.モーダルが画面外にはみ出さないように制限
      element.style.top = `${Math.min(Math.max(top, 0), window.innerHeight - element.clientHeight)}px`;
      element.style.left = `${Math.min(Math.max(left, 0), window.innerWidth - element.clientWidth)}px`;
    }
    function MouseUp() {
      _mouse_in_modal = {
        x: 0,
        y: 0,
      };
    }
    
    // 3.スマホ対応
    _target.addEventListener("pointerdown", (evt) => {
      // pointerEventでポインターの種類を確認
      if (evt.pointerType != "touch") {
        _target.addEventListener("dragstart", MouseDown);
        _target.addEventListener("drag", MouseMove);
        _target.addEventListener("dragend", MouseUp);
      } else {
        // touchのとき
        _target.addEventListener("touchstart", MouseDown);
        _target.addEventListener("touchmove", MouseMove);
        _target.addEventListener("touchend", MouseUp);
      }
    });
    _target.addEventListener("pointerup", (evt) => {
      _target.removeEventListener("touchmove", MouseMove);
    });
  });
}

余談

①モーダルにmargin:autoがかかっているとはいざ知らず、座標が合わなくて苦労した。
初期表示では画面中央に表示されてほしいから、ドラッグされたときに初めてマージンを0にすることにした。

②スマホで操作する際にPCほど精密な動作ができないだろうと思い、「モーダルが画面外にはみ出さない」というセーフティを設けてみた。

DragEventはスマホのブラウザが対応していないため、TouchEventを使用する。
PointerEventはマウスとタッチ両方に対応していると書いてあるけど、moveイベントがうまく取得できなかったため、タイプを判別する部分のみの採用になった。

Discussion