comparison .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
comparison
equal deleted inserted replaced
-1:000000000000 0:78edf6b517a0
1 // Две функции для чтения и установки cookie.
2 // Cookie используются для авторизации на сайте,
3 // для запоминания количества отображаемых страниц в пейджере
4 // и выбранной темы.
5 // Другие модули так же могут их использовать
6 // для запоминания своих настроек.
7 function get_cookie( name ) {
8 let cookies = document.cookie.split( ";" );
9 for ( let line of cookies ) {
10 let cookie = line.split( "=" );
11 // TODO: Нужно ли decodeURIComponent( cookie[ 0 ].trim() )?
12 // Имена указываются обычно латиницей, без специальных знаков...
13 if ( name == cookie[ 0 ].trim() ) {
14 return decodeURIComponent( cookie[ 1 ] );
15 }
16 }
17 return "";
18 }
19
20 // ;Path=/ в админке нет смысла указывать, но можно экономить байты куков
21 // браузер сам по умолчанию возьмет текущий путь
22 function set_cookie( name, value ) {
23 document.cookie = encodeURIComponent( name ) + "=" + encodeURIComponent( value ) + ";SameSite=Lax";
24 }
25
26 function set_cookie_expires( name, value ) {
27 let expires = ( new Date( Date.now() + 365 * 86400 * 1000 ) ).toUTCString();
28 document.cookie = encodeURIComponent( name ) + "=" + encodeURIComponent( value ) + ";SameSite=Lax;expires=" + expires;
29 }
30
31 // Чтобы удалить куки нужно установить отрицательную дату истечения срока действия
32 function del_cookie( name ) {
33 document.cookie = encodeURIComponent( name ) + "=;SameSite=Lax;max-age=-1";
34 }
35
36 // Notifications
37
38 // Уведомления, отображающиеся в правом верхнем. Обычно в течении 5 сек.
39 // Через 5 секунд к уведомлению добавляется класс timeout,
40 // благодаря ему происходит исчезновение уведомления.
41 function notify( message, classes, msec ) {
42 let bulb = document.createElement( "div" );
43 bulb.innerHTML = message;
44 bulb.className = classes;
45 document.querySelector( ".log-info-box" ).appendChild( bulb );
46 let h = bulb.offsetHeight;
47 // Чтобы анимировать схлопывание
48 bulb.setAttribute( "style", `height:${h}px` );
49 if ( msec ) {
50 setTimeout( function() {
51 bulb.classList.add( "timeout" );
52 }, msec);
53 }
54 }
55
56 // Translate
57 // module: "admin.mod.php" or "pages.mod.php" etc
58
59 // Шаблон admin добавляет глобальную js-переменную
60 // в которой содержится текущая локаль и переводы,
61 // загруженные из файлов .cms/lang/...
62 // Поэтому ими можно пользоваться и на стороне админки.
63 function __( str, module ) {
64 if ( cms && cms.locale && cms.lang && cms.lang[module] && cms.lang[module][cms.locale] && cms.lang[module][cms.locale][str] ) {
65 return cms.lang[module][cms.locale][str];
66 } else {
67 api( { fn: "no_translation", str: str, module: module } );
68 return str;
69 }
70 }
71
72 // Call server side API
73
74 // Передаваемые на сервер данные упаковываются как положено
75 // и можно передавать даже массивы.
76 // Массивы вложенные в массивы воспринимаются как объекты
77 // и кодируются в JSON.
78 // После того, как сервер вернет ответ, вызывается функция rfn
79 // И ответ передается ей в качестве параметра.
80 function api( data, rfn, efn ) {
81 const formData = new FormData();
82 buildFormData( formData, data );
83 // send data
84 // По умолчанию запросы отправляются асинхронно,
85 // но если нужно дождаться ответа и затем изменить полученные данные,
86 // то перед вызовом обновляющих функций нужно дописать строчку
87 // cms.async_api = false;
88 let ajax = new XMLHttpRequest();
89 ajax.addEventListener( "load", function( event ) {
90 let data = {};
91 try {
92 data = JSON.parse( event.target.responseText );
93 } catch {
94 notify( __( "server_error", "admin.mod.php" ), "info-error", 5000 );
95 if ( efn ) {
96 efn( event );
97 }
98 }
99 if ( rfn ) {
100 rfn( data );
101 }
102 } );
103 ajax.addEventListener( "error", function( event ) {
104 notify( __( "network_error", "admin.mod.php" ), "info-error", 5000 );
105 if ( efn ) {
106 efn( event );
107 }
108 } );
109 ajax.open( "POST", cms.api, cms.async_api );
110 ajax.send( formData );
111 cms.async_api = true;
112 }
113 function buildFormData( formData, data, parentKey ) {
114 if ( data && typeof data === 'object' && ! ( data instanceof Date ) && ! ( data instanceof File ) ) {
115 Object.keys( data ).forEach( key => {
116 buildFormData( formData, data[key], parentKey ? `${parentKey}[${key}]` : key );
117 } );
118 } else {
119 const value = data == null ? '' : data;
120 formData.append( parentKey, value );
121 }
122 }
123
124
125 // Create and connect Codemirror
126 function codemirror_connect( selector, name, options = {} ) {
127
128 if ( window[name] ) return;
129
130 let default_options = {
131 mode: "application/x-httpd-php",
132 styleActiveLine: true,
133 lineNumbers: true,
134 lineWrapping: true,
135 autoCloseBrackets: true,
136 smartIndent: true,
137 indentUnit: 4,
138 tabSize: 4,
139 matchBrackets: true,
140 foldGutter: true,
141 gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
142 autoCloseTags: {
143 whenClosing: true,
144 whenOpening: true,
145 indentTags: [ "div", "ul", "ol", "script", "style" ],
146 },
147 phrases: {
148 "Search:": __( "codemirror_search", "admin.mod.php" ),
149 "(Use /re/ syntax for regexp search)" : __( "codemirror_re", "admin.mod.php" ),
150 "Replace all:": __( "codemirror_replace_all", "admin.mod.php" ),
151 "With:": __( "codemirror_replace_with", "admin.mod.php" ),
152 "Replace:": __( "codemirror_replace_replace", "admin.mod.php" ),
153 "Replace?": __( "codemirror_replace_confirm", "admin.mod.php" ),
154 "Yes": __( "codemirror_yes", "admin.mod.php" ),
155 "No": __( "codemirror_no", "admin.mod.php" ),
156 "All": __( "codemirror_all", "admin.mod.php" ),
157 "Stop": __( "codemirror_stop", "admin.mod.php" ),
158 },
159 extraKeys: { "Ctrl-Space": "autocomplete" }
160 }
161
162 let txtarea = document.querySelector( selector );
163 options = Object.assign( default_options, options );
164 window[name] = CodeMirror.fromTextArea( txtarea, options );
165
166 if ( window[name].showHint ) {
167 window[name].on( "keydown", function( editor, event ) {
168 if ( event.ctrlKey == true ) { return } // Ctrl+S call Hint
169 let isAlphaKey = /^[a-zA-Z]$/.test( event.key );
170 if ( window[name].state.completionActive && isAlphaKey ) {
171 return;
172 }
173
174 // Prevent autocompletion in string literals or comments
175 let cursor = window[name].getCursor();
176 let token = window[name].getTokenAt( cursor );
177 if ( token.type === "string" || token.type === "comment" ) {
178 return;
179 }
180
181 let lineBeforeCursor = window[name].doc.getLine( cursor.line );
182 if ( typeof lineBeforeCursor !== "string" ) {
183 return;
184 }
185 lineBeforeCursor = lineBeforeCursor.substring( 0, cursor.ch );
186
187 // disable autoclose tag before text
188 let charAfterCursor = window[name].doc.getLine( cursor.line );
189 charAfterCursor = charAfterCursor.substring( cursor.ch, cursor.ch + 1 );
190 window[name].options.autoCloseTags.dontCloseTags = null;
191 if ( charAfterCursor.match( /\S/ ) && charAfterCursor != "<" ) {
192 if ( lineBeforeCursor.match( /<[^>]+$/ ) ) {
193 let tag = lineBeforeCursor.match( /<(\w+)\b[^>]*$/ );
194 if ( tag ) {
195 tag = tag[1];
196 window[name].options.autoCloseTags.dontCloseTags = [tag];
197 }
198 }
199 }
200
201 let m = CodeMirror.innerMode( window[name].getMode(), token.state );
202 let innerMode = m.mode.name;
203 let shouldAutocomplete;
204 if ( innerMode === "html" || innerMode === "xml" ) {
205 shouldAutocomplete = event.key === "<" ||
206 event.key === "/" && token.type === "tag" ||
207 isAlphaKey && token.type === "tag" ||
208 isAlphaKey && token.type === "attribute" ||
209 token.string === "=" && token.state.htmlState && token.state.htmlState.tagName;
210 } else if ( innerMode === "css" ) {
211 shouldAutocomplete = isAlphaKey ||
212 event.key === ":" ||
213 event.key === " " && /:\s+$/.test( lineBeforeCursor );
214 } else if ( innerMode === "javascript" ) {
215 shouldAutocomplete = isAlphaKey || event.key === ".";
216 } else if ( innerMode === "clike" && window[name].options.mode === "php" ) {
217 shouldAutocomplete = token.type === "keyword" || token.type === "variable";
218 }
219 if ( shouldAutocomplete ) {
220 window[name].showHint( { completeSingle: false } );
221 }
222 } );
223 }
224
225 // Replace < with &lt; in code context
226 // https://stackoverflow.com/a/36388061/20443861
227 window[name].on( "inputRead", function( cm, event ) {
228 if ( event.origin == "paste" ) {
229 if ( cm.paste_context == "code" ) {
230 let text = event.text.join( "\n" ); // pasted string
231 let new_text = text.replaceAll( /</g, "&lt;" );
232 new_text = new_text.replaceAll( /&&/g, "&amp;&amp;" );
233 cm.execCommand( "undo" );
234 cm.replaceSelection( new_text );
235 }
236 }
237 } );
238 window[name].on( "paste", async function( editor, event ) {
239 let cursor = window[name].getCursor();
240 let token = window[name].getTokenAt( cursor );
241 if ( token && token.state && token.state.html && token.state.html.htmlState && token.state.html.htmlState.context && token.state.html.htmlState.context.tagName == "code" ) {
242 window[name].paste_context = "code";
243 } else {
244 window[name].paste_context = "";
245 }
246 } );
247
248 }
249
250
251 document.addEventListener( "DOMContentLoaded", function( event ) {
252
253 function _( str ) {
254 return __( str, "admin.mod.php" );
255 }
256
257 // Mob Menu
258 document.querySelectorAll( "header .burger, .milk" ).forEach( function( el ) {
259 el.onclick = function() {
260 document.body.classList.toggle( "mobile-menu-open" );
261 }
262 } );
263
264 // Навигация стрелками браузера - фикс активного пункта
265 window.addEventListener( "popstate", function( e ) {
266 // Очищаем без всяких проверок чтобы не подсвечивало на #start
267 document.querySelectorAll( "aside a" ).forEach( function( page ) {
268 page.classList.remove( "active" );
269 } );
270 // Выделяем нужную ссылку
271 let target = window.location.hash;
272 let a = document.querySelector( `aside a[href="${target}"]` );
273 if ( a ) {
274 a.classList.add( "active" );
275 }
276 // надо ли непонятно, скопировал из предыдущего обработчика кликов на всякий случай
277 document.body.classList.remove( "mobile-menu-open" );
278 } );
279
280 // Theme switcher
281 document.querySelectorAll( ".theme-switcher" ).forEach( function( el ) {
282 el.addEventListener( "click", function( event ) {
283 event.preventDefault();
284 let n = get_cookie( "theme" ) || 0;
285 let styles2 = admin_styles[n];
286 let styles = styles2.split( " " );
287 styles.forEach( function( style ) {
288 document.documentElement.classList.remove( style );
289 } );
290 n = (+n+1) % admin_styles.length;
291 styles2 = admin_styles[n];
292 styles = styles2.split( " " );
293 styles.forEach( function( style ) {
294 document.documentElement.classList.add( style );
295 } );
296 notify( admin_styles[n], "info-success", 5000 );
297 set_cookie( "theme" , n );
298 theme_event = new Event( "theme" );
299 document.dispatchEvent( theme_event );
300 } );
301 } );
302
303 // Logout
304 document.querySelectorAll( "[data-logout]" ).forEach( function ( logoutBtn ) {
305 logoutBtn.addEventListener( "click", function() {
306 // Отправляем на бекенд чтобы закрыло сессию
307 api( { fn: "logout" },
308 // Перезагрузка страницы в случае если админка ответила
309 function() {
310 window.location.reload( true );
311 },
312 // Даже если нет связи, удаляем куки и перезагружаем страницу
313 function() {
314 del_cookie( "sess" );
315 window.location.reload( true );
316 }
317 );
318 });
319 } );
320
321 // Highlight active menu
322 if ( document.body.classList.contains( "logged" ) ) {
323 let page = window.location.hash;
324 // Зачем тут исключение #start?
325 if ( page && page != "#start" ) {
326 let el = document.querySelector( `a[href="${page}"]` );
327 if ( el ) el.classList.add( "active" );
328 } else if ( document.querySelector( "#start a[href='#base']" ) ) {
329 // Приветственная страничка с указанием подключиться к БД
330 window.location.hash = "#start";
331 } else {
332 // Если никакой пункт не выбран, выбираем по умолчанию Страницы
333 window.location.hash = "#pages";
334 }
335 }
336
337 // Clear Cache
338 document.querySelectorAll( ".clear-cache" ).forEach( function( el ) {
339 el.addEventListener( "click", function( e ){
340 e.preventDefault();
341 api( { fn: "clear_cache" }, function( r ){
342 if ( r.info_text ) {
343 notify( r.info_text, r.info_class, r.info_time );
344 }
345 });
346 });
347 } );
348
349
350 // Admin section, Save properties
351 document.querySelectorAll( "[data-am-save]" ).forEach( function( saveButton ) {
352 saveButton.addEventListener( "click", function( e ) {
353 let el = this.closest( "[data-am-item]" );
354 let item = el.getAttribute( "data-am-item" );
355 let selector = `#admin_menu [data-am-item="${item}"]`;
356 let title = document.querySelector( `${selector} [name=title]` );
357 if ( title ) {
358 title = title.value;
359 }
360 let section = document.querySelector( `${selector} .section-select-grid .field-select` );
361 if ( section ) {
362 section = section.getAttribute( "data-section" );
363 }
364 let data = {
365 fn: "admin_menu_save",
366 type: el.getAttribute( "data-am-type" ),
367 module: el.getAttribute( "data-am-module" ),
368 item: item,
369 title: title,
370 sort: document.querySelector( `${selector} [name=sort]` ).value,
371 section: section,
372 reset: this.hasAttribute( "data-am-reset" ),
373 }
374 api( data, function( r ) {
375 if ( r.ok == "true" ) {
376 window.location.reload( true );
377 }
378 } );
379 } );
380 } );
381
382 // Admin section, Delete Container
383 document.querySelectorAll( "[data-am-delete]" ).forEach( function( button ) {
384 button.addEventListener( "click", function( e ){
385 let item = this.closest( "[data-am-item]" ).getAttribute( "data-am-item" );
386 let childs = document.querySelectorAll( `[data-am-childs="${item}"] > div` ).length;
387 if ( childs ) {
388 notify( _( "not_empty_section" ), "info-error", 2000 );
389 return;
390 }
391 if ( ! confirm( _( "confirm_delete" ) ) ) return;
392 let data = {
393 fn: "admin_menu_del",
394 item: item,
395 }
396 api( data, function( r ) {
397 if ( r.info_text ) {
398 notify( r.info_text, r.info_class, r.info_time );
399 if ( r.info_time ) {
400 setTimeout( function() {
401 window.location.reload( true );
402 }, r.info_time );
403 }
404 }
405 } );
406 } );
407 } );
408
409 // Admin section, Hide
410 document.querySelectorAll( "[data-am-sw]" ).forEach( function( button ) {
411 button.addEventListener( "click", function( e ) {
412 let el = this.closest( "[data-am-item]" );
413 let data = {
414 fn: "admin_menu_hide",
415 type: el.getAttribute( "data-am-type" ),
416 module: el.getAttribute( "data-am-module" ),
417 item: el.getAttribute( "data-am-item" ),
418 hide: el.classList.contains( "showed" ),
419 }
420 if ( data.item == "admin_menu" ) {
421 if ( ! confirm( _( "hide_admin_settings" ) ) ) return false;
422 }
423 api( data, function( r ) {
424 if ( r.ok == "true" ) {
425 window.location.reload( true );
426 }
427 } );
428 } );
429 } );
430
431 // Admin section, Add Section
432 document.querySelectorAll( "#admin_menu .main-footer .add-section" ).forEach( function( button ) {
433 button.addEventListener( "click", function( e ) {
434 api( { fn: "admin_menu_add_section" }, function( r ) {
435 if ( r.info_text ) {
436 notify( r.info_text, r.info_class, r.info_time );
437 if ( r.info_time )
438 setTimeout( function() {
439 window.location.reload( true );
440 }, r.info_time );
441 }
442 } );
443 } );
444 } );
445
446
447 // Reset all items in Admin Menu
448 document.querySelectorAll( "#admin_menu .main-footer .reset-all" ).forEach( function( button ) {
449 button.addEventListener( "click", function( e ) {
450 api( { fn: "reset_admin_menu_items" }, function( r ) {
451 if ( r.info_text ) {
452 notify( r.info_text, r.info_class, r.info_time );
453 if ( r.info_time )
454 setTimeout( function() {
455 window.location.reload( true );
456 }, r.info_time );
457 }
458 } );
459 } );
460 } );
461
462
463 // Disable Modules
464 document.querySelectorAll( "#modules .module-sw-btn" ).forEach( function( button ) {
465 button.addEventListener( "click", function( e ) {
466 let closest = this.closest( "[data-module]" );
467 let data = {
468 fn: "module_disable",
469 disable: closest.classList.contains( "enabled" ),
470 module: closest.getAttribute( "data-module" ),
471 }
472 api( data, function( r ) {
473 if ( r.info_text ) {
474 notify( r.info_text, r.info_class, r.info_time );
475 } else {
476 window.location.reload( true );
477 }
478 } );
479 } );
480 } );
481
482 // Delete Module
483 document.querySelectorAll( "#modules .module-del-btn" ).forEach( function( button ) {
484 button.addEventListener( "click", function( e ) {
485 let module = this.closest( "[data-module]" ).getAttribute( "data-module" );
486 let data = {
487 fn: "module_del",
488 module: module,
489 }
490 api( data, function( r ) {
491 if ( r.info_text ) {
492 notify( r.info_text, r.info_class, r.info_time );
493 }
494 } );
495 } );
496 } );
497
498 // Close Sessions
499 document.querySelectorAll( "#auth [data-login]" ).forEach( function( button ) {
500 button.addEventListener( "click", function( e ) {
501 e.preventDefault();
502 if ( ! confirm( _( "confirm_logout" ) ) ) return;
503 let parent = button.parentElement;
504 var data = {
505 fn: "logout",
506 sess: this.getAttribute( "data-login" ),
507 }
508 api( data, function( r ) {
509 if ( r.info_text ) {
510 notify( r.info_text, r.info_class, r.info_time );
511 if ( r.result == "refresh" ) {
512 // refresh приходит если мы закрываем свою сессию
513 window.location.reload( true );
514 } else if ( r.result == "ok" ) {
515 // Перемещаем сессию в закрытые путем копирования html,
516 // предварительно удалив класс del-sess чтобы не отображался крестик.
517 // А старый элемент удаляем.
518 // При копировании html так же удаляется привязанная функция.
519 button.classList.remove( "del-sess" );
520 let html = parent.outerHTML;
521 parent.remove();
522 document.querySelector( ".history-sess .sess-table" ).insertAdjacentHTML( "afterbegin", html );
523 }
524 }
525 } );
526 } );
527 } );
528
529 // Show/Hide password
530 document.querySelectorAll( ".password-eye" ).forEach( function( eye ) {
531 eye.addEventListener( "click", function( e ) {
532 this.classList.toggle( "showed" );
533 let inp = this.previousElementSibling;
534 let t = inp.getAttribute( "type" );
535 if ( t == "password" ) {
536 inp.setAttribute( "type", "text" );
537 } else {
538 inp.setAttribute( "type", "password" );
539 }
540 inp.focus();
541 } );
542 } );
543
544 // Install module (upload module)
545 let input = document.querySelector( "#module-upload" );
546 if ( input ) input.addEventListener( "change", async function( e ) {
547 const formData = new FormData();
548 formData.append( "fn", "install_module" );
549 for ( let i = 0; i < input.files.length; i++ ) {
550 formData.append( "myfile[]", input.files[i] );
551 }
552 let ajax = new XMLHttpRequest();
553 /*
554 ajax.upload.addEventListener( "progress", function( event ) {
555 let percent = Math.round( (event.loaded / event.total) * 100 );
556 bar.style.width = percent + "%";
557 }, false );
558
559 ajax.addEventListener( "error", function( event ) {
560 notify( _( "error_upload_file" ), "info-error", 3600000 );
561 bar.style = "";
562 }, false );
563
564 ajax.addEventListener( "abort", function( event ) {
565 notify( _( "error_upload_file" ), "info-error", 3600000 );
566 bar.style = "";
567 }, false );
568 */
569 let google_chrome_fix = this;
570 ajax.addEventListener( "load", function( event ) {
571 google_chrome_fix.value = "";
572 let r = JSON.parse( event.target.responseText );
573 if ( r.info_text ) {
574 notify( r.info_text, r.info_class, r.info_time );
575 if ( r.info_class === "info-success" ) setTimeout( function() {
576 window.location.reload( true );
577 }, r.info_time );
578 }
579 } );
580 ajax.open( "POST", cms.api );
581 ajax.send( formData );
582 } );
583
584 // БД. Открытие дополнительных настроек.
585 document.querySelectorAll( "#base .pro-btn" ).forEach( function( pro ) {
586 pro.addEventListener( "click", function( e ) {
587 document.querySelector( "#base form" ).classList.toggle( "show-pro" );
588 } );
589 } );
590
591 // Выбор секции Админского меню. Select
592 document.querySelectorAll( "#admin_menu .field-select" ).forEach( function( select ) {
593 select.addEventListener( "click", function( e ) {
594 e.stopPropagation();
595 this.parentElement.classList.toggle( "open" );
596
597 // это можно убрать если переставить стили на родителя
598 select.nextElementSibling.classList.toggle( "open" );
599 } );
600 } );
601
602 // Выбор секции Админского меню. Option
603 document.querySelectorAll( "#admin_menu .field-options .option" ).forEach( function( select ) {
604 select.addEventListener( "click", function( e ) {
605 let input = this.closest( ".section-select-grid" ).querySelector( ".field-select" );
606 input.querySelector( ".value" ).innerText = this.innerText;
607 input.setAttribute( "data-section", this.getAttribute( "value" ) );
608 } );
609 } );
610 // Select
611 // Закрытие выпадающих списков при кликах вне их, а так же по ним
612 document.body.addEventListener( "click", function( e ) {
613 document.querySelectorAll( "#admin_menu .section-select-grid" ).forEach( function( list ) {
614 list.classList.remove( "open" );
615 } );
616
617 // это можно убрать если переставить стили на родителя
618 document.querySelectorAll( "#admin_menu .field-options" ).forEach( function( list ) {
619 list.classList.remove( "open" );
620 } );
621 } );
622
623
624 /* Обновление и проверка файлов */
625
626 document.querySelectorAll( "#modules [data-fn]" ).forEach( function( button ) {
627 button.addEventListener( "click", function() {
628 let fn = this.getAttribute( "data-fn" );
629 let data = {
630 fn: fn
631 }
632 api( data, function( r ) {
633 if ( r.answer ) {
634 switch ( fn ) {
635 case "cms_update":
636 document.querySelector( "#modules .update-window" ).insertAdjacentHTML( "beforeend", r.answer );
637 break;
638 case "create_zip":
639 document.querySelector( "#modules .dev-window" ).insertAdjacentHTML( "beforeend", r.answer );
640 break;
641 case "cms_check_update":
642 case "cms_check_dev_update":
643 document.querySelector( "#modules .check-answer" ).innerHTML = r.answer;
644 break;
645 case "cms_changed_files":
646 let div = document.querySelector( "#modules .changed-files" );
647 div.innerHTML = r.answer;
648 div.style.display = "";
649 break;
650 }
651 }
652 if ( r.info_text ) {
653 notify( r.info_text, r.info_class, r.info_time );
654 }
655 if ( r.reload ) {
656 setTimeout( function() {
657 window.location.reload( true );
658 }, r.info_time );
659 }
660 } );
661 } );
662 } );
663
664 // show update from dev buttons
665 document.querySelectorAll( "#modules [data-show-dev]" ).forEach( function( btn ) {
666 btn.addEventListener( "click", function() {
667 let dev = document.querySelector( "#modules .developers_only" );
668 if ( dev ) {
669 dev.classList.remove( "developers_only" );
670 if ( window.location.host == "dev.coffee-cms.ru" ) {
671 dev.querySelector( "#modules [data-fn='create_zip']" ).removeAttribute( "style" );
672 }
673 }
674 this.remove();
675 } );
676 } );
677
678 } );