AtCoderにコードのテンプレート機能を追加します。
// ==UserScript==
// @name AtCoder Code Template
// @name:en AtCoder Code Template
// @namespace https://kiramku.f5.si/
// @version 1.1.0
// @description AtCoderにコードのテンプレート機能を追加します。
// @description:en We will add a code template feature to AtCoder.
// @author kirameku
// @icon https://atcoder.jp/favicon.ico
// @match https://atcoder.jp/contests/*/tasks/*
// @match https://atcoder.jp/contests/*/submit*
// @match https://atcoder.jp/contests/*/custom_test*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 初期状態のテンプレート
const defaultTemplates = [
{
name: "新規テンプレート",
code: ``
},
];
// --- データ保存・取得用のヘルパー ---
const getTemplates = () => {
const saved = GM_getValue("atcoder_templates", null);
let tpls = saved ? JSON.parse(saved) : defaultTemplates;
return tpls.length > 0 ? tpls : defaultTemplates;
};
const saveTemplates = (templates) => GM_setValue("atcoder_templates", JSON.stringify(templates));
const getSelectedIndex = () => GM_getValue("atcoder_selected_template_index", 0);
const saveSelectedIndex = (index) => GM_setValue("atcoder_selected_template_index", index);
const getAutoInsertTemplateName = () => GM_getValue("atcoder_auto_insert_template_name", "新規テンプレート");
const saveAutoInsertTemplateName = (name) => GM_setValue("atcoder_auto_insert_template_name", name);
const getAutoInsertConfig = () => GM_getValue("atcoder_auto_insert_enable", true);
const saveAutoInsertConfig = (enable) => GM_setValue("atcoder_auto_insert_enable", enable);
const getLastInsertedCode = () => GM_getValue("atcoder_last_inserted_code", "");
const saveLastInsertedCode = (code) => GM_setValue("atcoder_last_inserted_code", code);
const getCurrentTemplate = () => {
const templates = getTemplates();
const index = getSelectedIndex();
return templates[index] || templates[0] || { name: "", code: "" };
};
const getAutoInsertTemplateCode = () => {
const templates = getTemplates();
const targetName = getAutoInsertTemplateName();
const target = templates.find(t => t.name === targetName);
return target ? target.code : (templates[0] ? templates[0].code : "");
};
// エディタ貼り付けロジック
const setEditor = (s, force = false, retryCount = 0) => {
const toggleButton = document.querySelector("button.btn-toggle-editor");
const editor = document.querySelector("#plain-textarea");
if (toggleButton && editor) {
const currentVal = editor.value.trim();
const lastInserted = getLastInsertedCode().trim();
if (!force && currentVal !== "" && currentVal !== lastInserted) {
return true;
}
try {
toggleButton.click();
editor.value = s;
toggleButton.click();
editor.value = s;
editor.dispatchEvent(new Event('input', { bubbles: true }));
if (editor.value.trim() === s.trim()) {
if (!force) saveLastInsertedCode(s);
return true;
}
} catch (e) {
console.error("Template insertion error:", e);
}
}
if (retryCount < 10) {
setTimeout(() => {
setEditor(s, force, retryCount + 1);
}, 200);
}
return false;
};
// UIを設置する関数
const injectUI = () => {
if (document.getElementById("atcoder-template-btn-row")) return;
const mainEditor = document.getElementById("editor");
if (!mainEditor) return;
const rowContainer = document.createElement("div");
rowContainer.id = "atcoder-template-btn-row";
rowContainer.style.display = "block";
rowContainer.style.width = "100%";
rowContainer.style.marginBottom = "10px";
rowContainer.style.clear = "both";
rowContainer.style.position = "relative";
rowContainer.style.zIndex = "10";
const dropdownGroup = document.createElement("div");
dropdownGroup.className = "btn-group";
dropdownGroup.style.marginRight = "5px";
dropdownGroup.style.verticalAlign = "middle";
const mainDropBtn = document.createElement("button");
mainDropBtn.type = "button";
mainDropBtn.className = "btn btn-default btn-sm dropdown-toggle";
mainDropBtn.setAttribute("data-toggle", "dropdown");
const listIcon = document.createElement("span");
listIcon.className = "glyphicon glyphicon-list";
listIcon.setAttribute("aria-hidden", "true");
mainDropBtn.appendChild(listIcon);
const btnTextNode = document.createTextNode("");
mainDropBtn.appendChild(document.createTextNode(" "));
mainDropBtn.appendChild(btnTextNode);
const caret = document.createElement("span");
caret.className = "caret";
caret.style.marginLeft = "5px";
mainDropBtn.appendChild(caret);
const menuList = document.createElement("ul");
menuList.className = "dropdown-menu";
const updateDropdownMenu = () => {
menuList.innerHTML = "";
const templates = getTemplates();
const currentIndex = getSelectedIndex();
const currentTpl = templates[currentIndex] || templates[0] || { name: "テンプレなし" };
btnTextNode.nodeValue = currentTpl.name + " ";
templates.forEach((tpl, idx) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "javascript:void(0);";
a.innerText = tpl.name;
a.addEventListener("click", (e) => {
e.preventDefault();
saveSelectedIndex(idx);
updateDropdownMenu();
});
li.appendChild(a);
menuList.appendChild(li);
});
};
updateDropdownMenu();
dropdownGroup.appendChild(mainDropBtn);
dropdownGroup.appendChild(menuList);
const pasteBtn = document.createElement("button");
pasteBtn.id = "atcoder-template-paste-btn";
pasteBtn.type = "button";
pasteBtn.className = "btn btn-default btn-sm";
pasteBtn.style.marginRight = "5px";
const pasteIcon = document.createElement("span");
pasteIcon.className = "glyphicon glyphicon-paste";
pasteIcon.setAttribute("aria-hidden", "true");
pasteBtn.appendChild(pasteIcon);
pasteBtn.appendChild(document.createTextNode(" 貼り付け"));
pasteBtn.addEventListener("click", () => setEditor(getCurrentTemplate().code, true));
const configBtn = document.createElement("button");
configBtn.id = "atcoder-template-modal-btn";
configBtn.type = "button";
configBtn.className = "btn btn-default btn-sm";
const configIcon = document.createElement("span");
configIcon.className = "glyphicon glyphicon-cog";
configIcon.setAttribute("aria-hidden", "true");
configBtn.appendChild(configIcon);
configBtn.appendChild(document.createTextNode(" テンプレ設定"));
rowContainer.appendChild(dropdownGroup);
rowContainer.appendChild(pasteBtn);
rowContainer.appendChild(configBtn);
mainEditor.parentNode.insertBefore(rowContainer, mainEditor);
if (!document.getElementById("atcoder-template-modal-overlay")) {
const modalOverlay = document.createElement("div");
modalOverlay.id = "atcoder-template-modal-overlay";
modalOverlay.style.position = "fixed";
modalOverlay.style.top = "0";
modalOverlay.style.left = "0";
modalOverlay.style.width = "100%";
modalOverlay.style.height = "100%";
modalOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
modalOverlay.style.zIndex = "9999";
modalOverlay.style.display = "none";
modalOverlay.style.justifyContent = "center";
modalOverlay.style.alignItems = "center";
const modalBody = document.createElement("div");
modalBody.style.backgroundColor = "#fff";
modalBody.style.padding = "20px";
modalBody.style.borderRadius = "6px";
modalBody.style.width = "620px";
modalBody.style.maxWidth = "95%";
modalBody.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
const title = document.createElement("h4");
title.innerText = "テンプレート管理・設定";
title.style.marginTop = "0";
title.style.borderBottom = "1px solid #eee";
title.style.paddingBottom = "10px";
const configRow = document.createElement("div");
configRow.style.display = "flex";
configRow.style.alignItems = "center";
configRow.style.flexWrap = "wrap";
configRow.style.gap = "15px";
configRow.style.marginBottom = "15px";
const checkboxContainer = document.createElement("div");
checkboxContainer.style.display = "flex";
checkboxContainer.style.alignItems = "center";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = "auto-insert-checkbox";
checkbox.style.marginRight = "8px";
const checkboxLabel = document.createElement("label");
checkboxLabel.htmlFor = "auto-insert-checkbox";
checkboxLabel.innerText = "ページアクセス時に自動で貼り付ける";
checkboxLabel.style.marginBottom = "0";
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(checkboxLabel);
const autoSelectContainer = document.createElement("div");
autoSelectContainer.style.display = "flex";
autoSelectContainer.style.alignItems = "center";
autoSelectContainer.style.gap = "5px";
const autoSelectLabel = document.createElement("label");
autoSelectLabel.innerText = "自動貼り付けテンプレ:";
autoSelectLabel.style.marginBottom = "0";
autoSelectLabel.style.fontSize = "12px";
const autoDropGroup = document.createElement("div");
autoDropGroup.className = "btn-group";
const autoDropBtn = document.createElement("button");
autoDropBtn.type = "button";
autoDropBtn.className = "btn btn-default btn-xs dropdown-toggle";
autoDropBtn.setAttribute("data-toggle", "dropdown");
autoDropBtn.style.minWidth = "130px";
autoDropBtn.style.textAlign = "left";
autoDropBtn.style.display = "flex";
autoDropBtn.style.justifyContent = "space-between";
autoDropBtn.style.alignItems = "center";
const autoDropText = document.createTextNode("");
autoDropBtn.appendChild(autoDropText);
const autoCaret = document.createElement("span");
autoCaret.className = "caret";
autoDropBtn.appendChild(autoCaret);
const autoDropMenu = document.createElement("ul");
autoDropMenu.className = "dropdown-menu";
autoDropGroup.appendChild(autoDropBtn);
autoDropGroup.appendChild(autoDropMenu);
autoSelectContainer.appendChild(autoSelectLabel);
autoSelectContainer.appendChild(autoDropGroup);
configRow.appendChild(checkboxContainer);
configRow.appendChild(autoSelectContainer);
const mainFlex = document.createElement("div");
mainFlex.style.display = "flex";
mainFlex.style.gap = "15px";
const leftPane = document.createElement("div");
leftPane.style.width = "190px";
leftPane.style.display = "flex";
leftPane.style.flexDirection = "column";
const listLabel = document.createElement("label");
listLabel.innerText = "テンプレ一覧 (⋮⋮ で並び替え):";
leftPane.appendChild(listLabel);
const tplListDiv = document.createElement("div");
tplListDiv.style.width = "100%";
tplListDiv.style.height = "210px"; // ボタンを無くした分、縦幅を有効活用
tplListDiv.style.border = "1px solid #ccc";
tplListDiv.style.borderRadius = "4px";
tplListDiv.style.overflowY = "auto";
tplListDiv.style.backgroundColor = "#fff";
tplListDiv.style.marginBottom = "8px";
leftPane.appendChild(tplListDiv);
const ioBtnArea = document.createElement("div");
ioBtnArea.style.display = "flex";
ioBtnArea.style.gap = "5px";
const exportBtn = document.createElement("button");
exportBtn.type = "button";
exportBtn.className = "btn btn-default btn-xs";
exportBtn.style.flexGrow = "1";
const exportIcon = document.createElement("span");
exportIcon.className = "glyphicon glyphicon-export";
exportBtn.appendChild(exportIcon);
exportBtn.appendChild(document.createTextNode(" 書き出し"));
const importBtn = document.createElement("button");
importBtn.type = "button";
importBtn.className = "btn btn-default btn-xs";
importBtn.style.flexGrow = "1";
const importIcon = document.createElement("span");
importIcon.className = "glyphicon glyphicon-import";
importBtn.appendChild(importIcon);
importBtn.appendChild(document.createTextNode(" 読み込み"));
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json";
fileInput.style.display = "none";
ioBtnArea.appendChild(exportBtn);
ioBtnArea.appendChild(importBtn);
ioBtnArea.appendChild(fileInput);
leftPane.appendChild(ioBtnArea);
const rightPane = document.createElement("div");
rightPane.style.flexGrow = "1";
rightPane.style.display = "flex";
rightPane.style.flexDirection = "column";
const actionBtnArea = document.createElement("div");
actionBtnArea.style.display = "flex";
actionBtnArea.style.justifyContent = "flex-end";
actionBtnArea.style.gap = "5px";
actionBtnArea.style.marginBottom = "8px";
const addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.className = "btn btn-success btn-xs";
const addIcon = document.createElement("span");
addIcon.className = "glyphicon glyphicon-plus";
addBtn.appendChild(addIcon);
addBtn.appendChild(document.createTextNode(" 新規追加"));
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.className = "btn btn-danger btn-xs";
const delIcon = document.createElement("span");
delIcon.className = "glyphicon glyphicon-trash";
delBtn.appendChild(delIcon);
delBtn.appendChild(document.createTextNode(" 削除"));
actionBtnArea.appendChild(addBtn);
actionBtnArea.appendChild(delBtn);
const nameLabel = document.createElement("label");
nameLabel.innerText = "テンプレート名:";
const warningAlert = document.createElement("div");
warningAlert.id = "atcoder-template-name-warning";
warningAlert.style.color = "#a94442";
warningAlert.style.backgroundColor = "#f2dede";
warningAlert.style.border = "1px solid #ebccd1";
warningAlert.style.padding = "6px 10px";
warningAlert.style.borderRadius = "4px";
warningAlert.style.fontSize = "11px";
warningAlert.style.marginBottom = "5px";
warningAlert.style.display = "none";
const alertIcon = document.createElement("span");
alertIcon.className = "glyphicon glyphicon-exclamation-sign";
alertIcon.style.marginRight = "5px";
warningAlert.appendChild(alertIcon);
warningAlert.appendChild(document.createTextNode("既に同じ名前のテンプレートが存在します。"));
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.className = "form-control input-sm";
nameInput.style.marginBottom = "5px";
const codeLabel = document.createElement("label");
codeLabel.innerText = "コード内容:";
const codeTextarea = document.createElement("textarea");
codeTextarea.style.width = "100%";
codeTextarea.style.height = "185px";
codeTextarea.style.fontFamily = "monospace";
codeTextarea.style.fontSize = "12px";
codeTextarea.style.padding = "5px";
codeTextarea.style.boxSizing = "border-box";
rightPane.appendChild(actionBtnArea);
rightPane.appendChild(nameLabel);
rightPane.appendChild(nameInput);
rightPane.appendChild(warningAlert);
rightPane.appendChild(codeLabel);
rightPane.appendChild(codeTextarea);
mainFlex.appendChild(leftPane);
mainFlex.appendChild(rightPane);
const btnArea = document.createElement("div");
btnArea.style.textAlign = "right";
btnArea.style.marginTop = "15px";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "btn btn-default btn-sm";
cancelBtn.style.marginRight = "10px";
cancelBtn.innerText = "キャンセル";
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "btn btn-primary btn-sm";
const saveIcon = document.createElement("span");
saveIcon.className = "glyphicon glyphicon-save";
saveBtn.appendChild(saveIcon);
saveBtn.appendChild(document.createTextNode(" 設定を保存"));
btnArea.appendChild(cancelBtn);
btnArea.appendChild(saveBtn);
modalBody.appendChild(title);
modalBody.appendChild(configRow);
modalBody.appendChild(mainFlex);
modalBody.appendChild(btnArea);
modalOverlay.appendChild(modalOverlay.cloneNode(false));
modalOverlay.innerHTML = "";
modalOverlay.appendChild(modalBody);
document.body.appendChild(modalOverlay);
let editingTemplates = [];
let currentEditIdx = 0;
let selectedAutoName = "";
let dragSourceIdx = null; // ドラッグ中のインデックス
const checkGlobalDuplicateValidation = () => {
if (editingTemplates[currentEditIdx]) {
editingTemplates[currentEditIdx].name = nameInput.value;
}
const nameList = editingTemplates.map(t => t.name || "");
const hasAnyDuplicate = nameList.some((name, idx) => name !== "" && nameList.indexOf(name) !== idx);
const currentName = nameInput.value;
const isCurrentDuplicate = nameList.filter(name => name === currentName).length > 1;
if (isCurrentDuplicate && currentName !== "") {
warningAlert.style.display = "block";
} else {
warningAlert.style.display = "none";
}
if (hasAnyDuplicate) {
saveBtn.disabled = true;
saveBtn.style.opacity = "0.5";
} else {
saveBtn.disabled = false;
saveBtn.style.opacity = "1";
}
};
const refreshAutoDropdown = () => {
autoDropMenu.innerHTML = "";
autoDropText.nodeValue = selectedAutoName || "(選択なし) ";
editingTemplates.forEach(t => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "javascript:void(0);";
a.innerText = t.name;
a.addEventListener("click", (e) => {
e.preventDefault();
selectedAutoName = t.name;
refreshAutoDropdown();
});
li.appendChild(a);
autoDropMenu.appendChild(li);
});
};
// ★新規・修正箇所: ドラッグ&ドロップ対応のリスト生成
const refreshModalList = () => {
tplListDiv.innerHTML = "";
editingTemplates.forEach((t, i) => {
const item = document.createElement("div");
item.style.display = "flex";
item.style.alignItems = "center";
item.style.borderBottom = "1px solid #eee";
item.style.userSelect = "none";
item.setAttribute("data-index", i);
// ⋮⋮ ハンドル
const handle = document.createElement("div");
handle.innerText = "⋮⋮";
handle.style.padding = "6px 4px 6px 8px";
handle.style.cursor = "grab";
handle.style.color = "#999";
handle.style.fontWeight = "bold";
handle.style.fontSize = "14px";
// ハンドルを掴んだ時だけドラッグを有効化する
handle.addEventListener("mousedown", () => { item.draggable = true; });
handle.addEventListener("mouseup", () => { item.draggable = false; });
// テキスト表示エリア
const labelSpan = document.createElement("span");
labelSpan.innerText = t.name || "(無名のテンプレ)";
labelSpan.style.flexGrow = "1";
labelSpan.style.padding = "6px 8px 6px 4px";
labelSpan.style.cursor = "pointer";
labelSpan.style.fontSize = "13px";
labelSpan.style.overflow = "hidden";
labelSpan.style.textOverflow = "ellipsis";
labelSpan.style.whiteSpace = "nowrap";
if (i === currentEditIdx) {
item.style.backgroundColor = "#337ab7";
item.style.color = "#fff";
handle.style.color = "#fff";
} else {
item.style.backgroundColor = "#fff";
item.style.color = "#333";
item.addEventListener("mouseenter", () => { if(dragSourceIdx === null) item.style.backgroundColor = "#f5f5f5"; });
item.addEventListener("mouseleave", () => { if(dragSourceIdx === null) item.style.backgroundColor = "#fff"; });
}
labelSpan.addEventListener("click", () => {
if (editingTemplates[currentEditIdx]) {
editingTemplates[currentEditIdx].name = nameInput.value || "無名のテンプレ";
}
currentEditIdx = i;
refreshModalList();
});
item.appendChild(handle);
item.appendChild(labelSpan);
// --- HTML5 ドラッグ&ドロップイベント ---
item.addEventListener("dragstart", (e) => {
dragSourceIdx = i;
e.dataTransfer.effectAllowed = "move";
setTimeout(() => { item.style.opacity = "0.4"; }, 0);
});
item.addEventListener("dragend", () => {
item.style.opacity = "1";
item.draggable = false;
dragSourceIdx = null;
refreshModalList();
});
item.addEventListener("dragover", (e) => {
e.preventDefault();
});
item.addEventListener("dragenter", (e) => {
e.preventDefault();
if (dragSourceIdx !== null && dragSourceIdx !== i) {
// 編集途中の名前を入力フィールドから同期
if (editingTemplates[currentEditIdx]) {
editingTemplates[currentEditIdx].name = nameInput.value;
}
// データの入れ替え
const draggedData = editingTemplates[dragSourceIdx];
editingTemplates.splice(dragSourceIdx, 1);
editingTemplates.splice(i, 0, draggedData);
// 選択中のインデックスの追従追跡
if (currentEditIdx === dragSourceIdx) {
currentEditIdx = i;
} else if (dragSourceIdx < currentEditIdx && i >= currentEditIdx) {
currentEditIdx--;
} else if (dragSourceIdx > currentEditIdx && i <= currentEditIdx) {
currentEditIdx++;
}
dragSourceIdx = i;
refreshModalList();
}
});
tplListDiv.appendChild(item);
});
if (editingTemplates[currentEditIdx]) {
nameInput.value = editingTemplates[currentEditIdx].name;
codeTextarea.value = editingTemplates[currentEditIdx].code;
nameInput.disabled = false;
codeTextarea.disabled = false;
} else {
nameInput.value = "";
codeTextarea.value = "";
nameInput.disabled = true;
codeTextarea.disabled = true;
}
checkGlobalDuplicateValidation();
refreshAutoDropdown();
};
const handleNameInputChange = () => {
if (editingTemplates[currentEditIdx]) {
const oldName = editingTemplates[currentEditIdx].name;
const nextName = nameInput.value;
checkGlobalDuplicateValidation();
const activeItem = tplListDiv.children[currentEditIdx];
if (activeItem) {
const span = activeItem.querySelector("span");
if (span) span.innerText = nextName || "(無名のテンプレ)";
}
if (selectedAutoName === oldName) { selectedAutoName = nextName; }
refreshAutoDropdown();
}
};
nameInput.addEventListener("input", handleNameInputChange);
nameInput.addEventListener("keyup", handleNameInputChange);
nameInput.addEventListener("blur", handleNameInputChange);
nameInput.addEventListener("change", handleNameInputChange);
codeTextarea.addEventListener("input", () => {
if (editingTemplates[currentEditIdx]) {
editingTemplates[currentEditIdx].code = codeTextarea.value;
}
});
addBtn.addEventListener("click", () => {
let newBaseName = "新規テンプレート";
let finalName = newBaseName;
let counter = 1;
while (editingTemplates.some(t => t.name === finalName)) {
finalName = `${newBaseName} ${editingTemplates.length + counter}`;
counter++;
}
editingTemplates.push({
name: finalName,
code: "// ここにコードを記述"
});
currentEditIdx = editingTemplates.length - 1;
refreshModalList();
tplListDiv.scrollTop = tplListDiv.scrollHeight;
});
delBtn.addEventListener("click", () => {
if (editingTemplates.length <= 1) {
alert("最低でも1つのテンプレートは残す必要があります。");
return;
}
const deletedName = editingTemplates[currentEditIdx].name;
editingTemplates.splice(currentEditIdx, 1);
currentEditIdx = Math.max(0, currentEditIdx - 1);
if (selectedAutoName === deletedName) {
selectedAutoName = editingTemplates[0] ? editingTemplates[0].name : "";
}
refreshModalList();
});
exportBtn.addEventListener("click", () => {
const dataStr = JSON.stringify(editingTemplates, null, 2);
const blob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "atcoder_templates.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
importBtn.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const parsed = JSON.parse(event.target.result);
if (Array.isArray(parsed) && parsed.length > 0) {
if (confirm("読み込んだテンプレート一覧に差し替えますか?")) {
editingTemplates = parsed;
currentEditIdx = 0;
selectedAutoName = parsed[0].name;
refreshModalList();
}
}
} catch (err) { alert("失敗しました。"); }
fileInput.value = "";
};
reader.readAsText(file);
});
configBtn.addEventListener("click", () => {
checkbox.checked = getAutoInsertConfig();
editingTemplates = JSON.parse(JSON.stringify(getTemplates()));
currentEditIdx = Math.min(getSelectedIndex(), editingTemplates.length - 1);
if (currentEditIdx < 0) currentEditIdx = 0;
selectedAutoName = getAutoInsertTemplateName();
refreshModalList();
modalOverlay.style.display = "flex";
});
const closeModal = () => { modalOverlay.style.display = "none"; };
cancelBtn.addEventListener("click", closeModal);
modalOverlay.addEventListener("click", (e) => { if (e.target === modalOverlay) closeModal(); });
saveBtn.addEventListener("click", () => {
if (saveBtn.disabled) return;
if (editingTemplates[currentEditIdx]) {
editingTemplates[currentEditIdx].name = nameInput.value || "無名のテンプレ";
}
saveTemplates(editingTemplates);
saveAutoInsertConfig(checkbox.checked);
if (!editingTemplates.some(t => t.name === selectedAutoName)) {
selectedAutoName = editingTemplates[0] ? editingTemplates[0].name : "";
}
saveAutoInsertTemplateName(selectedAutoName);
let selIdx = getSelectedIndex();
if (selIdx >= editingTemplates.length) {
saveSelectedIndex(0);
}
updateDropdownMenu();
closeModal();
alert("テンプレート一覧を保存しました!");
});
}
};
let lastInsertedUrl = "";
const observer = new MutationObserver(() => {
const currentUrl = window.location.href;
const toggleButton = document.querySelector("button.btn-toggle-editor");
const editor = document.querySelector("#plain-textarea");
if (toggleButton && editor) {
injectUI();
if (lastInsertedUrl !== currentUrl) {
if (getAutoInsertConfig()) {
setTimeout(() => {
setEditor(getAutoInsertTemplateCode(), false);
}, 150);
}
lastInsertedUrl = currentUrl;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();