Mercurial
view .cms/js/admin.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
line wrap: on
line source
// Две функции для чтения и установки cookie. // Cookie используются для авторизации на сайте, // для запоминания количества отображаемых страниц в пейджере // и выбранной темы. // Другие модули так же могут их использовать // для запоминания своих настроек. function get_cookie( name ) { let cookies = document.cookie.split( ";" ); for ( let line of cookies ) { let cookie = line.split( "=" ); // TODO: Нужно ли decodeURIComponent( cookie[ 0 ].trim() )? // Имена указываются обычно латиницей, без специальных знаков... if ( name == cookie[ 0 ].trim() ) { return decodeURIComponent( cookie[ 1 ] ); } } return ""; } // ;Path=/ в админке нет смысла указывать, но можно экономить байты куков // браузер сам по умолчанию возьмет текущий путь function set_cookie( name, value ) { document.cookie = encodeURIComponent( name ) + "=" + encodeURIComponent( value ) + ";SameSite=Lax"; } function set_cookie_expires( name, value ) { let expires = ( new Date( Date.now() + 365 * 86400 * 1000 ) ).toUTCString(); document.cookie = encodeURIComponent( name ) + "=" + encodeURIComponent( value ) + ";SameSite=Lax;expires=" + expires; } // Чтобы удалить куки нужно установить отрицательную дату истечения срока действия function del_cookie( name ) { document.cookie = encodeURIComponent( name ) + "=;SameSite=Lax;max-age=-1"; } // Notifications // Уведомления, отображающиеся в правом верхнем. Обычно в течении 5 сек. // Через 5 секунд к уведомлению добавляется класс timeout, // благодаря ему происходит исчезновение уведомления. function notify( message, classes, msec ) { let bulb = document.createElement( "div" ); bulb.innerHTML = message; bulb.className = classes; document.querySelector( ".log-info-box" ).appendChild( bulb ); let h = bulb.offsetHeight; // Чтобы анимировать схлопывание bulb.setAttribute( "style", `height:${h}px` ); if ( msec ) { setTimeout( function() { bulb.classList.add( "timeout" ); }, msec); } } // Translate // module: "admin.mod.php" or "pages.mod.php" etc // Шаблон admin добавляет глобальную js-переменную // в которой содержится текущая локаль и переводы, // загруженные из файлов .cms/lang/... // Поэтому ими можно пользоваться и на стороне админки. function __( str, module ) { if ( cms && cms.locale && cms.lang && cms.lang[module] && cms.lang[module][cms.locale] && cms.lang[module][cms.locale][str] ) { return cms.lang[module][cms.locale][str]; } else { api( { fn: "no_translation", str: str, module: module } ); return str; } } // Call server side API // Передаваемые на сервер данные упаковываются как положено // и можно передавать даже массивы. // Массивы вложенные в массивы воспринимаются как объекты // и кодируются в JSON. // После того, как сервер вернет ответ, вызывается функция rfn // И ответ передается ей в качестве параметра. function api( data, rfn, efn ) { const formData = new FormData(); buildFormData( formData, data ); // send data // По умолчанию запросы отправляются асинхронно, // но если нужно дождаться ответа и затем изменить полученные данные, // то перед вызовом обновляющих функций нужно дописать строчку // cms.async_api = false; let ajax = new XMLHttpRequest(); ajax.addEventListener( "load", function( event ) { let data = {}; try { data = JSON.parse( event.target.responseText ); } catch { notify( __( "server_error", "admin.mod.php" ), "info-error", 5000 ); if ( efn ) { efn( event ); } } if ( rfn ) { rfn( data ); } } ); ajax.addEventListener( "error", function( event ) { notify( __( "network_error", "admin.mod.php" ), "info-error", 5000 ); if ( efn ) { efn( event ); } } ); ajax.open( "POST", cms.api, cms.async_api ); ajax.send( formData ); cms.async_api = true; } function buildFormData( formData, data, parentKey ) { if ( data && typeof data === 'object' && ! ( data instanceof Date ) && ! ( data instanceof File ) ) { Object.keys( data ).forEach( key => { buildFormData( formData, data[key], parentKey ? `${parentKey}[${key}]` : key ); } ); } else { const value = data == null ? '' : data; formData.append( parentKey, value ); } } // Create and connect Codemirror function codemirror_connect( selector, name, options = {} ) { if ( window[name] ) return; let default_options = { mode: "application/x-httpd-php", styleActiveLine: true, lineNumbers: true, lineWrapping: true, autoCloseBrackets: true, smartIndent: true, indentUnit: 4, tabSize: 4, matchBrackets: true, foldGutter: true, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], autoCloseTags: { whenClosing: true, whenOpening: true, indentTags: [ "div", "ul", "ol", "script", "style" ], }, phrases: { "Search:": __( "codemirror_search", "admin.mod.php" ), "(Use /re/ syntax for regexp search)" : __( "codemirror_re", "admin.mod.php" ), "Replace all:": __( "codemirror_replace_all", "admin.mod.php" ), "With:": __( "codemirror_replace_with", "admin.mod.php" ), "Replace:": __( "codemirror_replace_replace", "admin.mod.php" ), "Replace?": __( "codemirror_replace_confirm", "admin.mod.php" ), "Yes": __( "codemirror_yes", "admin.mod.php" ), "No": __( "codemirror_no", "admin.mod.php" ), "All": __( "codemirror_all", "admin.mod.php" ), "Stop": __( "codemirror_stop", "admin.mod.php" ), }, extraKeys: { "Ctrl-Space": "autocomplete" } } let txtarea = document.querySelector( selector ); options = Object.assign( default_options, options ); window[name] = CodeMirror.fromTextArea( txtarea, options ); if ( window[name].showHint ) { window[name].on( "keydown", function( editor, event ) { if ( event.ctrlKey == true ) { return } // Ctrl+S call Hint let isAlphaKey = /^[a-zA-Z]$/.test( event.key ); if ( window[name].state.completionActive && isAlphaKey ) { return; } // Prevent autocompletion in string literals or comments let cursor = window[name].getCursor(); let token = window[name].getTokenAt( cursor ); if ( token.type === "string" || token.type === "comment" ) { return; } let lineBeforeCursor = window[name].doc.getLine( cursor.line ); if ( typeof lineBeforeCursor !== "string" ) { return; } lineBeforeCursor = lineBeforeCursor.substring( 0, cursor.ch ); // disable autoclose tag before text let charAfterCursor = window[name].doc.getLine( cursor.line ); charAfterCursor = charAfterCursor.substring( cursor.ch, cursor.ch + 1 ); window[name].options.autoCloseTags.dontCloseTags = null; if ( charAfterCursor.match( /\S/ ) && charAfterCursor != "<" ) { if ( lineBeforeCursor.match( /<[^>]+$/ ) ) { let tag = lineBeforeCursor.match( /<(\w+)\b[^>]*$/ ); if ( tag ) { tag = tag[1]; window[name].options.autoCloseTags.dontCloseTags = [tag]; } } } let m = CodeMirror.innerMode( window[name].getMode(), token.state ); let innerMode = m.mode.name; let shouldAutocomplete; if ( innerMode === "html" || innerMode === "xml" ) { shouldAutocomplete = event.key === "<" || event.key === "/" && token.type === "tag" || isAlphaKey && token.type === "tag" || isAlphaKey && token.type === "attribute" || token.string === "=" && token.state.htmlState && token.state.htmlState.tagName; } else if ( innerMode === "css" ) { shouldAutocomplete = isAlphaKey || event.key === ":" || event.key === " " && /:\s+$/.test( lineBeforeCursor ); } else if ( innerMode === "javascript" ) { shouldAutocomplete = isAlphaKey || event.key === "."; } else if ( innerMode === "clike" && window[name].options.mode === "php" ) { shouldAutocomplete = token.type === "keyword" || token.type === "variable"; } if ( shouldAutocomplete ) { window[name].showHint( { completeSingle: false } ); } } ); } // Replace < with < in code context // https://stackoverflow.com/a/36388061/20443861 window[name].on( "inputRead", function( cm, event ) { if ( event.origin == "paste" ) { if ( cm.paste_context == "code" ) { let text = event.text.join( "\n" ); // pasted string let new_text = text.replaceAll( /</g, "<" ); new_text = new_text.replaceAll( /&&/g, "&&" ); cm.execCommand( "undo" ); cm.replaceSelection( new_text ); } } } ); window[name].on( "paste", async function( editor, event ) { let cursor = window[name].getCursor(); let token = window[name].getTokenAt( cursor ); if ( token && token.state && token.state.html && token.state.html.htmlState && token.state.html.htmlState.context && token.state.html.htmlState.context.tagName == "code" ) { window[name].paste_context = "code"; } else { window[name].paste_context = ""; } } ); } document.addEventListener( "DOMContentLoaded", function( event ) { function _( str ) { return __( str, "admin.mod.php" ); } // Mob Menu document.querySelectorAll( "header .burger, .milk" ).forEach( function( el ) { el.onclick = function() { document.body.classList.toggle( "mobile-menu-open" ); } } ); // Навигация стрелками браузера - фикс активного пункта window.addEventListener( "popstate", function( e ) { // Очищаем без всяких проверок чтобы не подсвечивало на #start document.querySelectorAll( "aside a" ).forEach( function( page ) { page.classList.remove( "active" ); } ); // Выделяем нужную ссылку let target = window.location.hash; let a = document.querySelector( `aside a[href="${target}"]` ); if ( a ) { a.classList.add( "active" ); } // надо ли непонятно, скопировал из предыдущего обработчика кликов на всякий случай document.body.classList.remove( "mobile-menu-open" ); } ); // Theme switcher document.querySelectorAll( ".theme-switcher" ).forEach( function( el ) { el.addEventListener( "click", function( event ) { event.preventDefault(); let n = get_cookie( "theme" ) || 0; let styles2 = admin_styles[n]; let styles = styles2.split( " " ); styles.forEach( function( style ) { document.documentElement.classList.remove( style ); } ); n = (+n+1) % admin_styles.length; styles2 = admin_styles[n]; styles = styles2.split( " " ); styles.forEach( function( style ) { document.documentElement.classList.add( style ); } ); notify( admin_styles[n], "info-success", 5000 ); set_cookie( "theme" , n ); theme_event = new Event( "theme" ); document.dispatchEvent( theme_event ); } ); } ); // Logout document.querySelectorAll( "[data-logout]" ).forEach( function ( logoutBtn ) { logoutBtn.addEventListener( "click", function() { // Отправляем на бекенд чтобы закрыло сессию api( { fn: "logout" }, // Перезагрузка страницы в случае если админка ответила function() { window.location.reload( true ); }, // Даже если нет связи, удаляем куки и перезагружаем страницу function() { del_cookie( "sess" ); window.location.reload( true ); } ); }); } ); // Highlight active menu if ( document.body.classList.contains( "logged" ) ) { let page = window.location.hash; // Зачем тут исключение #start? if ( page && page != "#start" ) { let el = document.querySelector( `a[href="${page}"]` ); if ( el ) el.classList.add( "active" ); } else if ( document.querySelector( "#start a[href='#base']" ) ) { // Приветственная страничка с указанием подключиться к БД window.location.hash = "#start"; } else { // Если никакой пункт не выбран, выбираем по умолчанию Страницы window.location.hash = "#pages"; } } // Clear Cache document.querySelectorAll( ".clear-cache" ).forEach( function( el ) { el.addEventListener( "click", function( e ){ e.preventDefault(); api( { fn: "clear_cache" }, function( r ){ if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); } }); }); } ); // Admin section, Save properties document.querySelectorAll( "[data-am-save]" ).forEach( function( saveButton ) { saveButton.addEventListener( "click", function( e ) { let el = this.closest( "[data-am-item]" ); let item = el.getAttribute( "data-am-item" ); let selector = `#admin_menu [data-am-item="${item}"]`; let title = document.querySelector( `${selector} [name=title]` ); if ( title ) { title = title.value; } let section = document.querySelector( `${selector} .section-select-grid .field-select` ); if ( section ) { section = section.getAttribute( "data-section" ); } let data = { fn: "admin_menu_save", type: el.getAttribute( "data-am-type" ), module: el.getAttribute( "data-am-module" ), item: item, title: title, sort: document.querySelector( `${selector} [name=sort]` ).value, section: section, reset: this.hasAttribute( "data-am-reset" ), } api( data, function( r ) { if ( r.ok == "true" ) { window.location.reload( true ); } } ); } ); } ); // Admin section, Delete Container document.querySelectorAll( "[data-am-delete]" ).forEach( function( button ) { button.addEventListener( "click", function( e ){ let item = this.closest( "[data-am-item]" ).getAttribute( "data-am-item" ); let childs = document.querySelectorAll( `[data-am-childs="${item}"] > div` ).length; if ( childs ) { notify( _( "not_empty_section" ), "info-error", 2000 ); return; } if ( ! confirm( _( "confirm_delete" ) ) ) return; let data = { fn: "admin_menu_del", item: item, } api( data, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); if ( r.info_time ) { setTimeout( function() { window.location.reload( true ); }, r.info_time ); } } } ); } ); } ); // Admin section, Hide document.querySelectorAll( "[data-am-sw]" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { let el = this.closest( "[data-am-item]" ); let data = { fn: "admin_menu_hide", type: el.getAttribute( "data-am-type" ), module: el.getAttribute( "data-am-module" ), item: el.getAttribute( "data-am-item" ), hide: el.classList.contains( "showed" ), } if ( data.item == "admin_menu" ) { if ( ! confirm( _( "hide_admin_settings" ) ) ) return false; } api( data, function( r ) { if ( r.ok == "true" ) { window.location.reload( true ); } } ); } ); } ); // Admin section, Add Section document.querySelectorAll( "#admin_menu .main-footer .add-section" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { api( { fn: "admin_menu_add_section" }, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); if ( r.info_time ) setTimeout( function() { window.location.reload( true ); }, r.info_time ); } } ); } ); } ); // Reset all items in Admin Menu document.querySelectorAll( "#admin_menu .main-footer .reset-all" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { api( { fn: "reset_admin_menu_items" }, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); if ( r.info_time ) setTimeout( function() { window.location.reload( true ); }, r.info_time ); } } ); } ); } ); // Disable Modules document.querySelectorAll( "#modules .module-sw-btn" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { let closest = this.closest( "[data-module]" ); let data = { fn: "module_disable", disable: closest.classList.contains( "enabled" ), module: closest.getAttribute( "data-module" ), } api( data, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); } else { window.location.reload( true ); } } ); } ); } ); // Delete Module document.querySelectorAll( "#modules .module-del-btn" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { let module = this.closest( "[data-module]" ).getAttribute( "data-module" ); let data = { fn: "module_del", module: module, } api( data, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); } } ); } ); } ); // Close Sessions document.querySelectorAll( "#auth [data-login]" ).forEach( function( button ) { button.addEventListener( "click", function( e ) { e.preventDefault(); if ( ! confirm( _( "confirm_logout" ) ) ) return; let parent = button.parentElement; var data = { fn: "logout", sess: this.getAttribute( "data-login" ), } api( data, function( r ) { if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); if ( r.result == "refresh" ) { // refresh приходит если мы закрываем свою сессию window.location.reload( true ); } else if ( r.result == "ok" ) { // Перемещаем сессию в закрытые путем копирования html, // предварительно удалив класс del-sess чтобы не отображался крестик. // А старый элемент удаляем. // При копировании html так же удаляется привязанная функция. button.classList.remove( "del-sess" ); let html = parent.outerHTML; parent.remove(); document.querySelector( ".history-sess .sess-table" ).insertAdjacentHTML( "afterbegin", html ); } } } ); } ); } ); // Show/Hide password document.querySelectorAll( ".password-eye" ).forEach( function( eye ) { eye.addEventListener( "click", function( e ) { this.classList.toggle( "showed" ); let inp = this.previousElementSibling; let t = inp.getAttribute( "type" ); if ( t == "password" ) { inp.setAttribute( "type", "text" ); } else { inp.setAttribute( "type", "password" ); } inp.focus(); } ); } ); // Install module (upload module) let input = document.querySelector( "#module-upload" ); if ( input ) input.addEventListener( "change", async function( e ) { const formData = new FormData(); formData.append( "fn", "install_module" ); for ( let i = 0; i < input.files.length; i++ ) { formData.append( "myfile[]", input.files[i] ); } let ajax = new XMLHttpRequest(); /* ajax.upload.addEventListener( "progress", function( event ) { let percent = Math.round( (event.loaded / event.total) * 100 ); bar.style.width = percent + "%"; }, false ); ajax.addEventListener( "error", function( event ) { notify( _( "error_upload_file" ), "info-error", 3600000 ); bar.style = ""; }, false ); ajax.addEventListener( "abort", function( event ) { notify( _( "error_upload_file" ), "info-error", 3600000 ); bar.style = ""; }, false ); */ let google_chrome_fix = this; ajax.addEventListener( "load", function( event ) { google_chrome_fix.value = ""; let r = JSON.parse( event.target.responseText ); if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); if ( r.info_class === "info-success" ) setTimeout( function() { window.location.reload( true ); }, r.info_time ); } } ); ajax.open( "POST", cms.api ); ajax.send( formData ); } ); // БД. Открытие дополнительных настроек. document.querySelectorAll( "#base .pro-btn" ).forEach( function( pro ) { pro.addEventListener( "click", function( e ) { document.querySelector( "#base form" ).classList.toggle( "show-pro" ); } ); } ); // Выбор секции Админского меню. Select document.querySelectorAll( "#admin_menu .field-select" ).forEach( function( select ) { select.addEventListener( "click", function( e ) { e.stopPropagation(); this.parentElement.classList.toggle( "open" ); // это можно убрать если переставить стили на родителя select.nextElementSibling.classList.toggle( "open" ); } ); } ); // Выбор секции Админского меню. Option document.querySelectorAll( "#admin_menu .field-options .option" ).forEach( function( select ) { select.addEventListener( "click", function( e ) { let input = this.closest( ".section-select-grid" ).querySelector( ".field-select" ); input.querySelector( ".value" ).innerText = this.innerText; input.setAttribute( "data-section", this.getAttribute( "value" ) ); } ); } ); // Select // Закрытие выпадающих списков при кликах вне их, а так же по ним document.body.addEventListener( "click", function( e ) { document.querySelectorAll( "#admin_menu .section-select-grid" ).forEach( function( list ) { list.classList.remove( "open" ); } ); // это можно убрать если переставить стили на родителя document.querySelectorAll( "#admin_menu .field-options" ).forEach( function( list ) { list.classList.remove( "open" ); } ); } ); /* Обновление и проверка файлов */ document.querySelectorAll( "#modules [data-fn]" ).forEach( function( button ) { button.addEventListener( "click", function() { let fn = this.getAttribute( "data-fn" ); let data = { fn: fn } api( data, function( r ) { if ( r.answer ) { switch ( fn ) { case "cms_update": document.querySelector( "#modules .update-window" ).insertAdjacentHTML( "beforeend", r.answer ); break; case "create_zip": document.querySelector( "#modules .dev-window" ).insertAdjacentHTML( "beforeend", r.answer ); break; case "cms_check_update": case "cms_check_dev_update": document.querySelector( "#modules .check-answer" ).innerHTML = r.answer; break; case "cms_changed_files": let div = document.querySelector( "#modules .changed-files" ); div.innerHTML = r.answer; div.style.display = ""; break; } } if ( r.info_text ) { notify( r.info_text, r.info_class, r.info_time ); } if ( r.reload ) { setTimeout( function() { window.location.reload( true ); }, r.info_time ); } } ); } ); } ); // show update from dev buttons document.querySelectorAll( "#modules [data-show-dev]" ).forEach( function( btn ) { btn.addEventListener( "click", function() { let dev = document.querySelector( "#modules .developers_only" ); if ( dev ) { dev.classList.remove( "developers_only" ); if ( window.location.host == "dev.coffee-cms.ru" ) { dev.querySelector( "#modules [data-fn='create_zip']" ).removeAttribute( "style" ); } } this.remove(); } ); } ); } );