(function () { var navLock = false; var lastQuoteLinkBlock = true; $(document).ready(function () { // Изменение рейтинга комментария (с учётом форматирования) function setComVote(cell, rating) { if (rating > 0) cell .removeClass("con") .addClass("pro") .html("+" + rating); else if (rating < 0) cell .removeClass("pro") .addClass("con") .html("–" + parseInt(-rating)); else cell.removeClass("pro con").html(0); } // Голосование за комментарии $(document) .on("click", ".w-btn", function () { var vote = $(this).attr("vote"); if (vote != 0 && vote != 1) return false; var voted = $(this).is(".voted"); $(this).toggleClass("voted"); var diff = (vote == 1 && !voted) || (vote == 0 && voted) ? 1 : -1; var otherButton = $(this).siblings(".w-btn"); var votedOther = otherButton.is(".voted"); if (votedOther) { otherButton.removeClass("voted"); diff *= 2; } var cell = $(this).siblings(".w-rating"); var rating = parseInt( cell.is(".con") ? -cell.html().substring(1) : cell.html() ); setComVote(cell, rating + diff); var cell_ext = $(this).siblings(".w-rating-ext"); cell_ext.addClass("active-locked"); var pro = $(".pro", cell_ext); var con = $(".con", cell_ext); if (vote == 1 || (vote == 0 && votedOther)) pro.html( "+" + (parseInt(pro.text().substr(1)) + (vote == 1 && !voted ? 1 : -1)) ); if (vote == 0 || (vote == 1 && votedOther)) con.html( "–" + (parseInt(con.text().substr(1)) + (vote == 0 && !voted ? 1 : -1)) ); var wvote = $(this).closest(".wvote"); setTimeout(function () { $(".w-btn", wvote).removeClass("active"); }, 200); setTimeout(function () { cell_ext.removeClass("active active-locked"); }, 1000); $.getJSON( "/api/photo/comment/rate", { action: "vote-comment", wid: wvote.attr("wid"), vote: vote }, function (data) { if (data && !data[3]) { $('.w-btn[vote="1"]', wvote)[ data[0][1] ? "addClass" : "removeClass" ]("voted"); $('.w-btn[vote="0"]', wvote)[ data[0][0] ? "addClass" : "removeClass" ]("voted"); pro.html("+" + data[1][1]); con.html("" + data[1][0]); setComVote(cell, data[2]); } else if (data[3]) alert(data[3]); } ).fail(function (jx) { if (jx.responseText != "") alert(jx.responseText); }); return false; }) // Отображение кнопок .on("mouseenter mouseleave", '.w-btn[vote="1"]', function () { $(this).toggleClass("s2 s12"); }) .on("mouseenter mouseleave", '.w-btn[vote="0"]', function () { $(this).toggleClass("s5 s15"); }) .on("mouseenter touchstart", ".wvote", function () { $(".w-btn, .w-rating-ext", this).addClass("active"); }) .on("mouseleave", ".wvote", function () { $(".w-btn, .w-rating-ext", this).removeClass("active"); }) .on("touchstart", function (e) { if ( !$(e.target).is(".wvote") && $(e.target).closest(".wvote").length == 0 ) $(".w-btn, .w-rating-ext").removeClass("active"); }); // Подсветка комментария, если дана ссылка на комментарий var anchorTestReg = /#(\d+)$/; var arr = anchorTestReg.exec(window.location.href); if (arr != null) $('.comment[wid="' + arr[1] + '"]').addClass("s2"); // Ссылка на комментарий $(".cmLink").on("click", function () { var comment = $(this).closest(".comment"); comment.siblings().removeClass("s2"); comment.addClass("s2"); }); // Удаление комментария $(".delLink").on("click", function () { return confirm(_text["P_DEL_CONF"]); }); // Цитирование $(".quoteLink").on("click", function () { var comment = $(this).closest(".comment"), mText, mTextArray; var selection = window.getSelection(); var selectedText = selection.toString(); var quotedText = selectedText == "" ? $(".message-text", comment).text() : selectedText; var msg = ""; if (selectedText == "" && comment.next(".comment").length == 0) msg = _text["P_QUOTE_MSG"]; else if (quotedText.length > 600) msg = _text["P_QUOTE_LEN"]; if (msg != "") { if ($(".no-quote-last", comment).length == 0) comment.append('
' + msg + "
"); if (lastQuoteLinkBlock) { lastQuoteLinkBlock = false; return false; } } else $(".no-quote-last").remove(); if (selectedText == "") mText = $(".message-text", comment).html(); else mText = $("
") .append(selection.getRangeAt(0).cloneContents()) .html(); mText = mText .replace(/<\/?i[^>]*>/gi, "") .replace(/<\/?u>/gi, "") .replace(/<\/?span[^>]*>/gi, "") .replace(/<\/?div[^>]*>/gi, ""); mText = mText .replace(new RegExp('[^>]*', "ig"), ""); mText = mText .replace(/
]+)>)/gi, "") .replace(/\[br\]/gi, "") .replace(/</gi, "<") .replace(/"e;/gi, '"') .replace(/&/gi, "&"); mTextArray = mText.split(/\s*/i); var mText2 = ""; for (var i = 0; i < mTextArray.length; ++i) mText2 += "> " + mTextArray[i] + "\n"; var txtField = $("#wtext"); if (txtField.length) { var messageText = txtField.val(); var insertText = (messageText == "" ? "" : "\n") + _text["P_QUOTE_TXT"] + " (" + $(".message_author", comment).text() + ", " + $(".message_date", comment).text() + "):\n" + mText2 + "\n"; txtField.val(messageText + insertText); txtField[0].focus(); } return false; }); // Отправка комментария // Окно ввода комментария $("#wtext") .on("keypress", function (e) { if ((e.which == 10 || e.which == 13) && e.ctrlKey) $("#f1").submit(); }) .on("focus", function () { navLock = true; saveSelection(); }) .on("blur", function () { navLock = false; }) .on("blur", function () { navLock = false; }); }); let lastSelection = null; let autocompleteType = null; // 'emoji' или 'mention' let currentMentions = []; // Сохраняем позицию курсора function saveSelection() { const sel = window.getSelection(); if (sel.rangeCount > 0) { lastSelection = sel.getRangeAt(0); } } // Восстанавливаем позицию курсора function restoreSelection() { if (lastSelection) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(lastSelection); } } // Вставка смайла function insertEmoji(emojiElement) { const editor = document.getElementById("wtext"); if (!editor) return; // Принудительно фокусируем редактор editor.focus(); // Создаем новый диапазон, если выделение потеряно let sel = window.getSelection(); if (!sel.rangeCount || !editor.contains(sel.anchorNode)) { const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); // Курсор в конец sel.removeAllRanges(); sel.addRange(range); } // Сохраняем текущее выделение saveSelection(); // Создаем элемент эмодзи const img = document.createElement("img"); img.className = "editor-emoji"; img.src = emojiElement.src; img.dataset.code = `[${emojiElement.dataset.code}]`; img.contentEditable = "false"; // Вставляем в сохраненную позицию if (lastSelection) { lastSelection.insertNode(img); // Обновляем позицию курсора const newRange = document.createRange(); newRange.setStartAfter(img); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); } else { editor.appendChild(img); } // Форсируем обновление состояния editor.dispatchEvent(new Event('input', { bubbles: true })); } // Обработчики событий const showPickerElement = document.getElementById("showPicker"); if (showPickerElement) { showPickerElement.addEventListener("click", function (e) { e.stopPropagation(); const picker = document.getElementById("picker"); const rect = this.getBoundingClientRect(); picker.style.top = `${rect.bottom + window.scrollY - 450}px`; picker.style.left = `${rect.left + window.scrollX}px`; picker.classList.toggle("active"); }); } document.querySelectorAll(".emoji-option").forEach((emoji) => { emoji.addEventListener("mousedown", function(e) { // Используем mousedown вместо click e.preventDefault(); e.stopPropagation(); // Фокусируем редактор перед вставкой const editor = document.getElementById("wtext"); if (editor) editor.focus(); insertEmoji(emoji); document.getElementById("picker").classList.remove("active"); }); }); document.addEventListener('selectionchange', () => { const editor = document.getElementById("wtext"); const sel = window.getSelection(); if (editor && sel.rangeCount > 0) { const range = sel.getRangeAt(0); if (editor.contains(range.commonAncestorContainer)) { saveSelection(); } } }); const picker = document.getElementById("picker"); const showPicker = document.getElementById("showPicker"); if (picker) { document.addEventListener("click", function(e) { const isClickInsidePicker = picker.contains(e.target); const isClickOnShowButton = showPicker?.contains(e.target); if (!isClickInsidePicker && !isClickOnShowButton) { picker.classList.remove("active"); } }); } // Обновленный JavaScript для работы с новым форматом document.addEventListener("click", function (e) { if (e.target.classList.contains("toggle-message")) { e.preventDefault(); const container = e.target.closest(".message-text"); const isExpanded = container.classList.contains("show-all"); container.classList.toggle("show-all", !isExpanded); e.target.textContent = isExpanded ? "показать больше" : "скрыть"; } }); // Обработка отправки формы const formElement = document.querySelector("form"); const editorElement = document.getElementById("wtext"); const hiddenContentElement = document.getElementById("hiddenContent"); if (formElement && editorElement && hiddenContentElement) { formElement.addEventListener("submit", function (e) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = editorElement.innerHTML; // Обработка смайлов tempDiv.querySelectorAll("img.editor-emoji").forEach((img) => { const textNode = document.createTextNode(img.dataset.code); img.replaceWith(textNode); }); // Обработка упоминаний tempDiv.querySelectorAll(".user-mention").forEach((mention) => { const textNode = document.createTextNode( `@[${mention.dataset.userId}:${mention.innerText.replace("@", "")}]` ); mention.replaceWith(textNode); }); hiddenContentElement.value = tempDiv.innerHTML; }); } let allSmileys = []; function removeFirstLast(str) { return str.slice(1, -1); } async function loadSmileys() { try { const response = await fetch("/api/emoji/load"); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); allSmileys = (data.data || []).map((smiley) => { let code = smiley.code .replace(/\\(.)/g, "$1") .replace(/^\[/, "") // Удалить первую [ если есть .replace(/\]$/, ""); // Удалить последнюю ] если есть code = `${code}`; // Обернуть снова в одну пару [] return { ...smiley, code: code, }; }); console.log( "Processed codes:", allSmileys.map((s) => s.code) ); return allSmileys; } catch (error) { console.error("Error loading smileys:", error); return []; } } async function initEditor() { const editor = document.getElementById("wtext"); if (!editor) return; await loadSmileys(); let html = editor.innerHTML; // Заменяем шорткоды с точным совпадением allSmileys.forEach(({ code, url }) => { // Удаляем одну [ слева и одну ] справа, если они есть const trimmed = code .replace(/^\[/, "") // удаляет ПЕРВУЮ [ .replace(/\]$/, ""); // удаляет ПОСЛЕДНЮЮ ] const cleanedCode = `${trimmed}`; // Добавляем одну пару скобок const escapedCode = cleanedCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(escapedCode, "g"); console.log(cleanedCode); html = html.replace( regex, `` ); }); editor.innerHTML = html; } // Обработчик клавиш const wtextElement = document.getElementById("wtext"); if (wtextElement) { // Обработчик Backspace для удаления смайлов wtextElement.addEventListener("keydown", function (e) { if (e.key === "Backspace") { const selection = window.getSelection(); if (selection && selection.isCollapsed) { const range = selection.getRangeAt(0); const node = range.startContainer; if (node.previousSibling?.classList?.contains("editor-emoji")) { node.previousSibling.remove(); e.preventDefault(); } } } }); // Обработчик вставки текста wtextElement.addEventListener("paste", function (e) { e.preventDefault(); const text = (e.clipboardData || window.clipboardData).getData( "text/plain" ); document.execCommand("insertText", false, text); }); } // Автодополнение // Автодополнение function setupAutocomplete() { const editor = document.getElementById("wtext"); let currentWord = ""; let startPos = 0; let selectedIndex = -1; let isAutocompleteOpen = false; const acContainer = document.createElement("div"); acContainer.className = "autocomplete"; document.body.appendChild(acContainer); // Обработчик клавиш function handleKeyDown(e) { if (!isAutocompleteOpen) return; const items = acContainer.querySelectorAll(".autocomplete-item"); if (items.length === 0) return; switch (e.key) { case "ArrowDown": e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, items.length - 1); updateSelection(); break; case "ArrowUp": e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, -1); updateSelection(); break; case "Enter": e.preventDefault(); if (selectedIndex >= 0) { insertSuggestion(items[selectedIndex]); } break; case "Escape": e.preventDefault(); closeAutocomplete(); break; } } function updateSelection() { const items = acContainer.querySelectorAll(".autocomplete-item"); items.forEach((item, index) => { item.classList.toggle("selected", index === selectedIndex); }); } function insertSuggestion(item) { const code = item.dataset.code; const sel = window.getSelection(); const range = sel.getRangeAt(0); range.setStart(range.startContainer, startPos); range.deleteContents(); const textNode = document.createTextNode(`[${code}]`); range.insertNode(textNode); range.setStartAfter(textNode); range.collapse(true); closeAutocomplete(); editor.focus(); } function closeAutocomplete() { acContainer.style.display = "none"; acContainer.style.top = "-9999px"; acContainer.style.left = "-9999px"; acContainer.innerHTML = ""; // Сброс состояний selectedIndex = -1; isAutocompleteOpen = false; autocompleteType = null; } // Предполагаем, что editor получен через getElementById if (editor) { let isAutocompleteOpen = false; // Добавляем инициализацию переменных let autocompleteType = ""; let startPos = 0; let currentWord = ""; // Объявляем функции для безопасного использования const handleKeyDown = (e) => { /* ... */ }; const showAutocomplete = (word, range) => { /* ... */ }; const closeAutocomplete = () => { /* ... */ }; // Обработчик нажатия клавиш editor.addEventListener("keydown", function (e) { const isAtSymbol = e.key === "@" || (e.key === "2" && e.shiftKey) || (e.key === "Quote" && e.shiftKey); if (e.key === ":" && !isAutocompleteOpen) { autocompleteType = "emoji"; isAutocompleteOpen = true; editor.addEventListener("keydown", handleKeyDown); } else if (isAtSymbol && !isAutocompleteOpen) { autocompleteType = "mention"; isAutocompleteOpen = true; editor.addEventListener("keydown", handleKeyDown); } }); // Обработчик ввода editor.addEventListener("input", function (e) { if (!isAutocompleteOpen) return; const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); const text = range.startContainer?.textContent || ""; const pos = range.startOffset; let i = pos - 1; const stopChar = autocompleteType === "emoji" ? ":" : "@"; while (i >= 0 && text[i] !== stopChar && text[i] !== " ") i--; if (text[i] === stopChar) { startPos = i; currentWord = text.slice(i + 1, pos).toLowerCase(); showAutocomplete(currentWord, range); } else { closeAutocomplete(); } }); } // Показать подсказки function showAutocomplete(query, range) { if (autocompleteType === "emoji") { const suggestions = allSmileys .filter((smiley) => smiley.code.toLowerCase().includes(query.toLowerCase()) ) .slice(0, 10); if (suggestions.length === 0) { closeAutocomplete(); return; } const rect = range.getBoundingClientRect(); acContainer.style.top = `${rect.bottom + window.scrollY}px`; acContainer.style.left = `${rect.left + window.scrollX}px`; acContainer.innerHTML = suggestions .map( (smiley, index) => `
${smiley.code}
` ) .join(""); acContainer.style.display = "block"; selectedIndex = 0; updateSelection(); } else if (autocompleteType === "mention") { fetch(`/api/users/search?q=${encodeURIComponent(query)}`) .then((response) => response.json()) .then((users) => { currentMentions = users; const rect = range.getBoundingClientRect(); acContainer.style.top = `${rect.bottom + window.scrollY}px`; acContainer.style.left = `${rect.left + window.scrollX}px`; acContainer.innerHTML = users .map( (user, index) => `
${user.username}
` ) .join(""); acContainer.style.display = "block"; selectedIndex = 0; updateSelection(); }); } } // Обновленный обработчик клика acContainer.addEventListener("mousedown", function (e) { // Используем mousedown вместо click const item = e.target.closest(".autocomplete-item"); if (item) { insertSuggestion(item); e.preventDefault(); } }); // Скрыть при клике вне document.addEventListener("click", function (e) { if (!e.target.closest(".autocomplete")) { acContainer.style.display = "none"; } }); function insertSuggestion(item) { if (autocompleteType === "emoji") { const code = item.dataset.code; const sel = window.getSelection(); const range = sel.getRangeAt(0); range.setStart(range.startContainer, startPos); range.deleteContents(); // Создаем элемент изображения вместо текстового узла const img = document.createElement("img"); img.className = "editor-emoji"; img.src = item.querySelector("img").src; img.dataset.code = `[${code}]`; img.contentEditable = "false"; range.insertNode(img); // Сдвигаем курсор после изображения const newRange = document.createRange(); newRange.setStartAfter(img); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); closeAutocomplete(); editor.focus(); } else if (autocompleteType === "mention") { const userId = item.dataset.userId; const username = item.dataset.username; const sel = window.getSelection(); if (sel.rangeCount === 0) return; const range = sel.getRangeAt(0).cloneRange(); range.setStart(range.startContainer, startPos); range.deleteContents(); // Создаем основной элемент упоминания const mentionSpan = document.createElement("span"); mentionSpan.className = "user-mention"; mentionSpan.dataset.userId = userId; mentionSpan.textContent = `@${username}`; mentionSpan.contentEditable = "false"; // Создаем пробел после упоминания const spaceSpan = document.createTextNode("\u00A0"); // Вставляем элементы const fragment = document.createDocumentFragment(); fragment.appendChild(mentionSpan); fragment.appendChild(spaceSpan); range.insertNode(fragment); // Обновляем позицию курсора const newRange = document.createRange(); newRange.setStartAfter(spaceSpan); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); // Обновляем редактор closeAutocomplete(); editor.focus(); } closeAutocomplete(); editor.focus(); window.addEventListener("scroll", () => closeAutocomplete(), true); } function closeAutocomplete(immediate = false) { if (immediate) { // Немедленное удаление из DOM acContainer.style.display = "none"; acContainer.innerHTML = ""; acContainer.style.top = "-9999px"; // Убираем за пределы видимости selectedIndex = -1; isAutocompleteOpen = false; autocompleteType = null; return; } // Анимация исчезновения (опционально) acContainer.classList.add("closing"); setTimeout(() => { acContainer.style.display = "none"; acContainer.innerHTML = ""; acContainer.classList.remove("closing"); selectedIndex = -1; isAutocompleteOpen = false; autocompleteType = null; }, 200); } document.addEventListener("mouseover", async function (e) { const mention = e.target.closest(".user-mention"); if (mention && !mention.dataset.loaded) { const userId = mention.dataset.userId; const response = await fetch(`/api/users/load/${userId}`); const userInfo = await response.json(); } }); function updateSelection() { const items = acContainer.querySelectorAll(".autocomplete-item"); items.forEach((item, index) => { item.classList.toggle("selected", index === selectedIndex); }); // Прокрутка к выбранному элементу if (selectedIndex >= 0 && items[selectedIndex]) { items[selectedIndex].scrollIntoView({ behavior: "auto", block: "nearest", }); } } } // Инициализация после загрузки window.addEventListener("DOMContentLoaded", setupAutocomplete); function setupImageValidation() { const editor = document.getElementById("wtext"); if (!editor) return; // Блокировка вставки через CTRL+V editor.addEventListener("paste", function (e) { const clipboardData = e.clipboardData || window.clipboardData; if (!clipboardData?.items) return; for (let i = 0; i < clipboardData.items.length; i++) { if (clipboardData.items[i].type.indexOf("image") !== -1) { e.preventDefault(); alert("Разрешена вставка только смайликов через пикер!"); return; } } }); // Блокировка drag-and-drop изображений editor.addEventListener("dragover", (e) => e.preventDefault()); editor.addEventListener("drop", function (e) { e.preventDefault(); if (e.dataTransfer?.files?.length > 0) { alert("Загрузка собственных изображений запрещена!"); } }); // Запрет ручного добавления изображений editor.addEventListener("DOMNodeInserted", function (e) { if ( e.target?.tagName === "IMG" && !e.target?.classList?.contains("editor-emoji") ) { e.target.remove(); alert("Разрешены только смайлики из пикера!"); } }); } // Объединенный обработчик загрузки document.addEventListener("DOMContentLoaded", async () => { // Инициализация редактора const editor = document.getElementById("wtext"); if (editor) { setupImageValidation(); await loadSmileys().catch(console.error); initEditor(); setupAutocomplete(); } else { console.warn("Editor element (#wtext) not found"); } }); const form = document.getElementById("f1"); if (!form) { console.error("Форма #f1 не найдена!"); return; } const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.name = "filebody"; // Устанавливаем имя filebody fileInput.style.display = "none"; form.appendChild(fileInput); // Добавляем input внутрь формы const button = document.getElementById("attachFile"); const fileList = document.getElementById("fileList"); button.addEventListener("click", function () { fileInput.click(); }); fileInput.addEventListener("change", function () { const file = fileInput.files[0]; if (!file) return; const maxSize = 100 * 1024 * 1024; // 100 MB const forbiddenExtensions = [ ".html", ".php", ".htm", ".exe", ".com", ".cmd", ".bash", ".sh", ]; const fileName = file.name.toLowerCase(); const fileSize = file.size; if (fileSize > maxSize) { alert("Файл превышает 100 МБ."); return; } if (forbiddenExtensions.some((ext) => fileName.endsWith(ext))) { alert("Расширение не поддерживается."); return; } const fileItem = document.createElement("div"); fileItem.setAttribute( "style", "border:solid 1px #bbb; width:max-content; font-size: 12px; padding:3px 10px 3px; margin-bottom:13px; background-color:#e2e2e2" ); fileItem.textContent = file.name; const removeBtn = document.createElement("a"); removeBtn.classList.add("compl"); removeBtn.setAttribute( "style", "display: inline-block; margin-left: 5px; color:#292929; cursor: pointer;" ); removeBtn.textContent = "✖"; removeBtn.addEventListener("click", function () { fileItem.remove(); fileInput.value = ""; }); fileItem.appendChild(removeBtn); fileList.appendChild(fileItem); }); })();