input-dialog-userscript@1.0.0
ここにはソースコードのみを置く
リソース
ソースコード
scrapboxAlert.ts(メインコード)
code:scrapboxAlert.ts
/// <reference no-default-lib="true" />
/// <reference lib="es2022" />
/// <reference lib="dom" />
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: {
priorityCancelButtonIndex: 0,
},
OK_CANCEL: {
priorityCancelButtonIndex: 1,
},
YES_NO: {
},
YES_NO_CANCEL: {
priorityCancelButtonIndex: 2,
},
ENTER: {
priorityEnterButtonIndex: 0,
},
};
/**
* アラート内でユーザーが行ったことを格納する型
*/
export interface AlertAnswer {
button: AnswerButton;
inputValue?: string;
}
/**
* アラートを表示するよ!
*/
export async function scrapboxAlert(
mode: AlertMode = buildInAlertModes.OK,
title?: string,
description?: string,
defaultInputValue?: string,
): Promise<AlertAnswer> {
const { background, inputArea, buttonArea } = renderAlertBase(
title,
description,
);
const input = document.createElement("textarea");
const promise = createNewPromise<AlertAnswer>();
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 cancelButton =
(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<AlertAnswer>,
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) {
if (textarea?.textContent) {
return b.onClick({ InputValue: textarea?.textContent });
} 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;
}
deps.ts(依存関係)
code:deps.ts
export {
createNewPromise,
export type {
OutsidePromise,
export {
toCSSText,
export type {
Style,
style.ts
code:style.ts
/// <reference no-default-lib="true" />
/// <reference lib="es2022" />
import { Style } from "./deps.ts";
export const alertStyle: Style = {
"#background": {
"position": "fixed",
"z-index": 2000,
"top": "40px",
"width": "100%",
"height": "100%",
"background-color": "hsl(0deg 0% 0% / 50%)",
},
".alert-bg": {
"position": "absolute",
"display": "block",
"width": "100%",
"height": "100%",
"background-color": "transparent",
},
".container": {
"position": "absolute",
"display": "flex",
"max-width": "50em",
"top": "60px",
"left": "1em",
"right": "1em",
"margin": "auto",
"padding": "1em 1.2em 1.2em",
"font-size": "15px",
"border": "1px solid black",
"border-radius": "10px",
"flex-direction": "column",
"background-color": "hsl(0deg 0% 100% / 85%)",
".title": {
"margin": "0 0 5px",
"font-size": "1.2em",
"font-weight": 900,
},
".description": {
"margin": "0 0 5px",
},
".input-area": {
"textarea": {
"width": "100%",
"height": "5em",
},
},
".button-area": {
"display": "flex",
"flex-direction": "row",
"justify-content": "space-evenly",
"button": {
"padding": "5px",
"min-width": "5em",
"border": "solid 2px hsl(0deg 0% 42%)",
"border-radius": "10px",
},
},
},
};
button.ts
code:button.ts
/// <reference no-default-lib="true" />
/// <reference lib="es2022" />
/// <reference lib="dom" />
import { AlertAnswer } from "./scrapboxAlert.ts";
/**
* 組み込みで実装してあるボタン
*/
export const buildInButtons: {
[K in typeof buildInButtonNamesnumber]: Button; } = {
OK: {
label: "OK",
useInputForm: false,
onClick: () => {
return { button: "OK" };
},
className: "button-OK",
},
CANCEL: {
label: "キャンセル",
useInputForm: false,
onClick: () => {
return { button: "CANCEL" };
},
className: "button-CANCEL",
},
YES: {
label: "はい",
useInputForm: false,
onClick: () => {
return { button: "YES" };
},
className: "button-YES",
},
NO: {
label: "いいえ",
useInputForm: false,
onClick: () => {
return { button: "NO" };
},
className: "button-NO",
},
ENTER: {
label: "決定",
useInputForm: true,
onClick: (form) => {
return {
button: "ENTER",
inputValue: form.InputValue,
};
},
className: "button-ENTER",
},
};
/**
* ボタンの型
*/
export type Button = {
label: string;
useInputForm: true;
onClick: (form: { InputValue?: string }) => AlertAnswer;
className?: string;
} | {
label: string;
useInputForm: false;
onClick: (form: undefined) => AlertAnswer;
className?: string;
};
/**
* アラート内でユーザーが選択したボタンの型
*
* 現状名前を入れているだけ
*/
export type AnswerButton = typeof buildInButtonNamesnumber | string; eventListener.ts
code:eventListener.ts
/// <reference no-default-lib="true" />
/// <reference lib="es2022" />
/// <reference lib="dom" />
// deno-lint-ignore no-explicit-any
const eventLisnersInDocument: EventListerForDocument<any>[] = [];
export interface EventListerForDocument<K extends keyof DocumentEventMap> {
type: K;
// deno-lint-ignore no-explicit-any
listener: (this: Document, ev: DocumentEventMapK) => any; }
/** documentへイベントリスナーを登録する */
export function addEventListenerToDocument<K extends keyof DocumentEventMap>(
arg: EventListerForDocument<K>,
) {
const { type, listener } = arg;
document.addEventListener(type, listener);
eventLisnersInDocument.push({ type: type, listener: listener });
}
/** addEventListenerToDocumentで登録したイベントリスナーを全て削除する */
export function removeAllEventListenerFromDocument() {
for (const el of eventLisnersInDocument) {
document.removeEventListener(el.type, el.listener);
}
}