はじめに
モーダル要素の配置というのは親要素の位置関係とは無関係であると考えているのですが、どうにも「Vue モーダルウィンドウ」とか検索するとtemplateにmodal-componentを置くような日本語ページばかり見つかるんですよね。
イメージの話としてはそれで正しいのでしょうけれど、そのレベルで検索する自分みたいな人間にとっては(そのmodal-componentをscriptから書きこむにはどうしたらいいの?)で詰みます。
個人的に、以下の箇所以外にモーダルウィンドウ関連の情報を書きたくないです。
- ライブラリ読込部
- スクリプト上の呼び出し部
というわけで、親要素のtemplateやcssに変更を加えないで済むjsライブラリを書きました。
概要
今回の実装は主に以下の手順で実現されています。
- bodyの末尾に全面を覆うfixedでz-index最大値(2^32以上)な要素を追加
- 上記要素の子要素としてVueコンポーネント描画用のdivを適当な(要設定)コンテナdivに描画
- モーダル用Vueコンポーネントを動的に生成し、コンテナdivにmodal-componentを描画
メインコード
import { createVNode, render } from 'vue';
export default async function toModal(modal, props = {}, config = {}) {
// bodyの末尾にタグを追加し、そのタグの中にVue要素を追記する
const background = document.createElement("div");
const container = document.createElement("div");
background.style.position = "fixed";
// 現状、2^32以上はz-indexの最大値として扱われるらしいので、それなりに大きい値にセットしておく
background.style.zIndex = 2 ** 63;
// 画面全体に表示
background.style.width = "100vw";
background.style.height = "100vh";
background.style.top = "0px";
background.style.left = "0px";
// コンテナ描画エリアに関する設定(テキトー)
container.style.backgroundColor = "#ffffff";
container.style.boxShadow = '0 5px 15px 0 rgba(0, 0, 0, 0.25)';
container.style.maxWidth = config.width || '80%';
container.style.maxHeight = config.height || '80%';
container.style.marginLeft = config.marginLeft || 'auto';
container.style.marginRight = config.marginRight || 'auto';
container.style.marginTop = config.marginTop || '100px';
container.style.marginBottom = config.marginBottom || 'auto';
container.style.border = config.border || '1px';
container.style.borderStyle = config.borderStyle || 'solid';
container.style.borderRadius = config.borderRadius || '4px';
container.style.paddingTop = config.paddingTop || '20px';
container.style.paddingBottom = config.paddingBottom || '20px';
container.style.overflowY = 'auto';
container.addEventListener('click', (e) => {
e.stopPropagation();
});
document.body.appendChild(background);
background.appendChild(container);
return new Promise((resolve) => {
// モーダル要素の終了処理を追加
props.destroy = (val) => {
render(null, container);
vnode = undefined;
background.remove();
resolve(val);
};
// モーダル要素を生成
let vnode = createVNode(modal, props);
// バックグラウンドをクリック時にダイアログクローズを提案
if (config.closeOnClickOutside) {
background.addEventListener('click', () => {
if (window.confirm(config.closeMessage || 'モーダルウィンドウを閉じますか?')) {
render(null, container);
vnode = undefined;
background.remove();
resolve(undefined);
}
});
}
// モーダル要素を描画
render(vnode, container);
});
}
使用方法
// モーダルウィンドウ化したい自作Vue要素のスクリプト部に以下のような内容を追記
<script>
export default {
props: {
// toModalの終了処理を呼び出すdestroy関数をpropsとして用意
destroy: {
type: Function
}
},
methods: {
// 適当にtemplate内から呼び出される閉じる用関数を作成
close: function {
const retValue = 1;
// オプショナルチェーンを使えばモーダルとして使わない場合もOK
this.destroy?.(retValue);
}
}
}
</script>
// モーダルウィンドウを組み込みたい親要素のスクリプト部に以下のような内容を追記
<script>
import toModal from './toModal.js';
import modalComponent from './modalComponent.vue';
export default {
methods: {
dialog: async function() {
// モーダル要素のprops
const modalProps = {};
const modalResult = await toModal(modalComponent, modalProps);
console.log(modalResult);
}
}
}
</script>
表示順序について
思い付きで簡単にしか実験していないので間違ってる可能性もありますが、兄弟要素は弟の方が、親子要素は子の方が前に出るようになっているはずです。
なので、bodyの末尾にappendChildしたz-indexが最大のdiv要素の子要素としてモーダルウィンドウを設定すればとくに考えるまでもなく最前面に出る、という目論見です。
この辺の前提が崩れると考え直しが必要でしょうが、今のところ良好に動作しています。
ちょっと前に書いた、D&Dとクリックの共存みたいな事をモーダルウィンドウ上でしたくなったらどうしましょうかね・・。
その時は素直に親子パターンで書いてなんとかなるように祈ります。
おわりに
Googleで検索するよりもgithubでそれらしいものを探したりChatGPTに相談する方が早かったかもなと思う事もあるけれど、今回はChatGPTに上手く相談できなかった。
githubは日本語じゃないと分かんないからよく分かんない。
2023/4/16修正
toModal.jsの中身がmixin使ってた頃の構成になってたので修正。