🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

bb-helper

Добавляет возможность сохранять шаблоны BB-кода и использовать их

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         bb-helper
// @namespace    https://shikimori.one
// @version      1.0
// @description  Добавляет возможность сохранять шаблоны BB-кода и использовать их
// @author       LifeH
// @match        *://shikimori.org/*
// @match        *://shikimori.one/*
// @match        *://shikimori.me/*
// @grant        none
// @license MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.6/Sortable.min.js
// ==/UserScript==

(() => {
  'use strict';

  const defaultTemplates = [
    {
      "id": "folder-spoilers",
      "name": "Спойлеры",
      "folder": true,
      "templates": [
        {
          "id": "spoiler-is-fullwidth",
          "name": "Спойлер (fullwidth)",
          "code": "[spoiler=Спойлер is-fullwidth]Скрытый текст[/spoiler]"
        },
        {
          "id": "spoiler-is-fullwidth-is-centered",
          "name": "Спойлер (full+centered)",
          "code": "[spoiler=Спойлер is-fullwidth is-centered]Скрытый текст[/spoiler]"
        },
        {
          "id": "spoiler-is-fullwidth-is-centered-duble",
          "name": "Двойной спойлер",
          "code": "[div=cc-3]\n[div=right c-column mb-2 mr-4]\n[spoiler_block is-fullwidth is-centered]Скрытый текст[/spoiler_block]\n[/div]\n[div=left c-column mb-2 mr-4]\n[spoiler_block is-fullwidth is-centered]Скрытый текст[/spoiler_block]\n[/div]\nтекст\n[/div]"
        },
        {
          "id": "spoiler-is-fullwidth-is-centered-left",
          "name": "Спойлер справа",
          "code": "[div=cc-3]\n[div=right c-column mb-2 mr-4]\n[spoiler_block is-fullwidth is-centered]Скрытый текст[/spoiler_block]\n[/div]\nтекст\n[/div]"
        },
        {
          "id": "spoiler-is-fullwidth-is-centered-right",
          "name": "Спойлер слева",
          "code": "[div=cc-3]\n[div=left c-column mb-2 mr-4]\n[spoiler_block is-fullwidth is-centered]Скрытый текст[/spoiler_block]\n                [/div]\nтекст\n                [/div]"
        }
      ]
    },
    {
      "id": "folder-subheadlines",
      "name": "Заголовки",
      "folder": true,
      "templates": [
        {
          "id": "headline1",
          "name": "Большой заголовок",
          "code": "[div=headline m20]Большой заголовок[/div]"
        },
        {
          "id": "midheadline",
          "name": "Средний заголовок",
          "code": "[div=midheadline m20]Средний заголовок[/div]"
        },
        {
          "id": "subheadline1",
          "name": "Малый заголовок",
          "code": "[div=subheadline m20]Малый заголовок[/div]"
        },
        {
          "id": "subheadline2",
          "name": "дефолтный цвет",
          "code": "[div=subheadline]дефолтный цвет[/div]"
        },
        {
          "id": "subheadlines-with-tag",
          "name": "Заголовок с тегом",
          "code": "[div=headline d-flex align-items-center justify-content-between p-1]\n  [span]Заголовок с тегом[/span]\n  [div=b-anime_status_tag ongoing right m-0]Тег[/div]\n  [/div]"
        },
        {
          "id": "subheadline3",
          "name": "серый",
          "code": "[div=subheadline gray]серый[/div]"
        },
        {
          "id": "subheadline4",
          "name": "синий",
          "code": "[div=subheadline blue]синий[/div]"
        },
        {
          "id": "subheadline5",
          "name": "пыльносиний",
          "code": "[div=subheadline powderblue]пыльносиний[/div]"
        },
        {
          "id": "subheadline6",
          "name": "небосиний",
          "code": "[div=subheadline skyblue]небосиний[/div]"
        },
        {
          "id": "subheadline7",
          "name": "фиолетовый",
          "code": "[div=subheadline purple]фиолетовый[/div]"
        },
        {
          "id": "subheadline8",
          "name": "зелёный",
          "code": "[div=subheadline green]зелёный[/div]"
        },
        {
          "id": "subheadline9",
          "name": "жёлтый",
          "code": "[div=subheadline yellow]жёлтый[/div]"
        },
        {
          "id": "subheadline10",
          "name": "оранжевый",
          "code": "[div=subheadline orange]оранжевый[/div]"
        },
        {
          "id": "subheadline11",
          "name": "розовый",
          "code": "[div=subheadline pink]розовый[/div]"
        },
        {
          "id": "subheadline12",
          "name": "маджентовый",
          "code": "[div=subheadline magenta]маджентовый[/div]"
        },
        {
          "id": "subheadline13",
          "name": "коричневый",
          "code": "[div=subheadline brown]коричневый[/div]"
        }
      ]
    },
    {
      "id": "folder-tabs",
      "name": "Табы",
      "folder": true,
      "templates": [
        {
          "id": "tab1",
          "name": "Пример 1",
          "code": "[div=to-process data-dynamic=tabs]\n  [div=b-js-link active data-tab-switch]Tab 1[/div]\n  [div=b-js-link data-tab-switch]Tab 2[/div]\n  [div data-tab]Content 1[/div]\n  [div=hidden data-tab]Content 2[/div]\n  [/div]"
        },
        {
          "id": "tab2",
          "name": "Пример 2",
          "code": "[div=to-process data-dynamic=tabs]\n  [div=b-button active data-tab-switch]Tab 1[/div]\n  [div=b-button data-tab-switch]Tab 2[/div]\n  [div data-tab]Content 1[/div]\n  [div=hidden data-tab]Content 2[/div]\n  [/div]"
        },
        {
          "id": "tab3",
          "name": "Пример 3",
          "code": "[div=to-process data-dynamic=tabs]\n  [div=b-link_button inline active data-tab-switch]Tab 1[/div]\n  [div=b-link_button inline data-tab-switch]Tab 2[/div]\n  [div data-tab]Content 1[/div]\n  [div=hidden data-tab]Content 2[/div]\n  [/div]"
        },
        {
          "id": "tab4",
          "name": "Пример 4 вертикальный",
          "code": "[div=d-flex to-process data-dynamic=tabs]\n   [div=d-flex flex-column flex-shrink-0 mr-4]\n    [div=b-link_button active data-tab-switch]Tab 1[/div]\n    [div=b-link_button data-tab-switch]Tab 2[/div]\n    [div=b-link_button data-tab-switch]Tab 3[/div]\n    [div=b-link_button data-tab-switch]Tab 4[/div]\n   [/div]\n   [div=p-2 flex-fill data-tab]Content 1[/div]\n   [div=p-2 flex-fill hidden data-tab]Content 2[/div]\n   [div=p-2 flex-fill hidden data-tab]Content 3[/div]\n   [div=p-2 flex-fill hidden data-tab]Content 4[/div]\n  [/div]"
        }
      ]
    },
    {
      "id": "folder-image-and-vid",
      "name": "Изображения ",
      "folder": true,
      "templates": [
        {
          "id": "img1",
          "name": "Абзац с большой картинкой (слева)",
          "code": "[div=cc-3]\n    [div=c-column mb-2 mr-4]\n    [center][img w=360]https://i.imgur.com/aGMILHR.jpg[/img][/center]\n    [/div]\n    Текст\n    [div=clearfix][/div]\n    [/div]"
        },
        {
          "id": "img2",
          "name": "Абзац с большой картинкой (справа)",
          "code": "[div=cc-3]\n[div=c-column right mb-2 ml-4 mr-0]\n[center][img w=360]https://i.imgur.com/aGMILHR.jpg[/img][/center]\n[/div]\nТекст\n[div=clearfix][/div]\n[/div]"
        },
        {
          "id": "img3",
          "name": "Колонка с цитатой-подписью к изображению(слева)",
          "code": "[center][div=left b-quote d-inline-block p-2 pr-3 pl-3 m-0]\n    [img width=360]https://i.imgur.com/aGMILHR.jpg[/img]\n    Подпись\n    [/div][/center]"
        },
        {
          "id": "img4",
          "name": "Колонка с цитатой-подписью к изображению(справа)",
          "code": "[center][div=right b-quote d-inline-block p-2 pr-3 pl-3 m-0]\n    [img width=360]https://i.imgur.com/aGMILHR.jpg[/img]\n    Подпись\n    [/div][/center]"
        },
        {
          "id": "vid2",
          "name": "Колонка с цитатой-подписью к видео(слева)",
          "code": "[center][div=lift b-quote d-inline-block p-2 pr-3 pl-3 m-0]\nhttps://youtu.be/BL0YK8jryK0\n«Trust in you» by [sweet ARMS]\n[/div][/center]"
        },
        {
          "id": "vid1",
          "name": "Колонка с цитатой-подписью к видео(справа)",
          "code": "[center][div=right b-quote d-inline-block p-2 pr-3 pl-3 m-0]\nhttps://youtu.be/BL0YK8jryK0\n«Trust in you» by [sweet ARMS]\n[/div][/center]"
        },
        {
          "id": "char1",
          "name": "Блок персонажа(слева)",
          "code": "[div=left mb-2 mr-4]\n[character=496][img w=120 no-zoom]https://shikimori.one/system/characters/preview/496.jpg[/img][br] Сиро Эмия [/character]\n[/div]"
        },
        {
          "id": "char2",
          "name": "Блок персонажа(справа)",
          "code": "[div=right mb-2 mr-4]\n[character=496][img w=120 no-zoom]https://shikimori.one/system/characters/preview/496.jpg[/img][br] Сиро Эмия [/character]\n[/div]"
        },
        {
          "id": "char3",
          "name": "Колонка с цитатой-подписью к паре персонажей(слева)",
          "code": "[center][div=left b-quote d-inline-block p-2 pr-3 pl-3 m-0][div=d-flex]\n[character=496][img no-zoom]https://shikimori.one/system/characters/preview/496.jpg[/img][br]Сиро Эмия[/character]\n[character=496][img no-zoom]https://shikimori.one/system/characters/preview/497.jpg[/img][br]Сэйбер[/character]\n[/div][/div][/center]"
        },
        {
          "id": "char4",
          "name": "Колонка с цитатой-подписью к паре персонажей(справа)",
          "code": "[center][div=right b-quote d-inline-block p-2 pr-3 pl-3 m-0][div=d-flex]\n[character=496][img no-zoom]https://shikimori.one/system/characters/preview/496.jpg[/img][br]Сиро Эмия[/character]\n[character=496][img no-zoom]https://shikimori.one/system/characters/preview/497.jpg[/img][br]Сэйбер[/character]\n[/div][/div][/center]"
        }
      ]
    },
    {
      "id": "разное-1742059718284",
      "name": "Разное",
      "folder": true,
      "templates": [
        {
          "id": "anime_status_tag-review-positive-1742060047874",
          "name": "anime_status_tag review-positive",
          "code": "[div=b-anime_status_tag review-positive]TEST[/div]"
        },
        {
          "id": "anime_status_tag-review-neutral-1742059741253",
          "name": "anime_status_tag review-neutral",
          "code": "[div=b-anime_status_tag review-neutral]TEST[/div]"
        },
        {
          "id": "anime_status_tag-review-negative-1742059863925",
          "name": "anime_status_tag review-negative",
          "code": "[div=b-anime_status_tag review-negative]TEST[/div]"
        },
        {
          "id": "anime_status_tag-collection-1742059810407",
          "name": "anime_status_tag collection",
          "code": "[div=b-anime_status_tag collection]TEST[/div]"
        },
        {
          "id": "b-anime_status_tag-news-1742059769851",
          "name": "b-anime_status_tag news",
          "code": "[div=b-anime_status_tag news]TEST[/div]"
        },
        {
          "id": "anime_status_tag-censored-1742059787398",
          "name": "anime_status_tag censored",
          "code": "[div=b-anime_status_tag censored]TEST[/div]"
        },
        {
          "id": "anime_status_tag-ongoing-1742059888390",
          "name": "anime_status_tag ongoing",
          "code": "[div=b-anime_status_tag ongoing]TEST[/div]"
        },
        {
          "id": "anime_status_tag-offtopic-1742059898041",
          "name": "anime_status_tag offtopic",
          "code": "[div=b-anime_status_tag offtopic]TEST[/div]"
        },
        {
          "id": "anime_status_tag-critique-1742059912010",
          "name": "anime_status_tag critique",
          "code": "[div=b-anime_status_tag critique]TEST[/div]"
        },
        {
          "id": "anime_status_tag-contest-1742059922183",
          "name": "anime_status_tag contest",
          "code": "[div=b-anime_status_tag contest]TEST[/div]"
        },
        {
          "id": "anime_status_tag-other-1742060078178",
          "name": "anime_status_tag other",
          "code": "[div=b-anime_status_tag other]TEST[/div]"
        },
        {
          "id": "footer_vote-1742060178456",
          "name": "footer_vote",
          "code": "[div=b-footer_vote]\n[div=star]\n[/div]\n[div=notice]Этот отзыв полезен?[/div]\n[/div]"
        },
        {
          "id": "hot_topics-v2-1742060198003",
          "name": "hot_topics-v2",
          "code": "[div=b-hot_topics-v2 center m-0]\n[div=subject]\n1\n[/div]\n[div=subject]\n2\n[/div]\n[div=subject]\n3\n[/div]\n[div=subject]\n4\n[/div]\n[div=subject]\n5\n[/div]\n[div=subject]\n6\n[/div]\n[/div]"
        },
        {
          "id": "link_button-dark-active-1742060231034",
          "name": "link_button dark active",
          "code": "[div=b-link_button dark active]TEST[/div]"
        },
        {
          "id": "link_button-dark-create-topic-1742060257800",
          "name": "link_button dark create-topic",
          "code": "[div=b-link_button dark create-topic]test[/div]"
        }
      ]
    }
  ];

const bbCodeStorage = new Map();
let csrfToken = '';

// ====== сторедж ======
function getTemplates() {
  let stored = localStorage.getItem('bbTemplates');
  if (stored) {
    try {
      return JSON.parse(stored);
    } catch (e) {
      return defaultTemplates;
    }
  }
  return defaultTemplates;
}
function saveTemplates(tpls) {
  localStorage.setItem('bbTemplates', JSON.stringify(tpls));
}
function generateId(name) {
  return name.toLowerCase().replace(/\s+/g, '-') + '-' + Date.now();
}

function initStorage() {
  bbCodeStorage.clear();
  const tpls = getTemplates();
  function addTemplates(arr) {
    arr.forEach(item => {
      if (item.folder) {
        if (Array.isArray(item.templates)) {
          addTemplates(item.templates);
        }
      } else {
        bbCodeStorage.set(item.id, item.code);
      }
    });
  }
  addTemplates(tpls);
}

// ====== превью ======
async function fetchPreview(combinedText, attempt = 1) {
  try {
    const response = await fetch("https://shikimori.one/api/shiki_editor/preview", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken
      },
      body: JSON.stringify({ text: combinedText })
    });
    if (!response.ok) {
      if (response.status === 429) {
        throw new Error('429');
      } else {
        throw new Error(`status: ${response.status}`);
      }
    }
    const data = await response.json();
    return data.html;
  } catch (error) {
    if (error.message === '429' && attempt < 5) {
      console.warn(`[BB-Helper] 429, ждем 5 сек (попытка ${attempt})...`);
      await new Promise(resolve => setTimeout(resolve, 5000));
      return fetchPreview(combinedText, attempt + 1);
    }
    throw error;
  }
}
// ====== мейн ======
const init = () => {
  csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
  createModal();
  addMenuButton();
  initStorage();
  setupHandlers();
};

