/// /// /// import { createNewPromise, OutsidePromise, toCSSText } from "./deps.ts"; import { alertStyle } from "./style.ts"; import { AnswerButton, buildInButtons, Button } from "./button.ts"; import { addEventListenerToDocument, removeAllEventListenerFromDocument, } from "./eventListener.ts"; export { buildInButtons }; export type { AnswerButton, Button }; const shadowDomId = "scrapbox-alert"; /** * アラートの挙動を指定するための型 */ export interface AlertMode { buttons: Button[]; /** * 決定時に優先して選択されるボタンの番号(既定は0) * * ここで設定した値は以下の場面で使用される * - 入力フォームでCtrl+Enterを入力したときに選択されるボタンの番号 */ priorityEnterButtonIndex?: number; /** * キャンセル時に優先して選択されるボタンの番号(省略した場合はキャンセル操作ができない) * * 領域外をクリックした時やEscキーを入力した時などに使用される */ priorityCancelButtonIndex?: number; } /** * 組み込みのAlertMode */ export const buildInAlertModes: { [K in string]: AlertMode } = { OK: { buttons: [buildInButtons.OK], priorityCancelButtonIndex: 0, }, OK_CANCEL: { buttons: [buildInButtons.OK, buildInButtons.CANCEL], priorityCancelButtonIndex: 1, }, YES_NO: { buttons: [buildInButtons.YES, buildInButtons.NO], }, YES_NO_CANCEL: { buttons: [buildInButtons.YES, buildInButtons.NO, buildInButtons.CANCEL], priorityCancelButtonIndex: 2, }, ENTER: { buttons: [buildInButtons.ENTER], priorityEnterButtonIndex: 0, }, ENTER_CANCEL: { buttons: [buildInButtons.ENTER, buildInButtons.CANCEL], priorityEnterButtonIndex: 0, priorityCancelButtonIndex: 1, }, }; /** * アラート内でユーザーが行ったことを格納する型 */ export interface AlertAnswer { button: AnswerButton; inputValue?: string; } /** * アラートを表示するよ! */ export async function scrapboxAlert( mode: AlertMode = buildInAlertModes.OK, title?: string, description?: string, defaultInputValue?: string, ): Promise { const { background, inputArea, buttonArea } = renderAlertBase( title, description, ); const input = document.createElement("textarea"); const promise = createNewPromise(); if (isNeedInputForm(mode.buttons)) { if (defaultInputValue) input.textContent = defaultInputValue; inputArea.append(input); } const buttons = createButtonElements(mode.buttons, promise, input); buttonArea.append( ...buttons, ); const priorityEnterButtonIndex = mode.priorityEnterButtonIndex ? mode.priorityEnterButtonIndex : 0; const enterButton = buttons[priorityEnterButtonIndex]; const cancelButton = mode.priorityCancelButtonIndex ? buttons[mode.priorityCancelButtonIndex] : undefined; input.addEventListener("keydown", (e) => { if ( e.key === "Enter" && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey ) { enterButton.click(); } }); background.onclick = () => { if (cancelButton) cancelButton.click(); }; if (cancelButton) { addEventListenerToDocument({ type: "keydown", listener: (e) => { if ( e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey ) { cancelButton.click(); } }, }); } return promise.promise; } /** * アラートにおける共通部分のDOMを実装し、制御に使用するDOMの参照を返します */ function renderAlertBase( title?: string, description?: string, ): { background: HTMLDivElement; inputArea: HTMLDivElement; buttonArea: HTMLDivElement; } { const shadowParent = document.createElement("div"); shadowParent.id = shadowDomId; document.body.append(shadowParent); const shadow = shadowParent.attachShadow({ mode: "open" }); const background = document.createElement("div"); background.id = "background"; const style = document.createElement("style"); style.textContent = toCSSText(alertStyle); /** キャンセルできるようにするための背景 */ const alertBG = document.createElement("div"); alertBG.className = "alert-bg"; const alertContainer = document.createElement("div"); alertContainer.className = "container"; const titleElm = document.createElement("p"); titleElm.className = "title"; titleElm.textContent = title ? title : ""; const descriptionElm = document.createElement("div"); descriptionElm.className = "description"; if (description) { const descriptionLines = description.split("\n"); for (const line of descriptionLines) { const lineElm = document.createElement("span"); lineElm.textContent = line; const br = document.createElement("br"); descriptionElm.append(lineElm, br); } } const inputArea = document.createElement("div"); inputArea.className = "input-area"; const buttonArea = document.createElement("div"); buttonArea.className = "button-area"; alertContainer.append(titleElm, descriptionElm, inputArea, buttonArea); background.append(alertBG, alertContainer, style); shadow.append(background); return { background: alertBG, inputArea: inputArea, buttonArea: buttonArea, }; } /** * アラートのDOMを削除する */ function removeAlert() { const shadowParent = document.getElementById(shadowDomId); if (shadowParent === null) return; removeAllEventListenerFromDocument(); shadowParent.remove(); } /** * ボタンを作るよ! * * onClickの中身とかもここで設定する */ function createButtonElements( buttons: Button[], promise: OutsidePromise, textarea?: HTMLTextAreaElement, ) { const buttonElms: HTMLButtonElement[] = []; for (const b of buttons) { const buttonElm = document.createElement("button"); buttonElm.textContent = b.label; if (b.className) buttonElm.classList.add(b.className); buttonElm.onclick = () => { function runOnClick(): AlertAnswer { if (b.useInputForm) { console.log(`textContent: ${textarea?.value}`); if (textarea?.value) { return b.onClick({ InputValue: textarea?.value }); } else { return b.onClick({ InputValue: "" }); } } else return b.onClick(undefined); } promise.resolve(runOnClick()); removeAlert(); }; buttonElms.push(buttonElm); } return buttonElms; } /** * InputFormを必要とするボタンが1つでも存在していたなら`true`を返す */ function isNeedInputForm(buttons: Button[]): boolean { for (const b of buttons) { if (b.useInputForm) return true; } return false; }