/** * @file Basic_functions.js * @brief Основной JavaScript-файл проекта, содержит базовые функции, переменные и настройки */ /** @brief Действие менеджера */ window.managerDataAction = ""; /** @brief Текущий путь */ window.currentPath = ''; /** @brief История путей в менеджере */ window.managerHistoryPaths = [window.currentPath]; /** @brief Индекс текущего пути в истории */ window.managerHistoryIndex = 0; /** @brief Путь к файлу для таблицы менеджера */ window.managerTableDivFilePath = ""; /** @brief Имя файла для таблицы менеджера */ window.managerTableDivFileName = ""; /** @brief Последнее сохранённое имя страницы */ window.saveHowNameLast = "index.page.php"; /** @brief Путь для кнопки открытия страницы */ window.openPageButPath = "no/Select"; /** @brief Флаг, определяющий, запущено ли на телефоне */ window.isPhone = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); /** @brief Координаты касания по X (для мобильных устройств) */ window.touchX = 0; /** @brief Координаты касания по Y (для мобильных устройств) */ window.touchY = 0; /** * @brief Подключает плагин на страницу * @param String plugin Имя подключаемого плагина * @return Promise Разрешается после загрузки всех скриптов плагина */ function includePlugin(plugin) { console.log(plugin) return jsonrpcRequest("includePlugin", { plugin }) .then(html => { const div = document.createElement('div'); div.innerHTML = html; const scripts = []; Array.from(div.childNodes).forEach(node => { if (node.nodeType === 1) node.setAttribute('plugin', plugin); if (node.tagName === 'SCRIPT') { const s = document.createElement('script'); if (node.src) { s.src = node.src; s.async = false; scripts.push(new Promise(res => s.onload = res)); } else { s.textContent = node.textContent; } document.body.appendChild(s); } else { document.body.appendChild(node); } }); return Promise.all(scripts); }) .then(() => { const event = new Event("Load" + plugin + "Js"); document.dispatchEvent(event); window.dispatchEvent(event); }); } window.includePlugin = includePlugin; /** * @brief Удаляет DOM-элементы и скрипты плагина * @param String plugin Имя удаляемого плагина * @return Promise Разрешается после удаления всех файлов и элементов плагина */ function removePluginDom(plugin) { return jsonrpcRequest("removePluginDom", { plugin }) .then(files => { files.forEach(f => { if (f.startsWith('#')) { const el = document.getElementById(f.slice(1)); if (el) el.remove(); } else if (f.endsWith('.css')) { document.querySelectorAll(`link[href="${f}"]`).forEach(el => el.remove()); } else if (/\.js(\.php)?(\?.*)?$/.test(f)) { const nameJs = plugin + "Js"; delete window[nameJs]; const event = new Event('Load' + nameJs); console.log(document.dispatchEvent(event)); document.querySelectorAll('script').forEach(el => { if (el.getAttribute('plugin') === plugin || (el.src && el.src.includes(f))) { el.remove(); } }); } }); document.querySelectorAll(`[plugin="${plugin}"]`).forEach(el => el.remove()); }); } window.removePluginDom = removePluginDom; document.addEventListener('DOMContentLoaded', () => { const newsPlaceholder = document.getElementById('news-placeholder'); const centerFloat = document.querySelector('.center-float'); if (newsPlaceholder && centerFloat) { centerFloat.appendChild(newsPlaceholder); } else if (newsPlaceholder && !centerFloat) { newsPlaceholder.remove(); } }); (function(){ const hbody = document.getElementById('hbody') const menuBtn = document.querySelector('.menu-btn.open') const sideMenu = document.querySelector('.side-menu') const smenu = document.getElementById('smenu') const overlay = document.getElementById('overlay') let rafId function update() { const elems = [ document.querySelector('.menu-btn.open'), document.getElementById('shome'), document.getElementById('smenu'), document.getElementById('slng'), document.getElementById('authorizationButton') ].filter(Boolean) const originalDisplay = new Map() elems.forEach(el => { originalDisplay.set(el, el.style.display) el.style.display = '' }) const totalW = elems.reduce((sum, el) => { const style = getComputedStyle(el) return sum + el.offsetWidth + parseFloat(style.marginLeft) + parseFloat(style.marginRight) }, 0) const baseTop = elems[0].offsetTop const wrapped = elems.some(el => el.offsetTop > baseTop) elems.forEach(el => { el.style.display = originalDisplay.get(el) }) if (totalW > hbody.clientWidth || wrapped) { menuBtn.style.display = '' sideMenu.style.display = '' overlay.style.display = '' smenu.style.display = 'none' } else { menuBtn.style.display = 'none' sideMenu.style.display = 'none' overlay.style.display = 'none' smenu.style.display = '' } } function onResize() { cancelAnimationFrame(rafId) rafId = requestAnimationFrame(update) } window.addEventListener('resize', onResize) window.addEventListener('load', onResize); })() ;(function(){ const menuBtnOpen = document.querySelector('.menu-btn.open'); const menuBtnClose = document.querySelector('.menu-btn.close'); const checkbox = document.getElementById('menu-toggle'); const overlay = document.getElementById('overlay'); const sideMenu = document.querySelector('.side-menu'); menuBtnOpen.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); checkbox.checked = true; overlay.classList.add('active') }); menuBtnClose.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); checkbox.checked = false; overlay.classList.remove('active') }); checkbox.addEventListener('click', e => { e.stopPropagation(); }); document.addEventListener('click', e => { if (!sideMenu.contains(e.target) && !menuBtnOpen.contains(e.target)) { checkbox.checked = false; overlay.classList.remove('active') } }); })(); /** * @brief Получает значение cookie по имени * @param String name Имя cookie * @return String|null Значение cookie или null, если не найдено */ function getCookie(name) { const cookies = document.cookie.split(';'); for (let c of cookies) { let [key, ...val] = c.trim().split('='); if (key === name) return val.join('='); } return null; } /** * @brief Обновляет позицию фона и размер элементов редактирования */ function updateEditElements() { const scale = window.innerWidth <= 549 ? 1.15 : 1; document.querySelectorAll('.editib, .editimc, .sym').forEach(el => { if (!el.dataset.origBgX) { const [origX, origY] = getComputedStyle(el) .backgroundPosition .split(' ') .map(v => parseFloat(v)); el.dataset.origBgX = origX; el.dataset.origBgY = origY; } const origX = parseFloat(el.dataset.origBgX); const origY = parseFloat(el.dataset.origBgY); const newX = (origX * scale).toFixed(2) + 'px'; const newY = (origY * scale).toFixed(2) + 'px'; el.style.backgroundPosition = `${newX} ${newY}`; if (scale > 1) { el.style.width = '30px'; el.style.height = '30px'; el.style.backgroundSize = 'calc(1122px* 1.15)'; } else { el.style.width = ''; el.style.height = ''; el.style.backgroundSize = ''; } }); } window.addEventListener('load', updateEditElements); window.addEventListener('resize', updateEditElements); /** * @brief Настраивает обработчик движения меню по элементу * @param String id Идентификатор основного элемента меню * @param String extraId (опционально) Идентификатор дополнительного элемента для обработки */ function movementMenu(id, extraId = "") { const el = document.getElementById(id); const extraEl = extraId ? document.getElementById(extraId) : document.getElementById(id); if (!el) return; extraEl.addEventListener("pointerdown", e => { if (!isPhone) { coor(e, id); } }); } window.movementMenu = movementMenu; window.addEventListener('load', function(){ const container=document.querySelector('.toolbar-container') const panel=document.getElementById('panel') const arrowLeft=document.getElementById('arrow-left') if(!container||!panel||!arrowLeft||!('ontouchstart' in window))return let startX=0 let currentTranslate=0 container.addEventListener('touchstart',onTouchStart,{passive:false}) container.addEventListener('touchmove',onTouchMove,{passive:false}) container.addEventListener('touchend',onTouchEnd) function getBounds(){ const cw=container.scrollWidth const pw=panel.clientWidth const aw=arrowLeft.clientWidth const extra=aw const maxOffset=24 const minOffset=pw-cw-extra return{maxOffset,minOffset} } function getCurrentTranslate(){ const m=window.getComputedStyle(container).transform return m&&m!=='none'?parseFloat(m.split(',')[4]):0 } function onTouchStart(e){ currentTranslate=getCurrentTranslate() startX=e.touches[0].clientX container.style.transition='none' } function onTouchMove(e){ e.preventDefault() const deltaX=e.touches[0].clientX-startX const{maxOffset,minOffset}=getBounds() let nextTranslate=currentTranslate+deltaX nextTranslate=Math.min(maxOffset,nextTranslate) nextTranslate=Math.max(minOffset,nextTranslate) container.style.transform=`translateX(${nextTranslate}px)` } function onTouchEnd(){ currentTranslate=getCurrentTranslate() } }) addEventListener("pointerup", stco); addEventListener("touchend", stco); /** @brief Вертикальное смещение при перемещении элемента */ let dvV = 0; /** @brief Горизонтальное смещение при перемещении элемента */ let dvH = 0; /** @brief Таймер для удержания перед началом перемещения */ let holdTimer = null; /** @brief ID элемента, который в данный момент перемещается */ let targetId = ""; /** * @brief Запускает процесс захвата координат элемента для перемещения * @param Event event Событие указателя или касания * @param String id Идентификатор элемента для перемещения */ function coor(event, id) { if (event.type === "pointerdown" && event.button !== 0) return; if (event.target.closest('[contenteditable], input, textarea, select, option, ul, ol')) return; const el = document.getElementById(id); if (!el) return; el.style.touchAction = 'none'; el.style.overscrollBehavior = 'contain'; const rect = el.getBoundingClientRect(); el.style.top = rect.top + "px"; el.style.left = rect.left + "px"; removeTranslateOnly(el); const clientY = event.touches ? event.touches[0].clientY : event.clientY; const clientX = event.touches ? event.touches[0].clientX : event.clientX; dvV = clientY - rect.top; dvH = clientX - rect.left; targetId = id; clearTimeout(holdTimer); holdTimer = setTimeout(function() { if (targetId) { addEventListener("pointermove", neco, { passive: false }); addEventListener("touchmove", neco, { passive: false }); } }, 100); } /** * @brief Обновляет позицию перемещаемого элемента при движении указателя или касания * @param Event event Событие указателя или касания */ function neco(event) { if (!targetId) return; event.preventDefault(); const el = document.getElementById(targetId); const clientY = event.touches ? event.touches[0].clientY : event.clientY; const clientX = event.touches ? event.touches[0].clientX : event.clientX; const fvV = clientY - dvV; const fvH = clientX - dvH; el.style.top = fvV + "px"; el.style.left = fvH + "px"; if (targetId === "basis3" && fvH < 0) { el.style.left = "0px"; } } /** * @brief Завершает перемещение элемента и убирает обработчики событий */ function stco() { clearTimeout(holdTimer); removeEventListener("pointermove", neco, { passive: false }); removeEventListener("touchmove", neco, { passive: false }); targetId = ""; } /** * @brief Удаляет из transform только translate-преобразования * @param HTMLElement el Элемент, у которого нужно очистить трансформацию */ function removeTranslateOnly(el) { const tf = el.style.transform.trim(); if (!tf || tf === 'none') return; const parts = tf .split(/\)\s*/) .map(p => p.trim()) .filter(p => p && !p.startsWith('translate(') && !p.startsWith('translateX(') && !p.startsWith('translateY(')) .map(p => p + ')'); el.style.transform = parts.length > 0 ? parts.join(' ') : ''; } /** * @brief Создает AJAX-запрос с помощью jQuery * @param Object data Данные для отправки * @param Function successCallback Колбэк при успешном ответе */ function createAjaxRequest(data, successCallback) { $.ajax({ url: window.location.href, type: "POST", data: data, dataType: "json", success: successCallback, error: function(xhr, status, error) { console.error('Ошибка:', status, error); messageFunction("{{error}}"); } }); /* console.log(data + " " + successCallback); */ } window.createAjaxRequest = createAjaxRequest; /** * @brief Создает объект XMLHttpRequest с преднастроенным POST-запросом * @param Function callback Функция, вызываемая при завершении запроса * @return XMLHttpRequest Объект XHR */ function createXHR(callback) { let xhr = new XMLHttpRequest(); xhr.open("POST", window.location.href, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = callback; return xhr; } window.createXHR = createXHR; /** @brief Счетчик идентификаторов для JSON-RPC */ let rpcId = 0; /** @brief Флаг блокировки генерации идентификатора */ let idLock = false; /** * @brief Генерирует следующий уникальный идентификатор для RPC-запроса * @return Number Уникальный идентификатор */ async function getNextId() { while (idLock) await new Promise(r => setTimeout(r, 1)) idLock = true const id = ++rpcId idLock = false return id } /** * @brief Отправляет JSON-RPC запрос на сервер * @param String method Имя удаленного метода * @param Object params Параметры вызова метода * @return Promise Результат выполнения RPC */ async function jsonrpcRequest(method, params) { const id = await getNextId() const payload = { jsonrpc: "2.0", method, params, id } return fetch(window.location.href, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) .then(res => { if (!res.ok) { messageFunction("HTTP error: " + res.status) throw new Error(`HTTP ${res.status}`) } return res.json() }) .then(r => { if (r.error) { messageFunction("Error: " + r.error.message) throw new Error(`RPC function ${method} (id ${r.id}): ${r.error.code} ${r.error.message}`) } if (r.id !== id) { messageFunction("Error: mismatched function id") throw new Error(`Mismatched id: expected ${id}, got ${r.id}`) } return r.result }) } /** * @brief Долгое нажатие на телефоне * @param HTMLElement el Элемент, на который вешается обработчик * @param Function fn Функция, вызываемая при удержании */ function touchLong(el, fn) { let timer el.addEventListener('touchstart', function(e) { timer = setTimeout(function() { fn(e) }, 500) }, { passive: false }) el.addEventListener('touchend', function() { clearTimeout(timer) }, { passive: true }) el.addEventListener('touchmove', function() { clearTimeout(timer) }, { passive: true }) } document.addEventListener('pointerdown', function(e){ window.touchX = e.clientX window.touchY = e.clientY }, false) document.addEventListener('touchstart', function(e){ window.touchX = e.touches[0].clientX window.touchY = e.touches[0].clientY }, { passive: true }) /* двойный клик для телефонов */ if(isPhone){ document.querySelectorAll('td[ondblclick]').forEach(td=>{ let lastTap=0 td.addEventListener('touchend',function(e){ const now=Date.now() if(now-lastTap<300){ const js=td.getAttribute('ondblclick').replace(/;$/,'') new Function('event',js)(e) } lastTap=now },{passive:true}) }) } /** @brief Флаг, указывающий, были ли изменения в контенте */ window.contentIsEdit = false; if (document.querySelectorAll(".content").length) { document.querySelectorAll(".content").forEach(el => { el.addEventListener("input", () => window.contentIsEdit = true); const observer = new MutationObserver(() => window.contentIsEdit = true); observer.observe(el, { childList: true, subtree: true }); }); } /** @brief Очередь сообщений для отображения */ let messageQueue = []; window.messageQueue = messageQueue; /** @brief Флаг, указывающий, создается ли в данный момент сообщение */ let messageIsCreating = false; /** * @brief Добавляет сообщение в очередь и инициирует его показ * @param String message Текст сообщения */ function messageFunction(message) { messageQueue.push(message); if (!messageIsCreating) { messageCreate(); } } window.messageFunction = messageFunction; /** * @brief Создает и отображает сообщение пользователю */ function messageCreate() { if (messageQueue.length === 0) return; messageIsCreating = true; const messageBlock = document.createElement("div"); messageBlock.classList.add('messageBlock', 'borderStyle'); document.body.appendChild(messageBlock); requestAnimationFrame(() => { messageBlock.classList.add('show'); }); const messageBasicText = document.createElement('div'); messageBasicText.classList.add('messageBasicText', 'borderStyle'); messageBasicText.textContent = "{{message}}"; const messageText = document.createElement('div'); messageText.classList.add('messageText'); messageText.textContent = messageQueue.shift(); const messageButton = document.createElement('div'); messageButton.classList.add('messageButton', 'borderStyle'); messageButton.textContent = "{{ok}}"; messageButton.onclick = messageRemove; messageBlock.appendChild(messageBasicText); messageBlock.appendChild(messageText); messageBlock.appendChild(messageButton); setTimeout(messageRemove, 4000); function messageRemove() { if (messageBlock.parentElement) { messageBlock.classList.remove('show'); setTimeout(() => { messageBlock.remove(); messageIsCreating = false; messageCreate(); }, 150); } } } /** @brief Флаг, подтвержден ли пользователь в сообщении-вопросе */ let userConfirmed = false; window.messageCreateQuestion = messageCreateQuestion; /** * @brief Создает сообщение-вопрос (да/нет) * @return Promise true если пользователь нажал "Да", иначе false */ function messageCreateQuestion() { return new Promise((resolve, reject) => { if (messageQueue.length === 0) return resolve(false); messageIsCreating = true; let messageBlock = document.createElement("div"); messageBlock.className = 'messageBlock borderStyle'; document.body.appendChild(messageBlock); requestAnimationFrame(() => { messageBlock.classList.add('show'); }); let messageBasicText = document.createElement("div"); messageBasicText.classList.add('messageBasicText', 'borderStyle'); messageBasicText.textContent = "{{message}}"; let messageText = document.createElement("div"); messageText.className = 'messageText'; messageText.textContent = messageQueue.shift(); let messageButtonNo = document.createElement("div"); messageButtonNo.classList.add('messageButton', 'borderStyle'); messageButtonNo.textContent = "{{no}}"; messageButtonNo.style.float = "right"; messageButtonNo.onclick = function() { messageBlock.classList.remove('show'); setTimeout(() => { messageBlock.remove(); messageIsCreating = false; reject(false); }, 150); }; let messageButton = document.createElement("div"); messageButton.classList.add('messageButton', 'borderStyle'); messageButton.textContent = "{{yes}}"; messageButton.style.float = "left"; messageButton.onclick = function() { messageBlock.classList.remove('show'); setTimeout(() => { messageBlock.remove(); messageIsCreating = false; resolve(true); }, 0); }; messageBlock.appendChild(messageBasicText); messageBlock.appendChild(messageText); messageBlock.appendChild(messageButtonNo); messageBlock.appendChild(messageButton); }); } /** * @brief Создает сообщение с полем ввода текста * @param String initial Начальное значение для ввода * @return Promise Введенный пользователем текст или null при отмене */ function messageCreateInput(initial = '') { return new Promise((resolve, reject) => { if (messageQueue.length === 0) return resolve(false); messageIsCreating = true; const messageBlock = document.createElement("div"); messageBlock.classList.add("messageBlock", "borderStyle"); document.body.appendChild(messageBlock); requestAnimationFrame(() => { messageBlock.classList.add("show"); }); const messageBasicText = document.createElement("div"); messageBasicText.classList.add("messageBasicText", "borderStyle"); messageBasicText.style.fontSize = '1.4em'; messageBasicText.textContent = messageQueue.shift(); const messageInput = document.createElement("input"); messageInput.classList.add("messageInput", "borderStyle"); messageInput.value = initial; messageInput.type = "text"; const messageButtonOk = document.createElement("div"); messageButtonOk.classList.add("messageButton", "borderStyle"); messageButtonOk.textContent = "{{ok}}"; messageButtonOk.onclick = function() { const value = messageInput.value; messageBlock.classList.remove("show"); setTimeout(() => { messageBlock.remove(); messageIsCreating = false; resolve(value); messageCreate(); }, 150); }; const messageButtonCancel = document.createElement("div"); messageButtonCancel.classList.add("messageButton", "borderStyle"); messageButtonCancel.textContent = "{{cancel}}"; messageButtonCancel.style.width = 'auto'; messageButtonCancel.style.float = 'right'; messageButtonCancel.onclick = function() { messageBlock.classList.remove("show"); setTimeout(() => { messageBlock.remove(); messageIsCreating = false; resolve(null); messageCreate(); }, 150); }; messageBlock.appendChild(messageBasicText); messageBlock.appendChild(messageInput); messageBlock.appendChild(messageButtonOk); messageBlock.appendChild(messageButtonCancel); }); } /** @brief Счётчик для переключения режима отображения HTML */ let cou=1; /** * @brief Показать/скрыть HTML-код страницы в textarea */ let lastType = 'html'; function showCode(type) { let contents = document.getElementsByClassName("content"); let tex = document.getElementById("tex"); if (cou == 1) { let combined = ""; for (let i = 0; i < contents.length; i++) { combined += ""; } tex.value = combined; tex.value = decodeHtmlEntities(tex.value); if (type === 'html') { tex.value = formatHTML(tex.value); } else if (type === 'marked') { tex.value = marked.parse(tex.value); } let sbe = document.getElementsByClassName("sb"); for (let i = 0; i < sbe.length; i++) { if (sbe[i] != tex) sbe[i].style.visibility = "hidden"; } tex.style.visibility = tex.style.visibility == "hidden" ? "visible" : "hidden"; lastType = type; cou = 2; } else { let text = tex.value; let split = text.match(//gs); const centerFloat = document.querySelector('.center-float'); if (!centerFloat) return; let need = split ? split.length : 1; let floats = centerFloat.getElementsByClassName('bfloat'); while (floats.length < need) { let bf = document.createElement('div'); bf.className = 'bfloat'; if (floats.length > 0) bf.style.borderRadius = "10px"; let c = document.createElement('div'); c.className = 'content'; c.contentEditable = 'true'; centerFloat.appendChild(bf); bf.appendChild(c); centerFloat.appendChild(document.createElement('br')); floats = centerFloat.getElementsByClassName('bfloat'); } while (floats.length > need) { let next = floats[floats.length - 1].nextSibling; if (next && next.tagName === 'BR') centerFloat.removeChild(next); centerFloat.removeChild(floats[floats.length - 1]); floats = centerFloat.getElementsByClassName('bfloat'); } contents = centerFloat.getElementsByClassName("content"); if (split) { for (let i = 0; i < need; i++) { let val = split[i] ? split[i].replace(//g, '') : ''; if (lastType === 'marked') val = '
| {{name}} | ${sizeHeader}{{column_creation_date}} |