From 2009416c35b2314c64a80c87676dbeab7887ccb8 Mon Sep 17 00:00:00 2001 From: themohooks <81331307+themohooks@users.noreply.github.com> Date: Sun, 25 May 2025 15:04:30 +0300 Subject: [PATCH] update js --- static/js/act.js | 1 - static/js/comments.js | 1042 ++++++++++++++++++++++++++++++++++++----- static/js/index.js | 55 ++- static/js/photo.js | 2 +- static/js/routing.js | 258 ++++++++++ 5 files changed, 1233 insertions(+), 125 deletions(-) create mode 100644 static/js/routing.js diff --git a/static/js/act.js b/static/js/act.js index 73471d6..582bf9d 100644 --- a/static/js/act.js +++ b/static/js/act.js @@ -198,4 +198,3 @@ const deleteComment = (postId, modalid) => { }); }); } - diff --git a/static/js/comments.js b/static/js/comments.js index be86b1f..b04a9c7 100644 --- a/static/js/comments.js +++ b/static/js/comments.js @@ -1,161 +1,965 @@ -var navLock = false; -var lastQuoteLinkBlock = true; +(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).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 })); + } - // Голосование за комментарии - $(document).on('click', '.w-btn', function() - { - var vote = $(this).attr('vote'); - if (vote != 0 && vote != 1) return false; + // Обработчики событий + const showPickerElement = document.getElementById("showPicker"); - var voted = $(this).is('.voted'); - $(this).toggleClass('voted'); + if (showPickerElement) { + showPickerElement.addEventListener("click", function (e) { + e.stopPropagation(); + const picker = document.getElementById("picker"); + const rect = this.getBoundingClientRect(); - var diff = (vote == 1 && !voted || vote == 0 && voted) ? 1 : -1; + picker.style.top = `${rect.bottom + window.scrollY - 450}px`; + picker.style.left = `${rect.left + window.scrollX}px`; + picker.classList.toggle("active"); + }); + } - var otherButton = $(this).siblings('.w-btn'); - var votedOther = otherButton.is('.voted'); + 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"); + }); + }); - if (votedOther) - { - otherButton.removeClass('voted'); - diff *= 2; - } + 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(); + } + } + }); - var cell = $(this).siblings('.w-rating'); - var rating = parseInt(cell.is('.con') ? -cell.html().substring(1) : cell.html()); +const picker = document.getElementById("picker"); +const showPicker = document.getElementById("showPicker"); - setComVote(cell, rating + diff); +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"); - var cell_ext = $(this).siblings('.w-rating-ext'); - cell_ext.addClass('active-locked'); + container.classList.toggle("show-all", !isExpanded); + e.target.textContent = isExpanded ? "показать больше" : "скрыть"; + } + }); - var pro = $('.pro', cell_ext); - var con = $('.con', cell_ext); + // Обработка отправки формы + const formElement = document.querySelector("form"); + const editorElement = document.getElementById("wtext"); + const hiddenContentElement = document.getElementById("hiddenContent"); - 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))); + 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); + }); - var wvote = $(this).closest('.wvote'); - setTimeout(function() { $('.w-btn', wvote).removeClass('active'); }, 200); - setTimeout(function() { cell_ext.removeClass('active active-locked'); }, 1000); + // Обработка упоминаний + tempDiv.querySelectorAll(".user-mention").forEach((mention) => { + const textNode = document.createTextNode( + `@[${mention.dataset.userId}:${mention.innerText.replace("@", "")}]` + ); + mention.replaceWith(textNode); + }); - $.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'); + hiddenContentElement.value = tempDiv.innerHTML; + }); + } + let allSmileys = []; + function removeFirstLast(str) { + return str.slice(1, -1); + } - pro.html('+' + data[1][1]); - con.html('' + data[1][0]); + async function loadSmileys() { + try { + const response = await fetch("/api/emoji/load"); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); - setComVote(cell, data[2]); - } - else if (data[3]) alert(data[3]); - }) - .fail(function(jx) { if (jx.responseText != '') alert(jx.responseText); }); + const data = await response.json(); - 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'); }); + allSmileys = (data.data || []).map((smiley) => { + let code = smiley.code + .replace(/\\(.)/g, "$1") + .replace(/^\[/, "") // Удалить первую [ если есть + .replace(/\]$/, ""); // Удалить последнюю ] если есть + code = `${code}`; // Обернуть снова в одну пару [] - // Подсветка комментария, если дана ссылка на комментарий - var anchorTestReg = /#(\d+)$/; - var arr = anchorTestReg.exec(window.location.href); - if (arr != null) $('.comment[wid="' + arr[1] + '"]').addClass('s2'); + return { + ...smiley, + code: code, + }; + }); + console.log( + "Processed codes:", + allSmileys.map((s) => s.code) + ); + return allSmileys; + } catch (error) { + console.error("Error loading smileys:", error); + return []; + } + } - // Ссылка на комментарий - $('.cmLink').on('click', function() - { - var comment = $(this).closest('.comment'); - comment.siblings().removeClass('s2'); - comment.addClass('s2'); - }); + async function initEditor() { + const editor = document.getElementById("wtext"); + if (!editor) return; + await loadSmileys(); - // Удаление комментария - $('.delLink').on('click', function() { return confirm(_text['P_DEL_CONF']); }); + let html = editor.innerHTML; + // Заменяем шорткоды с точным совпадением + allSmileys.forEach(({ code, url }) => { + // Удаляем одну [ слева и одну ] справа, если они есть + const trimmed = code + .replace(/^\[/, "") // удаляет ПЕРВУЮ [ + .replace(/\]$/, ""); // удаляет ПОСЛЕДНЮЮ ] - // Цитирование - $('.quoteLink').on('click', function() - { - var comment = $(this).closest('.comment'), mText, mTextArray; - var selection = window.getSelection(); + const cleanedCode = `${trimmed}`; // Добавляем одну пару скобок - var selectedText = selection.toString(); - var quotedText = (selectedText == '') ? $('.message-text', comment).text() : selectedText; - var msg = ''; + const escapedCode = cleanedCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(escapedCode, "g"); + console.log(cleanedCode); - if (selectedText == '' && comment.next('.comment').length == 0) msg = _text['P_QUOTE_MSG']; else - if (quotedText.length > 600) msg = _text['P_QUOTE_LEN']; + html = html.replace( + regex, + `` + ); + }); - if (msg != '') - { - if ($('.no-quote-last', comment).length == 0) comment.append('
' + msg + '
'); + editor.innerHTML = html; + } - if (lastQuoteLinkBlock) - { - lastQuoteLinkBlock = false; - return false; - } - } - else $('.no-quote-last').remove(); + // Обработчик клавиш + const wtextElement = document.getElementById("wtext"); - if (selectedText == '') - mText = $('.message-text', comment).html(); - else mText = $('
').append(selection.getRangeAt(0).cloneContents()).html(); + 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; - mText = mText.replace(/<\/?i[^>]*>/ig, '').replace(/<\/?u>/ig, '').replace(/<\/?span[^>]*>/ig, '').replace(/<\/?div[^>]*>/ig, ''); - mText = mText.replace(new RegExp('\[^>]*\<\/a\>', 'ig'), ''); - mText = mText.replace(/
]+)>)/ig, '').replace(/\[br\]/ig, '').replace(/</ig, '<').replace(/"e;/ig, '"').replace(/&/ig, '&'); - mTextArray = mText.split(/\s*/i); + if (node.previousSibling?.classList?.contains("editor-emoji")) { + node.previousSibling.remove(); + e.preventDefault(); + } + } + } + }); - var mText2 = ''; - for (var i = 0; i < mTextArray.length; ++i) - mText2 += '> ' + mTextArray[i] + '\n'; + // Обработчик вставки текста + wtextElement.addEventListener("paste", function (e) { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData( + "text/plain" + ); + document.execCommand("insertText", false, text); + }); + } - 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'; + // Автодополнение + // Автодополнение + function setupAutocomplete() { + const editor = document.getElementById("wtext"); + let currentWord = ""; + let startPos = 0; + let selectedIndex = -1; + let isAutocompleteOpen = false; - txtField.val(messageText + insertText); - txtField[0].focus(); - } + const acContainer = document.createElement("div"); + acContainer.className = "autocomplete"; + document.body.appendChild(acContainer); - return false; - }); + // Обработчик клавиш + 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); + }); + } - // Окно ввода комментария - $('#wtext').on('keypress', function(e) { if ((e.which == 10 || e.which == 13) && e.ctrlKey) $('#f1').submit(); }) - .on('focus', function() { navLock = true; }) - .on('blur', function() { navLock = false; }); + function insertSuggestion(item) { + const code = item.dataset.code; + const sel = window.getSelection(); + const range = sel.getRangeAt(0); -}); \ No newline at end of file + 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); + }); +})(); diff --git a/static/js/index.js b/static/js/index.js index dba393c..2ae4ec3 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,7 +1,6 @@ ar1 = new Image(); ar1.src = '/img/ar1.gif'; - $(document).ready(function() { $('.ix-country > a[href="#"]').on('click', function(e) @@ -119,11 +118,59 @@ function searchVehicles() -function AddPhotoToBlock(block, arr, prepend) -{ - block[prepend ? 'prepend' : 'append']('
'); +function AddPhotoToBlock(block, arr, prepend) { + const html = ` +
+
+ ${arr.place} +
${arr.date}
+
+ + ${arr.ccnt != 0 ? ` +
+
${arr.ccnt}
+
` : ''} +
+
`; + + block[prepend ? 'prepend' : 'append'](html); + + // Инициируем загрузку для нового элемента + lazyLoadSingleImage(block.find('.blur-load').last()[0]); } +// Отдельная функция для загрузки одного изображения +function lazyLoadSingleImage(element) { + const realSrc = element.dataset.src; + const tempImg = new Image(); + + tempImg.src = realSrc; + tempImg.onload = () => { + element.style.backgroundImage = `url('${realSrc}')`; + element.classList.add('loaded'); + }; +} + +// Обновленный обработчик для всей страницы +function lazyLoadImages(selector = '.blur-load') { + document.querySelectorAll(selector).forEach(element => { + if(!element.dataset.loaded) { // Проверка чтобы не дублировать загрузку + element.dataset.loaded = true; + lazyLoadSingleImage(element); + } + }); +} + +// Инициализация при загрузке и после динамического добавления +window.addEventListener('load', () => { + lazyLoadImages(); + // Для динамически добавленных элементов можно вызывать lazyLoadImages() после добавления +}); + function LoadRandomPhotos() diff --git a/static/js/photo.js b/static/js/photo.js index fd93de2..5afa4a0 100644 --- a/static/js/photo.js +++ b/static/js/photo.js @@ -210,7 +210,7 @@ $(document).ready(function() { const url = window.location.pathname; const segments = url.split('/'); - const id = segments[segments.length - 1]; + const id = segments[2]; var faved = parseInt($(this).attr('faved')); $(this).html(faved ? 'Добавить фото в Избранное' : 'Удалить фото из Избранного').attr('faved', faved ? 0 : 1); if (!faved) $('.toggle').attr('class', 'toggle on'); diff --git a/static/js/routing.js b/static/js/routing.js new file mode 100644 index 0000000..be18c60 --- /dev/null +++ b/static/js/routing.js @@ -0,0 +1,258 @@ +document.addEventListener("DOMContentLoaded", () => { + const DEBUG = true; + const cache = new Map(); + const CACHE_TTL = 300000; // 5 минут + let loadingTimeout; + + const loadedScriptSrcs = new Set(); + + function reloadExternalScripts(doc) { + const scripts = Array.from(doc.querySelectorAll("script[src]")); + const loadedUrls = new Set(Array.from(document.scripts).map((s) => s.src)); + + // Загрузка скриптов последовательно + const loadScript = (index) => { + if (index >= scripts.length) return; + + const script = scripts[index]; + const src = script.src; + + if (!loadedUrls.has(src)) { + const newScript = document.createElement("script"); + newScript.src = src; + newScript.async = false; // Важно для порядка + + // Копируем все атрибуты + Array.from(script.attributes).forEach((attr) => { + newScript.setAttribute(attr.name, attr.value); + }); + + newScript.onload = () => { + loadedUrls.add(src); + loadScript(index + 1); // Следующий скрипт + }; + + newScript.onerror = () => { + console.error(`Failed to load: ${src}`); + loadScript(index + 1); // Продолжаем цепочку + }; + + document.body.appendChild(newScript); + } else { + loadScript(index + 1); // Пропускаем уже загруженный + } + }; + + loadScript(0); + } + + document.querySelectorAll("script[src]").forEach((script) => { + const src = script.getAttribute("src"); + if (src) loadedScriptSrcs.add(src); + }); + + // Текущий путь страницы + let lastPath = location.pathname; + + // Элементы интерфейса + const loader = createLoader(); + const contentContainers = ["td.main", "#pmain"]; + + initNavigation(); + + function initNavigation() { + document.body.addEventListener("click", handleClick); + window.addEventListener("popstate", handlePopState); + } + + function handleClick(e) { + const link = e.target.closest("a"); + if (link && shouldIntercept(link)) { + e.preventDefault(); + navigateTo(link.href); + } + } + + function handlePopState() { + navigateTo(window.location.href, true); + } + + function shouldIntercept(link) { + try { + const url = new URL(link.href); + return ( + url.origin === location.origin && !link.dataset.noAjax && !link.hash + ); + } catch { + return false; + } + } + + async function navigateTo(url, isHistoryNavigation = false) { + try { + showLoader(); + + window.commentsCleanup?.(); + delete window.commentsInitialized; + const cached = cache.get(url); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + updatePage(cached.html, url, isHistoryNavigation); + return; + } + + const html = await fetchContent(url); + cache.set(url, { html, timestamp: Date.now() }); + updatePage(html, url, isHistoryNavigation); + } catch (error) { + console.error("Navigation error:", error); + window.location.href = url; + } finally { + hideLoader(); + } + } + + async function fetchContent(url) { + const response = await fetch(url, { + headers: { "X-Requested-With": "XMLHttpRequest" }, + }); + return await response.text(); + } + + function updatePage(html, url, isHistoryNavigation) { + const doc = new DOMParser().parseFromString(html, "text/html"); + const newTitle = doc.title; + const newPath = new URL(url).pathname; + + // Обновление контента + contentContainers.forEach((selector) => { + const container = document.querySelector(selector); + const newContent = doc.querySelector(selector); + if (container && newContent) { + container.innerHTML = newContent.innerHTML; + } + }); + + // Обновление pmain: добавление/удаление + const currPmain = document.querySelector("#pmain"); + const newPmain = doc.querySelector("#pmain"); + if (!currPmain && newPmain) { + document.body.appendChild(newPmain.cloneNode(true)); + } else if (currPmain && !newPmain) { + currPmain.remove(); + } + + // Управление navbar и title-small + const navbar = document.querySelector("#navbard"); + const titleSmall = document.querySelector("#title-small"); + const isPhoto = /\/photo\/\d+/.test(newPath); + if (isPhoto) { + if (navbar) navbar.style.display = "none"; + if (titleSmall) titleSmall.style.display = ""; + } else { + if (navbar) navbar.style.display = ""; + if (titleSmall) titleSmall.style.display = "none"; + } + + // Обработка footer: удаляем дубликаты и ставим единственный в конец + const footers = Array.from(document.querySelectorAll("footer")); + if (footers.length > 1) footers.slice(1).forEach((f) => f.remove()); + const footer = document.querySelector("footer"); + if (footer) document.body.appendChild(footer); + + // Обработка td.footer: оставляем только один и помещаем внутрь таблицы в #pmain + const tdFooters = Array.from(document.querySelectorAll("td.footer")); + if (tdFooters.length > 1) tdFooters.slice(1).forEach((td) => td.remove()); + const singleTdFooter = document.querySelector("td.footer"); + if (singleTdFooter) { + let tableWrapper = document.querySelector("#pmain table.footer-wrapper"); + if (!tableWrapper) { + const tbl = document.createElement("table"); + tbl.className = "footer-wrapper"; + tbl.width = "100%"; + tbl.style.marginTop = "30px"; + const tbody = document.createElement("tbody"); + const tr = document.createElement("tr"); + tbody.appendChild(tr); + tbl.appendChild(tbody); + document.querySelector("#pmain").appendChild(tbl); + tableWrapper = tbl; + } + const tr = tableWrapper.querySelector("tr"); + tr.innerHTML = ""; + tr.appendChild(singleTdFooter); + } + + // Обновление истории + if (!isHistoryNavigation) window.history.pushState({}, "", url); + + // Обновление title + document.title = newTitle; + + // Перезагрузка inline-скриптов + reloadExternalScripts(doc); // Только новые внешние скрипты + reloadInlineScripts(); // Inline-скрипты, кроме Tracy // Инициализация логики + + // Прокрутка наверх + window.scrollTo({ top: 0, behavior: "smooth" }); + + lastPath = newPath; + } + + const executedInlineScripts = new Set(); + + function reloadInlineScripts() { + document.querySelectorAll("script:not([src])").forEach((oldScript) => { + const code = oldScript.textContent.trim(); + if (!code || /^Tracy\.Debug\.init/.test(code)) return; + + const hash = simpleHash(code); + if (executedInlineScripts.has(hash)) return; + + const newScript = document.createElement("script"); + Array.from(oldScript.attributes).forEach((attr) => + newScript.setAttribute(attr.name, attr.value) + ); + newScript.textContent = code; + oldScript.parentNode.replaceChild(newScript, oldScript); + + executedInlineScripts.add(hash); + }); + } + + function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; // Преобразование в 32-битное целое + } + return hash; + } + + function createLoader() { + const loader = document.createElement("div"); + loader.style = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px; + background: #fff; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + border-radius: 5px; + display: none; + z-index: 9999; + `; + loader.innerHTML = "🔄 Загрузка..."; + document.body.appendChild(loader); + return loader; + } + + function showLoader() { + clearTimeout(loadingTimeout); + loadingTimeout = setTimeout(() => (loader.style.display = "block"), 300); + } + + function hideLoader() { + clearTimeout(loadingTimeout); + loader.style.display = "none"; + } +});