複数のリンクをまとめて置換するUserScriptの置換editorV1
実装メモ
preactは使わず、生のDOMで実装する
変換終了後、背景クリックかOKボタンで画面をdialogを閉じる
code:test.ts
import { waitForConvertOrder } from "./converter.ts";
console.log(result);
code:converter.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
export interface Link {
before: string;
after: string;
}
export type ConvertOrder = {
convert: false;
} | {
convert: true;
links: Link[];
};
export const waitForConvertOrder = (links: string[]): Promise<ConvertOrder> => {
const root = document.createElement("div");
const shadowRoot = root.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = .fa{font-weight:900;font-family:"Font Awesome 5 Free";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-spinner{animation:spin 2s infinite linear}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.fa-spinner:before{content:""}.kamon:before{font-family:"AppIcons";-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-variant:normal;font-weight:normal;text-decoration:none;text-transform:none}.kamon-check-circle:before{content:""}.modal{position:fixed;inset:0;z-index:1050;background-color:#000c;display:flex;flex-direction:column;align-items:center;row-gap:10px;padding:10px}.closed{display:none}.modal>*{color:var(--page-text-color, #4a4a4a);background-color:var(--dropdown-menu-bg, #fff);border:1px solid rgba(0,0,0,.2);border-radius:6px}@media (min-width: 768px){.modal{padding:30px 0;--item-width: 600px}}.result{padding:15px;width:calc(var(--item-width, 100%) - 30px);overflow-y:scroll}.controller{padding:5px;width:calc(var(--item-width, 100%) - 10px);display:flex;flex-direction:row-reverse;gap:.2em}.progress{width:100%}.progress>*{padding:0 2px}a{text-decoration:none;color:var(--page-link-color, #5e8af7)}a:hover{color:var(--page-link-hover-color, #2d67f5)}.copy{font-family:"Font Awesome 5 Free";cursor:pointer;background:unset;color:unset;border:unset}textarea{width:100%;resize:none;background:var(--page-bg,#fff);color:var(--page-text-color, #4a4a4a);}};
shadowRoot.append(style);
const background = document.createElement("div");
background.id = "background";
background.classList.add("modal");
background.setAttribute("role", "dialog");
background.insertAdjacentHTML("beforeend", `
<div class="result">
<textarea class="editor"></textarea>
<div class="footer">
<button class="cancel">cancel</button>
<button class="replace">replace</button>
</div>
</div>
`);
shadowRoot.append(background);
const editor = background.querySelector(".editor") as HTMLTextAreaElement;
editor.rows = links.length;
editor.value = links.join("\n");
const cancel = background.querySelector(".cancel") as HTMLButtonElement;
const confirm = background.querySelector(".replace") as HTMLButtonElement;
const promise = new Promise<ConvertOrder>((resolve) => {
cancel.addEventListener("click", () => {
resolve({ convert: false });
root.remove();
});
confirm.addEventListener("click", () => {
const newLines = editor.value.split("\n");
resolve({
convert: true,
links: links.flatMap(
(before, i) => {
// 空文字の場合と、変化がない場合は飛ばす
if (before === newLinesi || before === "" || !newLinesi) return []; return before, after: newLinesi ?? before }; }
),
});
root.remove();
});
});
document.body.append(root);
return promise;
};
code:app.css
@import "./font.css";
.modal {
position:fixed;
inset:0;
z-index:1050;
background-color:#000c;
display: flex;
flex-direction: column;
align-items: center;
row-gap: 10px;
padding: 10px;
}
.closed {
display: none;
}
.modal > * {
background-color: var(--dropdown-menu-bg, #fff); border: 1px solid rgba(0,0,0,.2);
border-radius: 6px;
}
@media (min-width: 768px) {
.modal {
padding: 30px 0;
--item-width: 600px;
}
}
.result {
padding: 15px;
width: calc(var(--item-width, 100%) - 30px);
overflow-y: scroll;
}