//модалка
let currentModalFolder = null;
async function createModal() {
  const modal = document.createElement('div');
  modal.id = 'bb-helper-modal';
  modal.style.cssText = `
    display: none;
    position: fixed;
    top: 60px;
    right: 20px;
    background: #fff;
    z-index: 99999;
    box-shadow: 0 4px 20px rgba(0,0,0,0.1);
    border-radius: 8px;
    width: 400px;
    height: 300px;
    overflow: hidden;
    border: 1px solid #e0e0e0;
    font-family: system-ui;
    min-width: 300px;
    min-height: 200px;
  `;

  modal.innerHTML = `
    <div id="bb-modal-header" style="position: sticky; top: 0; background: #fff; z-index: 1000; display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; border-bottom: 1px solid #e0e0e0;">
      <button id="bb-modal-back" style="display:none; padding: 5px 10px; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">← Назад</button>
      <div id="bb-modal-breadcrumb" style="font-size: 14px;">Корневой уровень</div>
      <button id="bb-helper-close" style="background: none; border: none; font-size: 24px; cursor: pointer;">×</button>
    </div>
    <div id="bb-modal-content" style="padding: 10px; overflow: auto; height: calc(100% - 50px);">
      <div id="bb-templates-list"></div>
    </div>
    <!-- Ручка для изменения размеров (левый нижний угол) -->
    <div id="bb-modal-resizer-corner" style="
    position: absolute;
    bottom: 0;
    left: 0;
    width: 15px;
    height: 15px;
    cursor: nesw-resize;
    z-index: 101;"></div>
  `;

  document.body.appendChild(modal);

  document.getElementById('bb-helper-close').addEventListener('click', () => {
    modal.style.display = 'none';
    currentModalFolder = null;
  });

  document.getElementById('bb-modal-back').addEventListener('click', () => {
    currentModalFolder = null;
    loadTemplates();
  });

  const resizerCorner = document.getElementById('bb-modal-resizer-corner');
  let isResizing = false;
  let lastDownX = 0, lastDownY = 0;

  resizerCorner.addEventListener('mousedown', (e) => {
    isResizing = true;
    lastDownX = e.clientX;
    lastDownY = e.clientY;
    e.preventDefault();
  });

  document.addEventListener('mousemove', (e) => {
    if (!isResizing) return;
    const dx = lastDownX - e.clientX;
    const dy = e.clientY - lastDownY;
    modal.style.width = modal.offsetWidth + dx + 'px';
    modal.style.height = modal.offsetHeight + dy + 'px';
    lastDownX = e.clientX;
    lastDownY = e.clientY;
  });

  document.addEventListener('mouseup', () => {
    isResizing = false;
  });

  await loadTemplates();
}

