AtCoder Code Template

AtCoderにコードのテンプレート機能を追加します。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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 });
})();