Files
slava.home/data/Basic_functions.js

1566 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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) {
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
*/
function showHtmlCode() {
let contents = document.getElementsByClassName("content");
if (cou==1) {
let combined = "";
for (let i=0; i<contents.length; i++) combined += "<![CDATA[" + contents[i].innerHTML + "]]>";
document.getElementById("tex").value = combined;
document.getElementById("tex").value = decodeHtmlEntities(document.getElementById("tex").value);
document.getElementById("tex").value = formatHTML(document.getElementById("tex").value);
let sbe=document.getElementsByClassName("sb");
for(let i=0; i<sbe.length; i++) {
if (sbe[i] != document.getElementById("tex"))
sbe[i].style.visibility="hidden";
}
if (document.getElementById("tex").style.visibility=="hidden") {
document.getElementById("tex").style.visibility="visible";
} else {
document.getElementById("tex").style.visibility="hidden";
}
cou=2;
} else {
let text = document.getElementById("tex").value;
let split = text.match(/<!\[CDATA\[(.*?)\]\]>/gs);
if (split) {
for (let i=0; i<contents.length; i++) {
if (split[i]) contents[i].innerHTML = split[i].replace(/<!\[CDATA\[|\]\]>/g, '');
}
}
document.getElementById("tex").style.visibility="hidden";
cou=1;
}
}
window.showHtmlCode = showHtmlCode;
/** @brief Флаг нового состояния страницы при сохранении */
window.newPageFunValue = "";
/**
* @brief Сохраняет изменения страницы (основной контент, заголовок, плагины)
*/
function saveChanges() {
if (window.newPageFunValue == "newPage") {
document.getElementById("saveHow").click();
} else {
saveContentId();
saveHeading();
savePlugins();
}
document.getElementById("settingsMain_d").style.visibility="hidden";
}
window.saveChanges = saveChanges;
/**
* @brief Сохраняет центральный блок контента
*/
function saveContentId() {
if(cou==1) {
let contents = document.getElementsByClassName("content");
let combined = "";
for (let i = 0; i < contents.length; i++) combined += "<![CDATA[" + contents[i].innerHTML.trim() + "]]>";
document.getElementById("tex").value = combined;
}
document.getElementById("tex").value = decodeHtmlEntities(document.getElementById("tex").value);
let saveContentIdData = document.getElementById("tex").value.trim();
jsonrpcRequest("savePageCenterBlock", { saveContentIdData }).then(ans => {
if (ans == 'true') {
console.log("{{main_block_saved}}");
} else {
messageFunction("{{main_block_not_saved}}");
}
});
}
/**
* @brief Сохраняет данные подключённых плагинов (левой и правой колонок)
*/
function savePlugins() {
let pluginsFloatsId = ["left-float", "right-float"];
let floatsBlockKeys = ["lblock", "rblock"];
let requestsData = {
left: [],
right: []
};
pluginsFloatsId.forEach((pluginsFloatId, i) => {
let plugins = document.getElementById(pluginsFloatId).querySelectorAll('.plugin-url');
plugins.forEach(plugin => {
let pluginData = {
pluginUrl: plugin.pluginUrl,
title: "",
tclass: "",
bclass: ""
};
let tElem = plugin.querySelector('[tclass]');
pluginData.title = tElem ? tElem.textContent.trim() : "";
pluginData.tclass = tElem ? tElem.getAttribute('class') : "";
let bElem = plugin.querySelector('[bclass]');
pluginData.bclass = bElem ? bElem.getAttribute('class') : "";
let blockKey = floatsBlockKeys[i];
let blockArray = (blockKey === "lblock") ? requestsData.left : requestsData.right;
blockArray.push(pluginData);
});
});
let data = `floatsBlock=${encodeURIComponent(JSON.stringify(requestsData))}`;
jsonrpcRequest("savePageSideBlocks", { floatsBlock: JSON.stringify(requestsData) }).then(response => {
console.log(response);
});
}
/**
* @brief Сохраняет заголовок страницы
*/
function saveHeading() {
document.querySelectorAll('#mainTitle').forEach((editTitle) => {
if (functionOpenPage === true) {
messageFunction("{{open_page}}");
} else {
let newTitle = editTitle.innerHTML.trim();
if (newTitle === "") {
messageFunction("{{plugin_title_empty_error}}");
} else {
jsonrpcRequest("savePageTitle", { newTitle }).then(response => {
console.log(response);
});
}
}
});
}
/**
* @brief Декодирует HTML-сущности (&...;)
* @param String str Строка для декодирования
* @return String Декодированная строка
*/
function decodeHtmlEntities(str) {
var textarea = document.createElement('textarea');
textarea.innerHTML = str;
return textarea.value;
}
/**
* @brief Форматирует HTML-код (переносы строк и отступы)
* @param String html Исходный HTML
* @return String Отформатированный HTML
*/
function formatHTML(html) {
html = html.replace(/<([^\/][^>\s]*)[^>]*>/g, "\n$&\n");
html = html.replace(/<\/([^>\s]*)>/g, "\n$&\n");
html = html.replace(/^\s*[\r\n]/gm, '');
let lines = html.split('\n');
let indentLevel = 0;
let indentSize = 2;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line.match(/^<\/[^>]+>/)) indentLevel--;
if (line.match(/^<\/[^>]+>/) || line.match(/^<[^\/>]+[^>]*>$/)) lines[i] = ' '.repeat(indentLevel * indentSize) + line;
if (line.match(/^<[^\/>]+[^>]*>$/)) indentLevel++;
}
html = lines.join('\n');
return html;
}
/**
* @brief Сохраняет изменения в новый файл
* @param String currentPath Текущий путь
* @return Promise
*/
window.saveContentIdHow = async function (currentPath) {
if (cou == 1) {
let contents = Array.from(document.getElementsByClassName("content")).map(el => el.innerHTML.trim());
document.getElementById("tex").value = contents.map(c => `<![CDATA[${c}]]>`).join('');
}
let saveContentIdData = decodeHtmlEntities(document.getElementById("tex").value).trim();
let nameFile = document.getElementById('saveHowName').value;
if (!nameFile.endsWith('.page.php')) {
nameFile += '.page.php';
messageQueue.push('{{page_must_end_with_page_php}}');
return;
}
messageQueue.push('{{save_file_as}} ' + nameFile + '?');
if (!(await messageCreateQuestion())) {
return;
}
let ans = await jsonrpcRequest("checkFile", { path: currentPath, nameFile: nameFile });
if (ans == 'true') {
messageQueue.push(`{{file}} ${nameFile} {{exists_overwrite_prompt}}`);
if (!(await messageCreateQuestion())) {
return;
}
sendSaveRequest(currentPath, nameFile, saveContentIdData, true);
window.newPageFunValue = "";
document.getElementById("mainTitle").innerHTML = "<i>{{new_file}}</i>";
} else if (ans == 'false') {
createNewFile(currentPath, nameFile, saveContentIdData);
window.newPageFunValue = "";
document.getElementById("mainTitle").innerHTML = "<i>{{new_file}}</i>";
} else {
messageQueue.push("{{file_save_failed}}");
}
managerData(currentPath);
document.getElementById("managerDiv").style.visibility = "hidden";
removePluginDom("manager");
};
/**
* @brief Отправляет запрос на сохранение страницы
* @param String currentPath Текущий путь
* @param String nameFile Имя файла
* @param String saveContentIdData Содержимое
* @param Boolean overwrite Перезаписать существующий файл
*/
function sendSaveRequest(currentPath, nameFile, saveContentIdData, overwrite = false) {
let pluginsFloatsId = ["left-float", "right-float"];
let floatsBlockKeys = ["lblock", "rblock"];
let requestsData = { floatsBlock: [], title: [], pluginUrl: [], tclass: [], bclass: [] };
pluginsFloatsId.forEach((id, i) => {
document.getElementById(id).querySelectorAll('.plugin-url').forEach(plugin => {
requestsData.floatsBlock.push(floatsBlockKeys[i]);
let tElem = plugin.querySelector('[tclass]');
requestsData.title.push(tElem ? tElem.textContent.trim() : "");
requestsData.pluginUrl.push(plugin.pluginUrl);
requestsData.tclass.push(tElem ? tElem.getAttribute('class') : "");
let bElem = plugin.querySelector('[bclass]');
requestsData.bclass.push(bElem.getAttribute('class'));
});
});
let params = {
page_url: currentPath,
nameFile: nameFile,
overwrite: overwrite,
saveContentIdData: saveContentIdData,
floatsBlock: requestsData.floatsBlock,
title: requestsData.title,
pluginUrl: requestsData.pluginUrl,
tclass: requestsData.tclass,
bclass: requestsData.bclass,
};
jsonrpcRequest("saveHowPageContent", params).then(ans => {
messageFunction("{{changes_saved_successfully}}");
managerData(currentPath);
document.getElementById("managerDiv").style.visibility = "hidden";
removePluginDom("manager");
});
}
/**
* @brief Создаёт новый файл страницы
* @param String currentPath Текущий путь
* @param String nameFile Имя файла
* @param String saveContentIdData Содержимое
*/
function createNewFile(currentPath, nameFile, saveContentIdData) {
jsonrpcRequest("createNewPage", { saveContentIdData, page_url: currentPath, nameFile }).then(ans => {
if (ans == 'true') {
messageFunction(`{{file}} ${nameFile} {{created_successfully}}!`);
}
managerData(currentPath);
document.getElementById("managerDiv").style.visibility = "hidden";
removePluginDom("manager");
});
}
/**
* @brief Формирует строку запроса для XMLHttpRequest
* @param Array dataNames Массив имён параметров
* @param Array dataValues Массив значений параметров
* @return String Строка запроса
*/
function createQueryString(dataNames, dataValues) {
let data = "";
for (let i = 0; i < dataNames.length; i++) {
data += `${dataNames[i]}=${encodeURIComponent(dataValues[i])}&`;
}
return data.slice(0, -1);
}
/**
* @brief Переключает отображение меню настроек сайта
*/
window.toggleMenu = function() {
document.getElementById('siteSettings').style.display = document.getElementById('siteSettings').style.display === 'none' || document.getElementById('siteSettings').style.display === '' ? 'block' : 'none';
};
/**
* @brief Скрывает меню настроек сайта при клике вне кнопки
*/
window.onclick = function(event) {
var btn = document.getElementById('siteSettingsButton');
var settings = document.getElementById('siteSettings');
if (btn && !btn.contains(event.target)) {
settings && (settings.style.display = 'none');
}
};
/** @brief Режим настроек древа сайта */
window.treeSettingsMode = "";
/**
* @brief Открывает/закрывает древо сайта
*/
function basisVisSiteTree() {
siteTreeGeneration();
window.treeSettingsMode = "";
let treeDiv = document.getElementById("treeDiv");
if (treeDiv.style.visibility=="hidden") {
treeDiv.style.visibility = "visible";
if(isPhone) {
closeWindows("treeDiv");
}
} else {
treeDiv.style.visibility = "hidden";
document.getElementById("treeProperties").style.visibility = "hidden";
removePluginDom("site_tree")
}
document.getElementById('treeCloseFun').onclick = function() {
treeDiv.style.visibility = "hidden";
document.getElementById("treeProperties").style.visibility = "hidden";
removePluginDom("site_tree")
};
treeSettings();
}
window.basisVisSiteTree = basisVisSiteTree;
/**
* @brief Включает режим выбора ссылки из древа сайта
*/
function linkFromPage() {
siteTreeGeneration();
window.treeSettingsMode = 'linkFromPage';
let treeDiv = document.getElementById("treeDiv");
treeDiv.style.visibility = "visible";
document.getElementById('treeCloseFun').onclick = function() {
treeDiv.style.visibility = "hidden";
document.getElementById("treeProperties").style.visibility = "hidden";
removePluginDom("site_tree")
};
treeSettings();
removePluginDom("site_tree")
}
window.linkFromPage = linkFromPage;
/**
* @brief Переключает отображение дочерних элементов в древе
* @param HTMLElement el Элемент, по которому кликнули
*/
function toggleChildren(el) {
let details = el.closest('li').querySelector('.details');
if (!details) return;
details.style.display = details.style.display === 'none' ? 'block' : 'none';
el.querySelector('.tree-marker').textContent = details.style.display === 'block' ? '▼' : '►';
}
window.toggleChildren = toggleChildren;
/**
* @brief Открывает/закрывает менеджер файлов и папок
*/
async function basisVisManager() {
let managerDiv = document.getElementById('managerDiv');
if (managerDiv.style.visibility === "hidden") {
await includePlugin("manager");
managerDiv.style.visibility = "visible";
window.managerDataAction = "";
managerData(currentPath);
if(isPhone) closeWindows("managerDiv");
} else {
managerDiv.style.visibility = "hidden";
document.getElementById("managerProperties").style.visibility = "hidden";
removePluginDom("manager");
}
}
window.basisVisManager = basisVisManager;
/**
* @brief Закрывает одно окно при открытии другого (managerDiv/treeDiv)
* @param String div Идентификатор открываемого окна
*/
function closeWindows(div) {
if (div == "managerDiv") {
document.getElementById("treeDiv").style.visibility = "hidden";
document.getElementById("treeProperties").style.visibility = "hidden";
removePluginDom("site_tree")
} else if (div == "treeDiv") {
document.getElementById("managerDiv").style.visibility = "hidden";
document.getElementById("managerProperties").style.visibility = "hidden";
removePluginDom("manager");
}
}
/**
* @brief Обновляет данные менеджера файлов для указанного пути
* @param String path Путь к папке
*/
function managerData(path) {
currentPath = path
if (managerHistoryPaths[managerHistoryIndex] !== currentPath) {
managerHistoryPaths = managerHistoryPaths.slice(0, managerHistoryIndex + 1)
managerHistoryPaths.push(currentPath)
managerHistoryIndex++
}
jsonrpcRequest("getFolderContents", { managerPathFolder: path }).then(data => {
let rows = ''
for (let i = 0; i < data.length; i++) {
if (data[i].name) {
let newPath = `${path}/${data[i].name}`
let attr = 'style="cursor: pointer; display: flex; align-items: center;" '
if (data[i].type === '{{file}}') {
let dbl = 'ondblclick="managerData(\'' + newPath + '\');"'
attr += isPhone
? 'onclick="managerData(\'' + newPath + '\');" ' + dbl
: dbl
} else if (data[i].name.endsWith('.page.php')) {
let page = newPath.replace('.page.php', '')
let dbl = 'ondblclick="getPage(\'' + page + '\');"'
attr += isPhone
? 'onclick="getPage(\'' + page + '\');" ' + dbl
: dbl
}
let icon = data[i].type === '{{file}}'
? '<div style="margin-right:5px;background-position:-678px -559px;width:26px;height:26px;background-image:url(../../img/pict/mc_iconslyb.svg);display:inline-block;"></div>'
: '<div style="margin-right:5px;background-position:3px -557px;width:26px;height:26px;background-image:url(../../img/pict/mc_iconslyb.svg);display:inline-block;"></div>'
let unit = data[i].type === '{{file}}' ? ' files' : ' bytes'
let sizeText = data[i].size + unit
let nameCell = isPhone
? `<div style="display:flex;flex-direction:column;align-items:flex-start;">
<div style="display:flex;align-items:center;">${icon}${data[i].name}</div>
<div style="font-size:0.7em;color:#666;margin-left:31px;">${sizeText}</div>
</div>`
: `${icon}${data[i].name}`
rows += `
<tr style="height:30px;" class="managerTableDivFile" path="${data[i].path}">
<td ${attr}>${nameCell}</td>
${isPhone ? '' : `<td>${sizeText}</td>`}
<td>${data[i].creationTime}</td>
</tr>`
}
}
let actionButton = ''
if (document.getElementById('saveHowName')) {
if (document.getElementById('saveHowName').value === '{{select_file}}') {
document.getElementById('saveHowName').value = 'index.page.php'
}
saveHowNameLast = document.getElementById('saveHowName').value
document.getElementById('saveHowButton').removeEventListener('click', saveHow)
document.getElementById('saveHowButton').removeEventListener('click', openPageBut)
}
let managerTopTitleName = '{{file_manager_title}}'
if (window.managerDataAction === 'saveHow') {
actionButton = `<div id="saveHowDiv"> {{name}}: <input id="saveHowName" value="${saveHowNameLast}"><span id="saveHowButton">{{save}}</span> </div>`
managerTopTitleName = '{{save}} как'
} else if (window.managerDataAction === 'getPage') {
actionButton = `<div id="saveHowDiv"> {{name}}: <input readonly id="saveHowName" value="{{select_file}}"><span id="saveHowButton">{{open}}</span> </div>`
managerTopTitleName = '{{open}}'
} else if (window.managerDataAction === 'propertiesUrl') {
actionButton = `<div id="saveHowDiv"> {{name}}: <input readonly id="saveHowName" value="{{select_file}}"><span id="saveHowButton">Выбрать</span> </div>`
managerTopTitleName = '{{open}}'
} else if (window.managerDataAction === 'selectImgForm') {
actionButton = `<div id="saveHowDiv"> {{name}}: <input readonly id="saveHowName" value="{{select_file}}"><span id="saveHowButton">{{open}}</span> </div>`
managerTopTitleName = '{{choose}}'
} else if (window.managerDataAction === 'selectImgFormToForm') {
actionButton = `<div id="saveHowDiv"> {{name}}: <input readonly id="saveHowName" value="{{select_file}}"><span id="saveHowButton">{{open}}</span> </div>`
managerTopTitleName = '{{choose}}'
}
let sizeHeader = isPhone ? '' : `<td style="width:15%;">{{column_size_bytes}}</td>`
let dateWidth = isPhone ? '55%' : '23%'
document.getElementById('managerDiv').innerHTML = `
<div id="managerTop">
<span id="managerTopTitle">${managerTopTitleName}</span>
<span id="managerCloseFun" class="editib"></span>
</div>
<div id="managerTopDiv" style="margin:7px 20px;">
<span id="managerHistoryBackFun" class="editib"></span>
<span id="managerHistoryForwFun" class="editib"></span>
<span id="managerBackFun" class="editib"></span>
<span id="managerPath" contentEditable="false">
<span class="managerPathButton" onclick="managerData('');">&nbsp;/&nbsp;</span>
${currentPath.split('/').filter(Boolean).map((seg, idx) => {
let segPath = '/' + currentPath.split('/').slice(1, idx + 2).join('/')
return ` <span class="managerPathButton" onclick="managerData('${segPath}');">${seg} /</span>`
}).join('')}
</span>
</div>
<div id="managerTableDiv">
<table id="managerTable">
<tr id="managerTableTitle">
<td style="width:45%;">{{name}}</td>
${sizeHeader}
<td style="width:${dateWidth};">{{column_creation_date}}</td>
</tr>
${rows}
</table>
</div>
${actionButton}
`
if (window.managerDataAction === 'saveHow') {
document.getElementById('saveHowButton').addEventListener('click', saveHow)
} else if (window.managerDataAction === 'getPage') {
document.getElementById('saveHowButton').addEventListener('click', openPageBut)
} else if (window.managerDataAction === 'propertiesUrl') {
document.getElementById('saveHowButton').addEventListener('click', propertiesUrlFun)
} else if (window.managerDataAction === 'selectImgForm') {
document.getElementById('saveHowButton').addEventListener('click', selectImgFormButFun)
} else if (window.managerDataAction === 'selectImgFormToForm') {
document.getElementById('saveHowButton').addEventListener('click', selectImgFormToFormButFun)
}
openPageButPath = 'no/Select'
if (window.managerDataAction === 'getPage') {
document.querySelectorAll('.managerTableDivFile').forEach(el => {
el.addEventListener('click', function() {
openPageButPath = 'no/Select'
const td = this.querySelector('td')
const dbl = td.getAttribute('ondblclick') || ''
if (dbl.includes('getPage')) {
const m = dbl.match(/getPage\(['"]([^'"]+)['"]\)/)
if (m) openPageButPath = m[1]
const inp = document.getElementById('saveHowName')
if (inp) inp.value = td.innerText
}
})
})
} else if (window.managerDataAction === 'selectImgForm') {
document.querySelectorAll('.managerTableDivFile').forEach(el => {
el.addEventListener('click', function() {
openPageButPath = 'no/Select'
const td = this.querySelector('td')
if (td) {
openPageButPath = td.getAttribute('data-path') || openPageButPath
const inp = document.getElementById('saveHowName')
if (inp) inp.value = td.innerText
}
})
})
} else if (window.managerDataAction === 'selectImgFormToForm') {
document.querySelectorAll('.managerTableDivFile').forEach(el => {
el.addEventListener('click', function() {
openPageButPath = 'no/Select'
const td = this.querySelector('td')
if (td) {
openPageButPath = td.getAttribute('data-path') || openPageButPath
const inp = document.getElementById('saveHowName')
if (inp) inp.value = td.innerText
}
})
})
} else if (window.managerDataAction === 'saveHow' || window.managerDataAction === 'propertiesUrl') {
document.querySelectorAll('.managerTableDivFile').forEach(el => {
el.addEventListener('click', function() {
openPageButPath = 'no/Select'
const td = this.querySelector('td')
if (!td) return
let dbl = td.getAttribute('ondblclick') || ''
if (dbl.includes('getPage')) {
let nd = dbl.replace(/getPage\(['"][^'"]+['"]\)/, '')
if (!nd.trim()) td.removeAttribute('ondblclick')
else td.setAttribute('ondblclick', nd)
}
const inp = document.getElementById('saveHowName')
if (inp) inp.value = td.innerText
})
})
}
managerSettings()
managerFun()
})
}
/** @brief Флаг открытия страницы */
window.functionOpenPage = false;
/**
* @brief Загружает страницу по указанному пути
* @param String newPath Путь к новой странице
*/
function getPage(newPath) {
window.functionOpenPage = true;
jsonrpcRequest("getPage", { newPath }).then(page => {
const centerFloat = document.querySelector('.center-float');
const oldBlocks = centerFloat.querySelectorAll('.bfloat');
oldBlocks.forEach(b => {
if (b.querySelector('.content')) {
const next = b.nextSibling;
if (next && next.nodeType === 1 && next.tagName === 'BR') next.remove();
if (next && next.nodeType === 3 && next.textContent.trim() === '') {
const next2 = next.nextSibling;
if (next2 && next2.nodeType === 1 && next2.tagName === 'BR') next2.remove();
}
b.remove();
}
});
page.content.forEach((c, i) => {
const placeholder = document.getElementById('news-placeholder');
if (i > 0) {
const br = document.createElement('br');
centerFloat.insertBefore(br, placeholder);
}
const block = document.createElement('div');
block.className = 'bfloat' + (i === 0 ? ' content1' : '');
block.style.fontSize = '1em';
const inner = document.createElement('div');
inner.className = 'content';
inner.innerHTML = c;
block.appendChild(inner);
centerFloat.insertBefore(block, placeholder);
});
document.getElementById("right-float").innerHTML = page.right;
document.getElementById("left-float").innerHTML = page.left;
document.getElementById("managerDiv").style.visibility = "hidden";
document.getElementById("mainTitle").innerHTML = "<i>{{new_file}}!</i>";
removePluginDom("manager");
});
window.newPageFunValue = "";
}
//обьявление функции для того, чтобы обращатся к ней из других файлов
window.getPage = getPage;
window.managerData = managerData;
/* Функция z-index элементов */
document.addEventListener("DOMContentLoaded", function() {
var selectors = [
'#basis3 .cust',
'#managerDiv #managerSettings',
'#managerProperties',
'#treeDiv #treeSettings',
'#treeProperties',
'#authorizationDiv'
];
var groups = selectors.map(function(sel) {
var parts = sel.split(' ');
var elements = [];
parts.forEach(function(part) {
if (part.startsWith('#')) {
var el = document.getElementById(part.slice(1));
if (el) elements.push(el);
}
if (part.startsWith('.')) {
elements.push(...document.getElementsByClassName(part.slice(1)));
}
});
return elements;
});
var queue = [];
document.addEventListener('pointerdown', function(e) {
var clickedGroup = -1;
groups.forEach(function(group, idx) {
if (group.some(el => el.contains(e.target))) {
clickedGroup = idx;
}
});
if (clickedGroup !== -1) {
var i = queue.indexOf(clickedGroup);
if (i !== -1) queue.splice(i, 1);
queue.unshift(clickedGroup);
}
var result = selectors.map((_, idx) => {
var pos = queue.indexOf(idx);
return pos === -1 ? 100 : 199 - pos;
});
groups.forEach(function(group, idx) {
group.forEach(function(el) {
el.style.zIndex = result[idx];
});
});
});
var selectorsElements = [
'#basis3',
'.cust',
'#managerDiv',
'#managerSettings',
'#managerProperties',
'#treeDiv',
'#treeSettings',
'#treeProperties',
'#authorizationDiv'
];
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'style') {
var el = mutation.target;
var oldVal = mutation.oldValue || '';
var newVis = el.style.visibility;
if (newVis === 'visible' && !/visibility\s*:\s*visible/.test(oldVal)) {
selectorsElements.forEach(function(sel) {
document.querySelectorAll(sel).forEach(function(item) {
if (item === el) {
item.style.zIndex = 199;
} else {
let z = parseInt(item.style.zIndex, 10) || 100;
item.style.zIndex = z > 100 ? z - 1 : 100;
}
});
});
}
}
});
});
selectorsElements.forEach(function(sel) {
document.querySelectorAll(sel).forEach(function(el) {
observer.observe(el, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true
});
});
});
document.querySelectorAll('.cust').forEach(el=>{
let hideTimer, watchTimer, watching=false
const observer = new MutationObserver(muts=>{
muts.forEach(m=>{
if(m.attributeName==='style' && getComputedStyle(el).visibility==='visible'){
watching = false
clearTimeout(watchTimer)
watchTimer = setTimeout(()=> watching = true, 0)
}
})
})
observer.observe(el, { attributes: true, attributeFilter: ['style'] })
document.addEventListener('pointerdown', e=>{
if(watching && getComputedStyle(el).visibility==='visible' && !el.contains(e.target)){
clearTimeout(hideTimer)
hideTimer = setTimeout(()=> el.style.visibility = 'hidden', 0)
}
})
el.addEventListener('pointerdown', e=>{
e.stopPropagation()
clearTimeout(hideTimer)
})
})
function updateZIndices() {
const result = selectors.map((_, idx) => {
const pos = queue.indexOf(idx);
return pos === -1 ? 100 : 199 - pos;
});
groups.forEach((group, idx) => {
group.forEach(el => el.style.zIndex = result[idx]);
});
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'style') {
const el = mutation.target;
const oldVal = mutation.oldValue || '';
const newVis = getComputedStyle(el).visibility;
if (newVis === 'visible' && !/visibility\s*:\s*visible/.test(oldVal)) {
selectors.forEach((sel, idx) => {
groups[idx].forEach(member => {
if (member === el) {
const i = queue.indexOf(idx);
if (i !== -1) queue.splice(i, 1);
queue.unshift(idx);
updateZIndices();
}
});
});
}
}
});
});
});
/* путь элементов */
document.querySelectorAll('#left-float [plugin-url], #right-float [plugin-url]').forEach((el) => {
el.pluginUrl = el.getAttribute('plugin-url');
el.classList.add('plugin-url');
el.removeAttribute('plugin-url');
});
/* редактирования заголовков */
window.addEventListener('load', function() {
const editable = document.querySelector('#mainTitle');
const observer = new MutationObserver(() => {
const txt = editable.textContent.replace(/\u00A0/g, ' ');
if (editable.innerHTML !== txt) {
const sel = window.getSelection();
const anchorNode = sel.anchorNode;
const anchorOffset = sel.anchorOffset;
observer.disconnect();
editable.textContent = txt;
const range = document.createRange();
let node = editable.firstChild || editable;
const offset = Math.min(anchorOffset, node.textContent.length);
range.setStart(node, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
observer.observe(editable, { childList: true, subtree: true, characterData: true });
}
});
if(editable && editable.nodeType === 1){
observer.observe(editable, { childList: true, subtree: true, characterData: true });
}
});