// шаблоны
async function loadTemplates() {
  const list = document.getElementById('bb-templates-list');
  const breadcrumb = document.getElementById('bb-modal-breadcrumb');
  const backBtn = document.getElementById('bb-modal-back');
  let items;
  if (currentModalFolder === null) {
    items = getTemplates();
    breadcrumb.textContent = '/';
    backBtn.style.display = 'none';
  } else {
    items = currentModalFolder.templates || [];
    breadcrumb.textContent = currentModalFolder.name;
    backBtn.style.display = 'block';
  }

  // сбор в 1
  const flat = [];
  function collectTemplates(arr) {
    arr.forEach(item => {
      if (!item.folder) {
        flat.push(item);
      }
    });
  }
  collectTemplates(items);
  const previewMap = {};

  if (flat.length > 0) {
    const combinedText = flat
      .map(template => template.code.trim())
      .join("\n__SPLIT__\n");
    try {
      const combinedHtml = await fetchPreview(combinedText);
      const parts = combinedHtml.split("__SPLIT__");
      for (let i = 0; i < flat.length; i++) {
        previewMap[flat[i].id] = parts[i];
      }
    } catch (error) {
      console.error("[BB-Helper] Ошибка предпросмотра:", error);
      flat.forEach(template => {
        previewMap[template.id] = 'Ошибка загрузки превью';
      });
    }
  }

  list.innerHTML = '';
  items.forEach(item => {
    const div = document.createElement('div');
    div.style.padding = '8px';
    div.style.border = '1px solid #ccc';
    div.style.borderRadius = '4px';
    div.style.marginBottom = '6px';
    div.style.cursor = item.folder ? 'pointer' : 'grab';

    if (item.folder) {
      // Папка
      div.innerHTML = `<strong>📁 ${item.name}</strong>`;
      div.addEventListener('click', () => {
        currentModalFolder = item;
        loadTemplates();
      });
    } else {
      // Обычный шаблон
      div.innerHTML = `
        <div class="preview-container"
             style="min-width: 300px; overflow-x: auto; padding: 5px;">
          <div style="font-weight: 500; margin-bottom: 5px;">${item.name}</div>
          <div style="font-size:12px; color:#666;">${previewMap[item.id] || ''}</div>
        </div>
      `;
      div.addEventListener('click', () => {});
      div.draggable = true;
      div.addEventListener('dragstart', (e) => {
        const code = bbCodeStorage.get(item.id);
        e.dataTransfer.setData('text/plain', code);
        e.dataTransfer.effectAllowed = 'copy';
      });
    }

    list.appendChild(div);
  });
}

