您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Добавляет возможность сохранять шаблоны BB-кода и использовать их
// ==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); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址