dialogタグを使ってモーダルウィンドウ2

前回の記事

https://qiita.com/noenture/items/ecffc95acd7690127336

色々覚えたので刷新

# 概要

- **HTMLDialogElement**を継承し、カスタム要素として定義
- 生成時に **header/main/footer タグ**を子要素として追加
- footerに OK/CANCEL ボタンを追加
- 背景の透過部分（**backdrop**）のクリックでdialogを閉じる（空文字を返す）
- open()メソッド（promise）で開き、**then**で**returnValue**を受け取る
- question()メソッドで header/main の内容の書き換え


# 設定
書き癖そのままなので、読みにくいかもしれません

```diff_javascript:
class MyModal extends HTMLDialogElement {
	#selectable;#header;#main;#cancel;
	#clicked(e){	/////dialogをクリックした時の処理
		if(e.target.tagName === 'BUTTON'){	/////ボタンをクリック
			if(this.#selectable){
				//////////ここの処理もうちょっと簡潔にならないものか
				for(const x of this.#main.querySelectorAll('input')){
					if(x.checked) return this.close(x.value)
				}	/////選択したラジオボタンのvalueを返す
				this.close('')
			}else this.close(e.target.name)	/////OKボタンは「ok」、CANCELボタンは空文字を返す
-		}else if(e.target.tagName === 'DIALOG'){
+		}else if(e.target === this){
			if(e.target.offsetHeight < e.offsetY
			 || e.target.offsetWidth < e.offsetX
			 || e.offsetX < 0
			 || e.offsetY < 0
			) this.close('')	/////backdropをクリックした時は空文字を返す
		}
	}
	#closed(resolve){	/////dialogが閉じた時の処理
		return function(e){
			resolve(this.returnValue)	/////resolveでreturnValueを返す
			this.removeEventListener('close', this.#closed(resolve).bind(this), false)
		}
	}
	#radioCode(questions,defIdx){
		/////選択肢（ラジオボタン）のマークアップを生成
		/////defIdxはラジオボタンの初期選択のインデックス番号
		let code = ``
		for(let i = 0
			,q = Object.entries(questions)
			,m = q.length
			,x = defIdx
			;i < m
			;i = (i+1)|0
		){
			code += `<p><label><input type="radio" name="qs" value="${q[i][0]}"
				${x === i ? ' autofocus checked' : ''}>${q[i][1]}</label></p>`
		}
		return code
	}
	constructor(){	/////dialog内部の要素を生成・配置
		super()
		this.#header = document.createElement('header')
		this.#main = document.createElement('main')
		this.#main.style.paddingRight = '2.5rem'
		const footer = document.createElement('footer')
		footer.style.textAlign = 'right'
		
		this.#cancel = document.createElement('button')
		this.#cancel.name = ''
		this.#cancel.textContent = 'CANCEL'
		const ok = document.createElement('button')
		ok.name = 'ok'
		ok.textContent = 'OK'
		
		footer.append(ok,this.#cancel)
		this.append(this.#header,this.#main,footer)
		this.addEventListener('click', this.#clicked.bind(this), false)
	}
	question(quest,select,defIdx){	/////質問を指定・書き換えるメソッド
		this.#header.textContent = quest	/////質問文を設定
		this.#selectable = Object.prototype.toString.call(select) === '[object Object]'
			/////第2引数が連想配列か否か
		this.#main.innerHTML = this.#selectable ? this.#radioCode(select,defIdx|0) : ''
			/////第3引数は初期選択のインデックス番号
		this.#cancel.hidden = this.#selectable && this.#main.innerHTML !== ''
			/////選択肢がある時にCANCELボタンを消す
		return this
	}
	open(){	/////モーダルウィンドウを開くメソッド
		return new Promise(resolve => {
			this.addEventListener('close', this.#closed(resolve).bind(this), false)
			this.showModal()
		})
	}
}

window.customElements.define('my-modal',MyModal,{extends: 'dialog'})
	/////定義
```

# 使い方

```javascript:
/////モーダルウィンドウ生成
const modal = document.createElement('dialog', {is: 'my-modal'})
.question('radio buttons',{	/////このメソッドはdialog自身が返されるので繋げてもOK
	top: '上る',	/////第1引数は質問文
	right: '右へ',	/////第2引数に連想配列を置くことでラジオボタンによる選択肢の設定が可能
	bottom: '下る',	/////keyはモーダルを閉じた後に返される文字列
	left: '左へ',	/////valueは選択肢に表示される文
},2)	/////第3引数は生成時に選択されているインデックス番号
modal.id = 'modalWindow'
modal.classList.add('bgWhite','txBlue','padHalfRem')
document.body.appendChild(modal)	/////どこかに配置しないとshowModal()でエラー出ます
```

```javascript:
const doSomething1 = () => {
	modal.open().then(res => {	/////open()メソッドはPromiseなのでthenで受ける
		switch(res){
			case 'top': return console.log('上へ行く処理')
			case 'right': return console.log('右へ行く処理')
			case 'bottom': return console.log('下へ行く処理')
			case 'left': return console.log('左へ行く処理')
		}
	})
}
doSomething1()
```

```javascript:
modal.question('ok or cancel?')
	/////もう一度question()を使うと質問内容を書き換える
	/////連想配列を置かないと、質問文とOK/CANCELボタンが現れる

const doSomething2 = () => {
	modal.open().then(res => {
		if(res === 'ok'){	/////OKボタンを押すとokが返る
			console.log('OK')
		}else if(res === ''){	/////CANCELボタンを押すと空文字が返る、rejectでは返さない
			console.log('CANCEL')
		}		
	})
}
doSomething2()

```
# 最後に

デザインはCSSでしましょう
dialog本体外の透明な部分を調整をしたい時は、以下のようなCSSでいけたと思う

```css:
dialog::backdrop {
	background-color: rgb(90,90,90,0.5);
}
```

# 追記

~~そういえば、空の連想配列が指定された時の対処してないままだった~~ => 対処した

概要を追加