const addMenuButton = () => {
  const createButton = () => {
    const button = document.createElement('button');
    button.title = "Шаблоны";
    button.classList.add("icon", "icon-preview", "is-button");
    button.innerHTML = `<span style="display:flex;align-items:center;">
          <svg width="16" height="16" viewBox="0 0 24 24" style="fill:currentColor">
            <path d="M14 17H7V15H14V17M17 13H7V11H17V13M17 9H7V7H17V9M19 3H5C3.89 3 3 3.89 3 5V19C3 20.11 3.89 21 5 21H19C20.11 21 21 20.11 21 19V5C21 3.89 20.11 3 19 3Z"/>
          </svg>
          Шаблоны
        </span>`;

    button.style.cssText = `
      display: inline-flex;
      align-items: center;
      cursor: pointer;
      font-size: 13px;
      height: 19px;
      border: none;
      background: none;
      padding: 2px 4px;
    `;

    button.addEventListener('click', (e) => {
      e.preventDefault();
      const modal = document.getElementById('bb-helper-modal');
      if (!modal) return;
      if (modal.style.display === 'block') {
        modal.style.display = 'none';
        currentModalFolder = null;
      } else {
        currentModalFolder = null;
        modal.style.display = 'block';
        loadTemplates();
      }
    });

    return button;
  };

  const tryAddButton = () => {
    const menuGroup = document.querySelector('.menu_group-controls');
    if (menuGroup) {
      menuGroup.appendChild(createButton());
      return true;
    }
    return false;
  };

  if (tryAddButton()) return;

  const observer = new MutationObserver((mutations, obs) => {
    if (tryAddButton()) {
      obs.disconnect();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
};

//drop
const setupHandlers = () => {
  const editorContainer = document.querySelector('.editor-container');
  if (!editorContainer) return;
  const isCodeMode = editorContainer.classList.contains('is-source');
  if (isCodeMode) {
    const textarea = editorContainer.querySelector('textarea.ProseMirror');
    if (!textarea) return;
    editorContainer.addEventListener('dragover', handleDragOver);
    editorContainer.addEventListener('drop', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const code = e.dataTransfer.getData('text/plain');
      if (code) {
        insert(code, textarea, true);
      }
    });
  } else {
    const editor = editorContainer.querySelector('.ProseMirror');
    if (!editor) return;
    editor.addEventListener('dragover', handleDragOver);
    editor.addEventListener('drop', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const code = e.dataTransfer.getData('text/plain');
      if (code) {
        insert(code, editor, false);
      }
    });
  }
};

const handleDragOver = (e) => {
  e.preventDefault();
  e.dataTransfer.dropEffect = 'copy';
};

//вставка
const insert= (code, editor, isCodeMode) => {
  if (isCodeMode) {
    const start = editor.selectionStart;
    const end = editor.selectionEnd;
    const newValue = editor.value.slice(0, start) + code + editor.value.slice(end);
    editor.value = newValue;
    editor.selectionStart = editor.selectionEnd = start + code.length;
    editor.dispatchEvent(new Event('input', { bubbles: true }));
    editor.dispatchEvent(new Event('change', { bubbles: true }));
  } else {
    const selection = window.getSelection();
    if (!selection.rangeCount) return;
    const range = selection.getRangeAt(0);
    range.deleteContents();
    const frag = document.createDocumentFragment();
    code.split('\n').forEach((line, index) => {
      if (index > 0) {
        frag.appendChild(document.createElement('br'));
      }
      frag.appendChild(document.createTextNode(line));
    });
    range.insertNode(frag);
    range.collapse(false);
    selection.removeAllRanges();
    selection.addRange(range);
    editor.dispatchEvent(new InputEvent('input', { bubbles: true }));
  }
};

// ====== GUI ======
let isEditing = false;
let editingIndex = null;

function updateParentSelect() {
  const select = document.getElementById('tpl-parent');
  if (!select) return;
  select.innerHTML = '<option value="none">Без папки</option>';
  const tpls = getTemplates();
  tpls.forEach(item => {
    if (item.folder) {
      const option = document.createElement('option');
      option.value = item.id;
      option.textContent = item.name;
      select.appendChild(option);
    }
  });
  select.disabled = false;
}

function templateGUI() {
  const settingsBlock = document.querySelector('.block.edit-page.misc');
  if (!settingsBlock) return;
  if (document.querySelector('.bb-template-config')) return;

  let container = document.createElement('div');
  container.className = 'bb-template-config';
  container.style.padding = '20px';
  container.style.border = '1px solid #ccc';
  container.style.marginTop = '20px';
  container.style.background = '#f9f9f9';
  container.style.borderRadius = '8px';
  container.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  container.innerHTML = `
    <h3 style="margin-bottom: 20px; text-align: center;">Настройка шаблонов</h3>
    <div style="display: flex; flex-direction: column; gap: 10px;">
      <input type="text" id="tpl-name" placeholder="Название" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
      <textarea id="tpl-code" placeholder="BB-код шаблона (оставьте пустым для папки)" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; resize: vertical;"></textarea>
      <div style="display: flex; align-items: center; gap: 10px;">
        <div class="toggle-switch" style="position: relative; width: 40px; height: 20px;">
          <input type="checkbox" id="tpl-folder" style="opacity: 0; width: 100%; height: 100%; margin: 0; padding: 0; position: absolute; z-index: 2; cursor: pointer;">
          <span class="slider" style="
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 20px;
          "></span>
        </div>
        <label for="tpl-folder" style="font-size:14px;">Это папка</label>
      </div>
      <div style="display: flex; align-items: center; gap: 10px;">
        <label for="tpl-parent" style="font-size:14px;">Родительская папка:</label>
        <select id="tpl-parent" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
          <option value="none">Без папки</option>
        </select>
      </div>
      <button id="tpl-add" style="padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Добавить шаблон</button>
    </div>
    <div id="tpl-list" style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;"></div>
  `;
  settingsBlock.appendChild(container);

  let resetBtn = document.createElement('button');
  resetBtn.id = 'tpl-reset';
  resetBtn.textContent = 'Сбросить данные';
  resetBtn.style.cssText = `
    margin-top: 10px;
    padding: 10px;
    background-color: #F44336;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  `;
  container.appendChild(resetBtn);
  resetBtn.addEventListener('click', () => {
    if (confirm('Внимание: Все шаблоны будут сброшены на стандартные. Продолжить?')) {
      saveTemplates(defaultTemplates);
      updateTemplatesList();
      updateParentSelect();
      initStorage();
      clearTemplateForm();
      alert('Данные сброшены до стандартных.');
    }
  });

  let style = document.createElement('style');
  style.textContent = `
    .toggle-switch input:checked + .slider {
      background-color: #4CAF50;
    }
    .toggle-switch .slider:before {
      position: absolute;
      content: "";
      height: 16px;
      width: 16px;
      left: 2px;
      bottom: 2px;
      background-color: white;
      transition: .4s;
      border-radius: 50%;
    }
    .toggle-switch input:checked + .slider:before {
      transform: translateX(20px);
    }
  `;
  document.head.appendChild(style);

  document.getElementById('tpl-folder').addEventListener('change', (e) => {
    document.getElementById('tpl-parent').disabled = e.target.checked;
  });

  document.getElementById('tpl-add').addEventListener('click', () => {
    let name = document.getElementById('tpl-name').value.trim();
    let code = document.getElementById('tpl-code').value.trim();
    let isFolder = document.getElementById('tpl-folder').checked;
    let parentId = document.getElementById('tpl-parent').value;
    if (!name || (!isFolder && !code)) {
      alert('Пожалуйста, заполните поля.');
      return;
    }
    let allTpls = getTemplates();
    let newElement = isFolder
      ? { id: generateId(name), name, folder: true, templates: [] }
      : { id: generateId(name), name, code };

    if (isEditing && editingIndex !== null) {
      if (typeof editingIndex === 'object') {
        let currentFolder = allTpls[editingIndex.parent];
        let currentTemplate = currentFolder.templates[editingIndex.child];
        let updatedElement = { ...currentTemplate, name, code };
        if (parentId && parentId !== "none") {
          if (parentId === currentFolder.id) {
            currentFolder.templates[editingIndex.child] = updatedElement;
          } else {
            currentFolder.templates.splice(editingIndex.child, 1);
            let targetFolder = allTpls.find(item => item.folder && item.id === parentId);
            if (targetFolder) {
              targetFolder.templates.push(updatedElement);
            } else {
              allTpls.push(updatedElement);
            }
          }
        } else {
          currentFolder.templates.splice(editingIndex.child, 1);
          allTpls.push(updatedElement);
        }
      } else {
        if (allTpls[editingIndex].folder) {
          allTpls[editingIndex].name = name;
        } else {
          if (parentId && parentId !== "none") {
            let elem = allTpls.splice(editingIndex, 1)[0];
            newElement = { ...elem, name, code };
            let folder = allTpls.find(item => item.folder && item.id === parentId);
            if (folder) {
              folder.templates.push(newElement);
            } else {
              allTpls.push(newElement);
            }
          } else {
            allTpls[editingIndex] = newElement;
          }
        }
      }
      isEditing = false;
      editingIndex = null;
      document.getElementById('tpl-add').textContent = 'Добавить шаблон';
      document.getElementById('tpl-folder').disabled = false;
      document.getElementById('tpl-parent').disabled = false;
    } else {
      if (parentId && parentId !== "none") {
        let folder = allTpls.find(item => item.folder && item.id === parentId);
        if (folder) {
          folder.templates.push(newElement);
        } else {
          allTpls.push(newElement);
        }
      } else {
        allTpls.push(newElement);
      }
    }
    saveTemplates(allTpls);
    updateTemplatesList();
    clearTemplateForm();
    initStorage();
  });

  updateTemplatesList();
  updateParentSelect();
}

function clearTemplateForm() {
  document.getElementById('tpl-name').value = '';
  document.getElementById('tpl-code').value = '';
  document.getElementById('tpl-folder').checked = false;
  document.getElementById('tpl-folder').disabled = false;
  document.getElementById('tpl-parent').value = 'none';
  document.getElementById('tpl-parent').disabled = false;
  document.getElementById('tpl-add').textContent = 'Добавить шаблон';
  isEditing = false;
  editingIndex = null;
}

function updateTemplatesList() {
  let list = document.getElementById('tpl-list');
  list.innerHTML = '';
  let tpls = getTemplates();
  tpls.forEach((item, index) => {
    if (item.folder) {
      let folderDiv = document.createElement('div');
      folderDiv.className = 'tpl-folder';
      folderDiv.setAttribute('data-index', index);
      folderDiv.style.border = '1px solid #aaa';
      folderDiv.style.padding = '10px';
      folderDiv.style.marginBottom = '10px';
      folderDiv.style.background = '#eee';
      folderDiv.innerHTML = `
        <div class="folder-header" style="display:flex; justify-content: space-between; align-items: center; cursor: pointer;">
          <span class="folder-icon" style="font-size:18px; margin-right:5px;">📂</span>
          <strong>${item.name}</strong>
          <div>
            <button onclick="editTemplate(${index})" style="padding: 5px 10px; background-color: #FFC107; color: white; border: none; border-radius: 4px; cursor: pointer;">Редактировать</button>
            <button onclick="deleteTemplate(${index})" style="padding: 5px 10px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Удалить</button>
          </div>
        </div>
        <div class="folder-templates" style="margin-top: 10px; padding-left: 10px; border-left: 2px dashed #ccc;"></div>
      `;
      let inner = folderDiv.querySelector('.folder-templates');
      if (item.templates && item.templates.length) {
        item.templates.forEach((tpl, tplIndex) => {
          let tplDiv = document.createElement('div');
          tplDiv.className = 'tpl-item';
          tplDiv.style.display = 'flex';
          tplDiv.style.justifyContent = 'space-between';
          tplDiv.style.alignItems = 'center';
          tplDiv.style.padding = '5px';
          tplDiv.style.border = '1px solid #ccc';
          tplDiv.style.borderRadius = '4px';
          tplDiv.style.marginBottom = '5px';
          tplDiv.innerHTML = `
            <span>${tpl.name}</span>
            <div>
              <button onclick="editTemplate(${index}, ${tplIndex}, true)" style="padding: 3px 8px; background-color: #FFC107; color: white; border: none; border-radius: 4px; cursor: pointer;">Редактировать</button>
              <button onclick="deleteTemplate(${index}, ${tplIndex}, true)" style="padding: 3px 8px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Удалить</button>
            </div>
          `;
          inner.appendChild(tplDiv);
        });
        new Sortable(inner, {
          group: { name: 'templates', pull: false, put: false },
          animation: 150,
          onEnd: function (evt) {
            let allTpls = getTemplates();
            let folderId = evt.from.closest('.tpl-folder').getAttribute('data-index');
            let folder = allTpls[folderId];
            if (folder && folder.templates) {
              let movedItem = folder.templates.splice(evt.oldIndex, 1)[0];
              folder.templates.splice(evt.newIndex, 0, movedItem);
              allTpls[folderId] = folder;
              saveTemplates(allTpls);
              updateTemplatesList();
              initStorage();
            }
          }
        });
      } else {
        inner.innerHTML = `<div style="font-size:13px; color:#777;">(Пустая папка)</div>`;
      }
      list.appendChild(folderDiv);
    } else {
      let card = document.createElement('div');
      card.style.display = 'flex';
      card.style.justifyContent = 'space-between';
      card.style.alignItems = 'center';
      card.style.padding = '10px';
      card.style.border = '1px solid #ccc';
      card.style.borderRadius = '8px';
      card.style.background = '#fff';
      card.style.marginBottom = '10px';
      card.innerHTML = `
        <span>${item.name}</span>
        <div>
          <button onclick="editTemplate(${index})" style="padding: 5px 10px; background-color: #FFC107; color: white; border: none; border-radius: 4px; cursor: pointer;">Редактировать</button>
          <button onclick="deleteTemplate(${index})" style="padding: 5px 10px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Удалить</button>
        </div>
      `;
      list.appendChild(card);
    }
  });
  new Sortable(list, {
    group: { name: 'templates', pull: false, put: false },
    animation: 150,
    onEnd: function (evt) {
      let tpls = getTemplates();
      let movedItem = tpls.splice(evt.oldIndex, 1)[0];
      tpls.splice(evt.newIndex, 0, movedItem);
      saveTemplates(tpls);
      updateTemplatesList();
      initStorage();
    }
  });
  updateParentSelect();
}

// редактирование/удаление
function editTemplate(parentIndex, tplIndex, isNested) {
  let tpls = getTemplates();
  if (isNested) {
    let tpl = tpls[parentIndex].templates[tplIndex];
    document.getElementById('tpl-name').value = tpl.name;
    document.getElementById('tpl-code').value = tpl.code;
    document.getElementById('tpl-folder').checked = false;
    //в папке можно изменять папку (шаблон)
    document.getElementById('tpl-folder').disabled = true; // тип менять нельзя
    document.getElementById('tpl-parent').disabled = false;
    document.getElementById('tpl-parent').value = tpls[parentIndex].id;
    isEditing = true;
    editingIndex = { parent: parentIndex, child: tplIndex };
    document.getElementById('tpl-add').textContent = 'Сохранить изменения';
  } else {
    let item = tpls[parentIndex];
    document.getElementById('tpl-name').value = item.name;
    if (item.folder) {
      document.getElementById('tpl-code').value = '';
      document.getElementById('tpl-folder').checked = true;
      document.getElementById('tpl-folder').disabled = true;
      document.getElementById('tpl-parent').value = 'none';
      document.getElementById('tpl-parent').disabled = true;
    } else {
      document.getElementById('tpl-code').value = item.code;
      document.getElementById('tpl-folder').checked = false;
      document.getElementById('tpl-folder').disabled = true;
      document.getElementById('tpl-parent').value = 'none';
      document.getElementById('tpl-parent').disabled = false;
    }
    isEditing = true;
    editingIndex = parentIndex;
    document.getElementById('tpl-add').textContent = 'Сохранить изменения';
  }
}
function deleteTemplate(parentIndex, tplIndex, isNested) {
  if (confirm('Вы уверены, что хотите удалить этот шаблон?')) {
    let tpls = getTemplates();
    if (isNested) {
      tpls[parentIndex].templates.splice(tplIndex, 1);
    } else {
      tpls.splice(parentIndex, 1);
    }
    saveTemplates(tpls);
    updateTemplatesList();
    initStorage();
  }
}
window.editTemplate = editTemplate;
window.deleteTemplate = deleteTemplate;

function ready(fn) {
  document.addEventListener('page:load', fn);
  document.addEventListener('turbolinks:load', fn);
  if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}
ready(templateGUI);
ready(init);
})();