Введение
BB редактор в uCoz функционально рабочий, но по ощущениям устарел. При регулярной работе с материалами это быстро начинает мешать. Кнопки выглядят разрозненно, фокус может теряться при нажатии, выделение текста иногда сбивается, а вставка текста из Word или Google Docs почти всегда приносит в поле лишний мусор.
В итоге вместо спокойной работы с контентом приходится постоянно исправлять форматирование и следить, чтобы редактор не вел себя странно.
В этой статье разобрано практическое улучшение BB редактора uCoz, реализованное исключительно с помощью CSS и JavaScript:
- Без плагинов
- Без внешних библиотек
- Без вмешательства в ядро системы
Решение устанавливается точечно и аккуратно улучшает стандартный редактор, не ломая его логику и не создавая проблем при обновлениях.
Что делает это улучшение
Скрипт не заменяет редактор и не переписывает его логику. Он работает поверх стандартного BB+HTML редактора uCoz и решает конкретные проблемы:
- Приводит панель редактора к аккуратному и современному виду.
- Делает кнопки единообразными и предсказуемыми.
- Полностью устраняет потерю выделения текста при нажатии кнопок.
- Корректно заворачивает выделенный текст в BB теги.
- Добавляет недостающие кнопки форматирования.
- Подсвечивает активные теги, если курсор находится внутри них.
- Добавляет уведомления о действиях через аккуратные toast сообщения.
- Очищает вставку из буфера от типографского мусора.
- Позволяет очистить форматирование выделенного фрагмента.
- Поддерживает горячие клавиши для быстрого форматирования.
- Корректно работает в светлой и тёмной теме браузера.
Редактор остаётся стандартным uCoz, но пользоваться им становится заметно комфортнее.
Какие возможности добавляются
После установки в редакторе появляются дополнительные инструменты:
- Зачеркнутый текст.
- Надчеркнутый текст.
- Горизонтальная линия.
- Цитата с корректными переносами строк.
- Блок кода.
- Спойлер.
- Список с готовым шаблоном пунктов.
- Быстрая вставка ссылки.
- Очистка форматирования выделенного текста.
Также работают горячие клавиши:
- Ctrl B — жирный текст.
- Ctrl I — курсив.
- Ctrl U — подчёркивание.
- Ctrl Shift C — блок кода.
- Ctrl K — вставка ссылки.
Где и как устанавливать код в uCoz
Это принципиально важный момент. Код не нужно вставлять в глобальные шаблоны сайта. Он устанавливается только в шаблоны форм, где используется редактор материалов.
Правильный порядок установки:
- Открой Панель управления сайтом.
- Перейди в раздел Управление дизайном.
- Выбери модуль, для которого нужно улучшить редактор. Например Каталог статей или Новости.
- Открой шаблон Форма добавления и редактирования материала и вставь код в самый низ шаблона.
- Сохрани изменения.
Если редактор используется в нескольких модулях, код нужно добавить в каждый модуль отдельно, так как формы добавления и редактирования у uCoz независимые.
Полный рабочий код улучшенного BB+HTML редактора
Ниже приведён оригинальный код без каких-либо изменений.
Его необходимо вставлять целиком, без удаления и переписывания частей:
<style>
/* Панели управления */
.ucoz-editor-panel{
display:none;
background:#ffffff!important;
border:1px solid #e2e8f0!important;
border-radius:12px 12px 0 0!important;
padding:8px 10px!important;
flex-wrap:wrap!important;
gap:4px!important;
align-items:center!important;
margin-bottom:-1px!important;
position:relative;
z-index:100;
}
.ucoz-editor-panel[style*="display: block"],
.ucoz-editor-panel[style=""]{
display:flex!important;
}
/* Кнопки */
.ucoz-editor-panel .codeButtons,
.ucoz-editor-panel button.custom-bb{
background:#ffffff!important;
border:1px solid #e2e8f0!important;
border-radius:8px!important;
color:#475569!important;
height:30px!important;
min-width:32px;
padding:0 6px!important;
font-size:12px!important;
cursor:pointer;
display:inline-flex;
align-items:center;
justify-content:center;
transition:background .2s, border-color .2s, box-shadow .2s, transform .05s ease;
margin:0!important;
user-select:none;
line-height:1;
}
.ucoz-editor-panel .codeButtons:hover,
.ucoz-editor-panel button.custom-bb:hover{
background:#f8fafc!important;
border-color:#3b82f6!important;
}
.ucoz-editor-panel .codeButtons:active,
.ucoz-editor-panel button.custom-bb:active{
transform:translateY(1px);
}
.ucoz-editor-panel .codeButtons:focus-visible,
.ucoz-editor-panel button.custom-bb:focus-visible{
outline:none;
box-shadow:0 0 0 3px rgba(59,130,246,.25);
border-color:#3b82f6!important;
}
/* Активная кнопка */
.ucoz-editor-panel .codeButtons.is-active,
.ucoz-editor-panel button.custom-bb.is-active{
background:#eff6ff!important;
border-color:#3b82f6!important;
color:#1d4ed8!important;
}
/* Очистка мусора uCoz */
.ucoz-editor-panel br,
.ucoz-editor-panel span[id*="bc"] br{
display:none!important;
}
.ucoz-editor-panel span[id*="bc"]{
display:contents!important;
}
/* Поле */
textarea.manFl{
border-radius:0 0 12px 12px!important;
border:1px solid #e2e8f0!important;
padding:15px!important;
}
textarea.manFl:focus-visible{
outline:none;
box-shadow:0 0 0 3px rgba(59,130,246,.20);
border-color:#3b82f6!important;
}
/* Тост */
.ucoz-editor-toast{
position:fixed;
left:50%;
bottom:16px;
transform:translateX(-50%);
background:rgba(15,23,42,.92);
color:#fff;
padding:10px 12px;
border-radius:12px;
font-size:13px;
line-height:1.2;
box-shadow:0 10px 30px rgba(0,0,0,.25);
opacity:0;
pointer-events:none;
transition:opacity .2s, transform .2s;
z-index:999999;
}
.ucoz-editor-toast.is-show{
opacity:1;
transform:translateX(-50%) translateY(-6px);
}
/* Dark mode */
@media (prefers-color-scheme: dark){
.ucoz-editor-panel{
background:#0b1220!important;
border-color:#223047!important;
}
.ucoz-editor-panel .codeButtons,
.ucoz-editor-panel button.custom-bb{
background:#0b1220!important;
border-color:#223047!important;
color:#cbd5e1!important;
}
.ucoz-editor-panel .codeButtons:hover,
.ucoz-editor-panel button.custom-bb:hover{
background:#0f1a2e!important;
border-color:#60a5fa!important;
}
textarea.manFl{
background:#0b1220!important;
color:#e2e8f0!important;
border-color:#223047!important;
}
}
</style>
<script>
(function () {
const iconMap = {
b: "<b>B</b>",
i: "<i>I</i>",
u: "<u>U</u>",
s: "<s>S</s>",
o: '<span style="text-decoration:overline">O</span>',
hr: "—",
quote: "❞",
code: "‹/›",
spoiler: "👁️",
img: "🖼️",
list: "•≡",
hide: "🔒",
clear: "🧽",
http: "🔗"
};
function toast(msg){
let el = document.getElementById("ucozEditorToast");
if (!el){
el = document.createElement("div");
el.className = "ucoz-editor-toast";
el.id = "ucozEditorToast";
document.body.appendChild(el);
}
el.textContent = msg;
el.classList.add("is-show");
clearTimeout(el._t);
el._t = setTimeout(()=>el.classList.remove("is-show"), 1200);
}
function getActiveTextarea(){
const a = document.activeElement;
return (a && a.tagName === "TEXTAREA") ? a : null;
}
function getTargetTextarea(panel, targetId){
const active = getActiveTextarea();
if (active) return active;
const byId = document.getElementById(targetId);
if (byId && byId.tagName === "TEXTAREA") return byId;
const byName = document.querySelector('textarea[name="' + CSS.escape(targetId) + '"]');
if (byName) return byName;
const wrap = panel.closest(".ucoz-editor-wrap, .comm-form-box, form, .ucoz-editor") || panel.parentElement;
if (wrap){
const near = wrap.querySelector("textarea");
if (near) return near;
}
return null;
}
function getSel(area){
const val = area.value || "";
const selStart = typeof area.selectionStart === "number" ? area.selectionStart : val.length;
const selEnd = typeof area.selectionEnd === "number" ? area.selectionEnd : val.length;
return { val, selStart, selEnd, selectedText: val.substring(selStart, selEnd) };
}
function setSel(area, start, end){
try{ area.setSelectionRange(start, end); }catch(e){}
}
function wrapSelection(area, start, end, opts){
if (!area) return;
const o = opts || {};
const { val, selStart, selEnd, selectedText } = getSel(area);
const replacement = start + selectedText + end;
area.value = val.substring(0, selStart) + replacement + val.substring(selEnd);
area.focus();
if (selectedText.length){
const newStart = selStart + start.length;
const newEnd = newStart + selectedText.length;
setSel(area, newStart, newEnd);
} else {
const pos = selStart + start.length + (o.cursorOffset || 0);
setSel(area, pos, pos);
}
}
function insertSmart(tag, area){
const hasSelection = (getSel(area).selectedText || "").length > 0;
if (tag === "hr"){
const { val, selStart, selEnd } = getSel(area);
const ins = "[hr]";
area.value = val.substring(0, selStart) + ins + val.substring(selEnd);
area.focus();
setSel(area, selStart + ins.length, selStart + ins.length);
return;
}
if (tag === "list" && !hasSelection){
const tpl = "[list]\n[*]Пункт 1\n[*]Пункт 2\n[/list]";
const { val, selStart, selEnd } = getSel(area);
area.value = val.substring(0, selStart) + tpl + val.substring(selEnd);
area.focus();
const cursor = selStart + "[list]\n[*]".length;
setSel(area, cursor, cursor + "Пункт 1".length);
return;
}
if (tag === "code" && !hasSelection){
wrapSelection(area, "[code]\n", "\n[/code]", { cursorOffset: 0 });
return;
}
if (tag === "quote" && !hasSelection){
wrapSelection(area, "[quote]\n", "\n[/quote]", { cursorOffset: 0 });
return;
}
if (tag === "spoiler" && !hasSelection){
wrapSelection(area, "[spoiler]\n", "\n[/spoiler]", { cursorOffset: 0 });
return;
}
if (tag === "http"){
const { selectedText } = getSel(area);
const url = prompt("Ссылка");
if (!url) return;
if (selectedText){
wrapSelection(area, "[url=" + url + "]", "[/url]", { cursorOffset: 0 });
} else {
const title = prompt("Текст ссылки") || url;
const { val, selStart, selEnd } = getSel(area);
const ins = "[url=" + url + "]" + title + "[/url]";
area.value = val.substring(0, selStart) + ins + val.substring(selEnd);
area.focus();
const a = selStart + ("[url=" + url + "]").length;
setSel(area, a, a + title.length);
}
return;
}
wrapSelection(area, "[" + tag + "]", "[/" + tag + "]", { cursorOffset: 0 });
}
function cleanFormatting(area){
const { val, selStart, selEnd, selectedText } = getSel(area);
if (!selectedText){
toast("Нечего чистить");
return;
}
let t = selectedText;
t = t.replace(/\[\/?(b|i|u|s|o|quote|code|spoiler|hide|url)(=[^\]]+)?\]/gi, "");
t = t.replace(/\[hr\]/gi, "");
t = t.replace(/\[list\]|\[\/list\]|\[\*\]/gi, "");
t = t.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
area.value = val.substring(0, selStart) + t + val.substring(selEnd);
area.focus();
setSel(area, selStart, selStart + t.length);
toast("Форматирование очищено");
}
function isCursorInsideTag(area, tag){
const val = area.value || "";
const pos = typeof area.selectionStart === "number" ? area.selectionStart : val.length;
const open = "[" + tag + "]";
const close = "[/" + tag + "]";
const left = val.lastIndexOf(open, pos);
if (left === -1) return false;
const rightClose = val.indexOf(close, left + open.length);
if (rightClose === -1) return false;
return pos > left + open.length && pos <= rightClose;
}
function updateActiveButtons(panel, area){
if (!panel || !area) return;
const toCheck = ["b","i","u","s","o","quote","code","spoiler"];
toCheck.forEach(t=>{
const btn = panel.querySelector('[data-tag="' + t + '"]');
if (!btn) return;
const on = isCursorInsideTag(area, t);
if (on) btn.classList.add("is-active");
else btn.classList.remove("is-active");
});
}
function cleanPastedText(text){
if (!text) return text;
return text
.replace(/\u00A0/g, " ")
.replace(/[“”]/g, '"')
.replace(/[‘’]/g, "'")
.replace(/\u2013|\u2014/g, "-")
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
}
function bindTextarea(area, panel){
if (!area || area.dataset.ucozEditorBound) return;
area.dataset.ucozEditorBound = "1";
const onUpdate = () => updateActiveButtons(panel, area);
area.addEventListener("keyup", onUpdate, true);
area.addEventListener("mouseup", onUpdate, true);
area.addEventListener("focus", onUpdate, true);
area.addEventListener("paste", (e)=>{
try{
const clip = e.clipboardData && e.clipboardData.getData("text/plain");
if (!clip) return;
e.preventDefault();
const cleaned = cleanPastedText(clip);
const { val, selStart, selEnd } = getSel(area);
area.value = val.substring(0, selStart) + cleaned + val.substring(selEnd);
const newPos = selStart + cleaned.length;
area.focus();
setSel(area, newPos, newPos);
onUpdate();
toast("Вставка очищена");
} catch (err) {}
}, true);
}
function fire(panel, targetId, tag){
const area = getTargetTextarea(panel, targetId);
if (!area) return;
if (tag === "clear"){
cleanFormatting(area);
updateActiveButtons(panel, area);
return;
}
insertSmart(tag, area);
updateActiveButtons(panel, area);
}
function handleHotkeys(e, panel, targetId){
if (!(e.ctrlKey || e.metaKey)) return;
const k = (e.key || "").toLowerCase();
if (k === "b"){ e.preventDefault(); fire(panel, targetId, "b"); return; }
if (k === "i"){ e.preventDefault(); fire(panel, targetId, "i"); return; }
if (k === "u"){ e.preventDefault(); fire(panel, targetId, "u"); return; }
if (k === "k"){ e.preventDefault(); fire(panel, targetId, "http"); return; }
if (k === "c" && e.shiftKey){ e.preventDefault(); fire(panel, targetId, "code"); return; }
}
function process(panel){
if (panel.dataset.working) return;
panel.dataset.working = "true";
const targetId = panel.id && panel.id.includes("brief") ? "brief" : "message";
panel.querySelectorAll(".codeButtons").forEach((el)=>{
if (el.tagName === "INPUT" && el.type === "button"){
const btn = document.createElement("button");
btn.type = "button";
btn.className = el.className;
btn.title = el.title || "";
btn.setAttribute("aria-label", btn.title || "button");
btn.onmousedown = (e)=>{
e.preventDefault();
if (typeof el.onclick === "function"){
try { el.onclick(); } catch (err) {}
}
};
btn.innerHTML = iconMap[el.value] || el.value || el.innerHTML;
el.replaceWith(btn);
}
const html = (el.innerHTML || "").trim();
const val = (el.value || "").trim();
if (html === "···" || val === "···"){
if ((el.id && el.id.includes("cdl")) || el.name === "left") el.innerHTML = "◀";
if ((el.id && el.id.includes("cdc")) || el.name === "center") el.innerHTML = "◀▶";
if ((el.id && el.id.includes("cdr")) || el.name === "right") el.innerHTML = "▶";
}
});
const custom = [
{ t:"s", n:"Зачеркнутый" },
{ t:"o", n:"Надчеркнутый" },
{ t:"hr", n:"Линия" },
{ t:"quote", n:"Цитата" },
{ t:"code", n:"Код" },
{ t:"spoiler", n:"Спойлер" },
{ t:"list", n:"Список" },
{ t:"http", n:"Ссылка" },
{ t:"clear", n:"Очистить форматирование" }
];
const closeBtn = panel.querySelector(".codeCloseAll") || panel.lastElementChild;
custom.forEach((tag)=>{
if (!panel.querySelector('[data-tag="' + tag.t + '"]')){
const b = document.createElement("button");
b.type = "button";
b.className = "codeButtons custom-bb";
b.dataset.tag = tag.t;
b.title = tag.n;
b.setAttribute("aria-label", tag.n);
b.innerHTML = iconMap[tag.t] || tag.t;
b.onmousedown = (e)=>{
e.preventDefault();
fire(panel, targetId, tag.t);
};
if (closeBtn && closeBtn.parentNode === panel) panel.insertBefore(b, closeBtn);
else panel.appendChild(b);
}
});
const ta = getTargetTextarea(panel, targetId);
if (ta){
bindTextarea(ta, panel);
if (!ta.dataset.ucozEditorHotkeys){
ta.dataset.ucozEditorHotkeys = "1";
ta.addEventListener("keydown", (e)=>handleHotkeys(e, panel, targetId), true);
}
updateActiveButtons(panel, ta);
}
delete panel.dataset.working;
}
let rafScheduled = false;
const observer = new MutationObserver(()=>{
if (rafScheduled) return;
rafScheduled = true;
requestAnimationFrame(()=>{
rafScheduled = false;
document.querySelectorAll(".ucoz-editor-panel").forEach(process);
});
});
observer.observe(document.body, {
childList:true,
subtree:true,
attributes:true,
attributeFilter:["style","class"]
});
document.querySelectorAll(".ucoz-editor-panel").forEach(process);
})();
</script>
Как работает редактор после установки
После установки поведение редактора становится стабильным и предсказуемым:
- Выделенный текст корректно заворачивается в BB+HTML теги.
- Если текст не выделен, тег вставляется, а курсор ставится внутрь.
- Списки, цитаты и код вставляются с готовыми шаблонами.
- Вставка текста из буфера очищается автоматически.
- Активные кнопки подсвечиваются в зависимости от позиции курсора.
- Очистка форматирования работает только по выделенному фрагменту.
Заключение
Это решение не пытается заменить BB+HTML редактор uCoz или превратить его в визуальный комбайн. Оно аккуратно доводит стандартный редактор до удобного состояния, в котором можно спокойно писать и оформлять материалы без постоянной борьбы с форматированием.
- Минимум вмешательства.
- Максимум практической пользы.
- И ровно тот уровень комфорта, которого не хватает стандартному редактору.
Оцените полезность материала!
Лицензия: CC BY-SA 4.0
Автор: Юрий Герук
Комментарии