scripts.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. document.addEventListener('DOMContentLoaded', function() {
  2. tinymce.init({
  3. license_key: 'gpl',
  4. remove_script_host: false,
  5. relative_urls: false,
  6. entity_encoding: 'raw', // Записывает символы как есть
  7. valid_elements: '*[*]', // Разрешает любые элементы и атрибуты
  8. content_css: false, // Отключает внешнюю CSS-стилизацию
  9. force_br_newlines: true,
  10. //force_p_newlines: true,
  11. entities: '160,nbsp', // Мини
  12. remove_linebreaks: false,
  13. selector: '.secondary-content', // Ваши текстовые области
  14. encoding: 'UTF-8', // Указывает кодировку
  15. plugins: 'image link media code table lists', // Подключены плагины для изображений, таблиц, списков
  16. toolbar: 'undo redo | styleselect | bold italic | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent | link image media table | code', // Добавлены кнопки для списков
  17. menubar: 'file edit view insert format tools table help', // Включаем меню с таблицами
  18. contextmenu: 'table', // Контекстное меню для работы с таблицами
  19. images_upload_url: '/admin/upload.php', // URL для загрузки изображений
  20. automatic_uploads: true, // Автоматическая загрузка изображений
  21. file_picker_types: 'image', // Включаем выбор файлов
  22. images_reuse_filename: true, // Сохраняем оригинальное название файла
  23. paste_as_text: true,
  24. setup: function (editor) {
  25. // Обработка перед сохранением
  26. editor.on('SaveContent', function (e) {
  27. e.content = e.content.replace(/<img([^>]*?)src=/g, '<img$1class="lazy" data-src=');
  28. });
  29. // Обработка контента при загрузке в редактор
  30. editor.on('BeforeSetContent', function (e) {
  31. e.content = e.content.replace(/<img([^>]*?)src=/g, '<img$1class="lazy" data-src=');
  32. });
  33. },
  34. table_default_attributes: {
  35. border: '1', // Граница таблицы по умолчанию
  36. },
  37. table_default_styles: {
  38. width: '100%', // Ширина таблицы по умолчанию
  39. borderCollapse: 'collapse', // Границы объединяются
  40. },
  41. file_picker_callback: function(callback, value, meta) {
  42. // Если это изображение, открываем стандартный файловый выбор
  43. if (meta.filetype === 'image') {
  44. const input = document.createElement('input');
  45. input.setAttribute('type', 'file');
  46. input.setAttribute('accept', 'image/*');
  47. input.onchange = function() {
  48. const file = this.files[0];
  49. const formData = new FormData();
  50. formData.append('file', file);
  51. fetch('/admin/upload.php', {
  52. method: 'POST',
  53. body: formData
  54. })
  55. .then(response => response.json())
  56. .then(result => {
  57. callback(result.location); // URL изображения
  58. })
  59. .catch(() => alert('Ошибка при загрузке изображения.'));
  60. };
  61. input.click();
  62. }
  63. },
  64. content_style: `
  65. body { font-family: Arial, sans-serif; font-size: 14px; }
  66. table { border-collapse: collapse; width: 100%; }
  67. th, td { border: 1px solid #ddd; padding: 8px; }
  68. th { background-color: #f2f2f2; text-align: left; }
  69. ` // Добавлены стили для таблиц
  70. });
  71. });
  72. // Выбор цвета-------------------------
  73. document.addEventListener('DOMContentLoaded', function () {
  74. // Получаем все элементы input с классами inp-color и inp-color-text
  75. const colorPickers = document.querySelectorAll('.inp-color');
  76. const textInputs = document.querySelectorAll('.inp-color-text');
  77. // Синхронизация inp-color -> inp-color-text
  78. colorPickers.forEach(colorPicker => {
  79. colorPicker.addEventListener('input', function () {
  80. const textInput = this.nextElementSibling;
  81. if (textInput && textInput.classList.contains('inp-color-text')) {
  82. textInput.value = this.value;
  83. }
  84. });
  85. });
  86. // Синхронизация inp-color-text -> inp-color
  87. textInputs.forEach(textInput => {
  88. textInput.addEventListener('input', function () {
  89. const colorPicker = this.previousElementSibling;
  90. if (colorPicker && colorPicker.classList.contains('inp-color')) {
  91. // Проверяем, что введенное значение является валидным цветом в формате HEX
  92. if (/^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(this.value)) {
  93. colorPicker.value = this.value;
  94. }
  95. }
  96. });
  97. // Проверяем и синхронизируем значение при загрузке страницы
  98. const colorPicker = textInput.previousElementSibling;
  99. if (colorPicker && colorPicker.classList.contains('inp-color')) {
  100. // Проверяем, что значение в текстовом поле валидное
  101. if (/^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(textInput.value)) {
  102. colorPicker.value = textInput.value;
  103. }
  104. }
  105. });
  106. });
  107. // function addRepeaterItem(type) {
  108. // const container = document.getElementById(`${type}-items`);
  109. // const index = container.children.length;
  110. // const html = `
  111. // <div class="repeater-item">
  112. // <input type="text" name="items[${index}][question]" placeholder="Question">
  113. // <input type="text" name="items[${index}][answer]" placeholder="Answer">
  114. // <button type="button" onclick="this.parentElement.remove()">Remove</button>
  115. // </div>`;
  116. // container.insertAdjacentHTML('beforeend', html);
  117. // }
  118. function addCasinoItem() {
  119. const container = document.getElementById('casinoRepeater');
  120. const tpl = document.getElementById('casinoTemplate').content.cloneNode(true);
  121. container.appendChild(tpl);
  122. }
  123. function uploadImage(input, wrapper) {
  124. const file = input.files[0];
  125. const formData = new FormData();
  126. formData.append('image', file);
  127. fetch('upload_repeater_image.php', {
  128. method: 'POST',
  129. body: formData
  130. })
  131. .then(resp => resp.json())
  132. .then(data => {
  133. if (data.success) {
  134. wrapper.querySelector('.image').value = data.path;
  135. } else {
  136. alert('Ошибка загрузки изображения: ' + data.message);
  137. }
  138. });
  139. }
  140. function saveRepeater() {
  141. const items = [];
  142. document.querySelectorAll('#casinoRepeater .casino-item').forEach((item, index) => {
  143. items.push({
  144. id: item.dataset.id || null,
  145. sort_order: index,
  146. image: item.querySelector('.image').value,
  147. alt: item.querySelector('.alt').value,
  148. title: item.querySelector('.title').value,
  149. heading: item.querySelector('.heading').value,
  150. text: item.querySelector('.text').value,
  151. button: item.querySelector('.button').value,
  152. keitaro: item.querySelector('.keitaro').value,
  153. });
  154. });
  155. document.getElementById('casinoRepeaterData').value = JSON.stringify(items);
  156. return true;
  157. }
  158. // Drag and Drop functionality for casino items
  159. document.addEventListener('DOMContentLoaded', function() {
  160. const container = document.getElementById('casinoRepeater');
  161. if (!container) return;
  162. let draggedElement = null;
  163. let placeholder = null;
  164. function updateOrderNumbers() {
  165. const items = container.querySelectorAll('.casino-item');
  166. items.forEach((item, index) => {
  167. const numberElement = item.querySelector('.casino-order-number');
  168. if (numberElement) {
  169. numberElement.textContent = index + 1;
  170. }
  171. });
  172. }
  173. function createPlaceholder() {
  174. const div = document.createElement('div');
  175. div.className = 'drag-placeholder';
  176. div.style.cssText = 'height: 60px; margin: 10px 0; border: 2px dashed #007bff; background: #f0f8ff; opacity: 0.5;';
  177. return div;
  178. }
  179. // Accordion functionality
  180. function initAccordion() {
  181. const items = container.querySelectorAll('.casino-item');
  182. items.forEach(item => {
  183. const header = item.querySelector('.casino-item-header');
  184. const toggleBtn = item.querySelector('.toggle-accordion');
  185. const content = item.querySelector('.casino-item-content');
  186. const dragHandle = item.querySelector('.drag-handle');
  187. if (!header || !toggleBtn || !content) return;
  188. // Toggle accordion
  189. const toggleAccordion = (e) => {
  190. // Не закрывать если клик по drag handle
  191. if (e.target.closest('.drag-handle')) {
  192. return;
  193. }
  194. const isOpen = content.style.display !== 'none' && content.style.display !== '';
  195. if (isOpen) {
  196. content.style.display = 'none';
  197. toggleBtn.textContent = '▼';
  198. } else {
  199. content.style.display = 'flex';
  200. toggleBtn.textContent = '▲';
  201. }
  202. };
  203. header.addEventListener('click', toggleAccordion);
  204. // Запрет на перетаскивание при клике на header (кроме drag-handle)
  205. header.addEventListener('mousedown', (e) => {
  206. if (!e.target.closest('.drag-handle')) {
  207. item.setAttribute('draggable', 'false');
  208. }
  209. });
  210. header.addEventListener('mouseup', () => {
  211. item.setAttribute('draggable', 'true');
  212. });
  213. // Drag только за handle
  214. dragHandle.addEventListener('mousedown', (e) => {
  215. e.stopPropagation();
  216. item.setAttribute('draggable', 'true');
  217. });
  218. // Обновление заголовка при изменении Casino title
  219. const headingInput = item.querySelector('.heading');
  220. if (headingInput) {
  221. headingInput.addEventListener('input', function() {
  222. const titleElement = item.querySelector('.casino-item-title');
  223. if (titleElement) {
  224. titleElement.textContent = this.value || 'New Casino Item';
  225. }
  226. });
  227. }
  228. });
  229. }
  230. function initDragAndDrop() {
  231. const items = container.querySelectorAll('.casino-item');
  232. items.forEach(item => {
  233. item.setAttribute('draggable', 'true');
  234. const dragHandle = item.querySelector('.drag-handle');
  235. // Drag только при клике на handle
  236. dragHandle.addEventListener('mousedown', function(e) {
  237. item.setAttribute('draggable', 'true');
  238. });
  239. item.addEventListener('dragstart', function(e) {
  240. // Проверяем что перетаскивание началось с drag-handle
  241. if (!e.target.querySelector('.drag-handle')) {
  242. e.preventDefault();
  243. return;
  244. }
  245. draggedElement = this;
  246. this.classList.add('dragging');
  247. e.dataTransfer.effectAllowed = 'move';
  248. e.dataTransfer.setData('text/html', '');
  249. setTimeout(() => {
  250. this.style.display = 'none';
  251. }, 0);
  252. });
  253. item.addEventListener('dragend', function(e) {
  254. this.style.display = '';
  255. this.classList.remove('dragging');
  256. if (placeholder && placeholder.parentNode) {
  257. placeholder.parentNode.removeChild(placeholder);
  258. }
  259. placeholder = null;
  260. draggedElement = null;
  261. updateOrderNumbers();
  262. });
  263. });
  264. }
  265. container.addEventListener('dragover', function(e) {
  266. e.preventDefault();
  267. e.dataTransfer.dropEffect = 'move';
  268. if (!draggedElement) return;
  269. const afterElement = getDragAfterElement(container, e.clientY);
  270. const currentItems = [...container.querySelectorAll('.casino-item:not(.dragging)')];
  271. if (!placeholder) {
  272. placeholder = createPlaceholder();
  273. }
  274. if (afterElement == null) {
  275. container.appendChild(placeholder);
  276. } else {
  277. container.insertBefore(placeholder, afterElement);
  278. }
  279. });
  280. container.addEventListener('drop', function(e) {
  281. e.preventDefault();
  282. if (!draggedElement || !placeholder) return;
  283. if (placeholder.parentNode) {
  284. placeholder.parentNode.insertBefore(draggedElement, placeholder);
  285. placeholder.parentNode.removeChild(placeholder);
  286. }
  287. draggedElement.style.display = '';
  288. placeholder = null;
  289. });
  290. function getDragAfterElement(container, y) {
  291. const draggableElements = [...container.querySelectorAll('.casino-item:not(.dragging)')];
  292. return draggableElements.reduce((closest, child) => {
  293. const box = child.getBoundingClientRect();
  294. const offset = y - box.top - box.height / 2;
  295. if (offset < 0 && offset > closest.offset) {
  296. return { offset: offset, element: child };
  297. } else {
  298. return closest;
  299. }
  300. }, { offset: Number.NEGATIVE_INFINITY }).element;
  301. }
  302. // Инициализация при загрузке
  303. initAccordion();
  304. initDragAndDrop();
  305. updateOrderNumbers();
  306. // Слушаем событие удаления элемента
  307. container.addEventListener('itemRemoved', function() {
  308. updateOrderNumbers();
  309. });
  310. // Переопределяем addCasinoItem для инициализации drag&drop на новых элементах
  311. window.originalAddCasinoItem = window.addCasinoItem;
  312. window.addCasinoItem = function() {
  313. if (window.originalAddCasinoItem) {
  314. window.originalAddCasinoItem();
  315. } else {
  316. const container = document.getElementById('casinoRepeater');
  317. const tpl = document.getElementById('casinoTemplate').content.cloneNode(true);
  318. container.appendChild(tpl);
  319. }
  320. setTimeout(() => {
  321. initAccordion();
  322. initDragAndDrop();
  323. updateOrderNumbers();
  324. }, 10);
  325. };
  326. });
  327. const pageSelect = document.getElementById('page-select');
  328. if (pageSelect) {
  329. pageSelect.addEventListener('change', function () {
  330. const selected = this.value;
  331. const blocks = document.querySelectorAll('.page-block');
  332. blocks.forEach(block => {
  333. if (block.classList.contains(selected)) {
  334. block.style.display = 'block';
  335. } else {
  336. block.style.display = 'none';
  337. }
  338. });
  339. });
  340. }
  341. // Tab switching functionality for new admin interface
  342. function switchTab(tabName) {
  343. // Hide all tab contents
  344. const contents = document.querySelectorAll('.tab-content');
  345. contents.forEach(content => content.classList.remove('active'));
  346. // Remove active class from all tabs
  347. const tabs = document.querySelectorAll('.tab');
  348. tabs.forEach(tab => tab.classList.remove('active'));
  349. // Show selected tab content
  350. document.getElementById(tabName + '-tab').classList.add('active');
  351. // Add active class to clicked tab
  352. event.target.classList.add('active');
  353. }
  354. // Repeater functionality for new admin interface
  355. function addRepeaterItem(type) {
  356. const container = document.getElementById(`${type}-items`);
  357. const index = container.children.length;
  358. const html = `
  359. <div class="repeater-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd;">
  360. ${type === 'menu' ? `
  361. <input type="text" name="repeaters[${type}][${index}][title]" placeholder="Menu Title" style="width: 100%; margin-bottom: 10px;">
  362. <input type="text" name="repeaters[${type}][${index}][anchor]" placeholder="Anchor Link (#id)" style="width: 100%; margin-bottom: 10px;">
  363. ` : `
  364. <input type="text" name="repeaters[${type}][${index}][question]" placeholder="Question" style="width: 100%; margin-bottom: 10px;">
  365. <textarea name="repeaters[${type}][${index}][answer]" placeholder="Answer" style="width: 100%; margin-bottom: 10px;"></textarea>
  366. `}
  367. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 5px 10px;">Remove</button>
  368. </div>`;
  369. container.insertAdjacentHTML('beforeend', html);
  370. }
  371. function removeRepeaterItem(button) {
  372. button.parentElement.remove();
  373. }
  374. function addCasinoItem() {
  375. const container = document.getElementById('casino-items');
  376. const index = container.children.length;
  377. const html = `
  378. <div class="casino-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid #ddd;">
  379. <h4>Casino Item #${index + 1}</h4>
  380. <input type="text" name="repeaters[casino_list][${index}][heading]" placeholder="Casino Name" style="width: 100%; margin-bottom: 10px;">
  381. <input type="text" name="repeaters[casino_list][${index}][text]" placeholder="Bonus Text" style="width: 100%; margin-bottom: 10px;">
  382. <input type="text" name="repeaters[casino_list][${index}][button]" placeholder="Button Text" style="width: 100%; margin-bottom: 10px;">
  383. <input type="file" name="casino_image_${index}" style="margin-bottom: 10px;">
  384. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 5px 10px;">Remove</button>
  385. </div>`;
  386. container.insertAdjacentHTML('beforeend', html);
  387. }
  388. function addHreflangItem() {
  389. const container = document.getElementById('hreflang-items');
  390. const index = container.children.length;
  391. const html = `
  392. <div style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd;">
  393. <div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 15px; align-items: center;">
  394. <div>
  395. <label style="display: block; margin-bottom: 5px; font-weight: bold;">Hreflang</label>
  396. <input type="text" name="seo[hreflang][${index}][hreflang]" placeholder="en, es, fr, etc." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
  397. </div>
  398. <div>
  399. <label style="display: block; margin-bottom: 5px; font-weight: bold;">Href URL</label>
  400. <input type="text" name="seo[hreflang][${index}][href]" placeholder="https://example.com/en/" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
  401. </div>
  402. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 8px 12px; border-radius: 4px; margin-top: 25px;">Remove</button>
  403. </div>
  404. </div>`;
  405. container.insertAdjacentHTML('beforeend', html);
  406. }
  407. // Load existing data for new admin interface
  408. function loadExistingData() {
  409. const menuItems = window.menuItems || [];
  410. const faqItems = window.faqItems || [];
  411. const casinoItems = window.casinoItems || [];
  412. const hreflangItems = window.hreflangItems || [];
  413. // Load existing menu items
  414. menuItems.forEach((item, index) => {
  415. if (item && item.title) {
  416. const container = document.getElementById('menu-items');
  417. if (container) {
  418. const html = `
  419. <div class="repeater-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd;">
  420. <input type="text" name="repeaters[menu][${index}][title]" value="${item.title}" placeholder="Menu Title" style="width: 100%; margin-bottom: 10px;">
  421. <input type="text" name="repeaters[menu][${index}][anchor]" value="${item.anchor || ''}" placeholder="Anchor Link (#id)" style="width: 100%; margin-bottom: 10px;">
  422. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 5px 10px;">Remove</button>
  423. </div>`;
  424. container.insertAdjacentHTML('beforeend', html);
  425. }
  426. }
  427. });
  428. // Load existing FAQ items
  429. faqItems.forEach((item, index) => {
  430. if (item && item.question) {
  431. const container = document.getElementById('faq-items');
  432. if (container) {
  433. const html = `
  434. <div class="repeater-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd;">
  435. <input type="text" name="repeaters[faq][${index}][question]" value="${item.question}" placeholder="Question" style="width: 100%; margin-bottom: 10px;">
  436. <textarea name="repeaters[faq][${index}][answer]" placeholder="Answer" style="width: 100%; margin-bottom: 10px;">${item.answer || ''}</textarea>
  437. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 5px 10px;">Remove</button>
  438. </div>`;
  439. container.insertAdjacentHTML('beforeend', html);
  440. }
  441. }
  442. });
  443. // Load existing casino items
  444. casinoItems.forEach((item, index) => {
  445. if (item && item.heading) {
  446. const container = document.getElementById('casino-items');
  447. if (container) {
  448. const html = `
  449. <div class="casino-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid #ddd;">
  450. <h4>${item.heading}</h4>
  451. <input type="text" name="repeaters[casino_list][${index}][heading]" value="${item.heading}" placeholder="Casino Name" style="width: 100%; margin-bottom: 10px;">
  452. <input type="text" name="repeaters[casino_list][${index}][text]" value="${item.text || ''}" placeholder="Bonus Text" style="width: 100%; margin-bottom: 10px;">
  453. <input type="text" name="repeaters[casino_list][${index}][button]" value="${item.button || ''}" placeholder="Button Text" style="width: 100%; margin-bottom: 10px;">
  454. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 5px 10px;">Remove</button>
  455. </div>`;
  456. container.insertAdjacentHTML('beforeend', html);
  457. }
  458. }
  459. });
  460. // Load existing hreflang items
  461. hreflangItems.forEach((item, index) => {
  462. if (item && (item.hreflang || item.href)) {
  463. const container = document.getElementById('hreflang-items');
  464. if (container) {
  465. const html = `
  466. <div style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd;">
  467. <div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 15px; align-items: center;">
  468. <div>
  469. <label style="display: block; margin-bottom: 5px; font-weight: bold;">Hreflang</label>
  470. <input type="text" name="seo[hreflang][${index}][hreflang]" value="${item.hreflang || ''}" placeholder="en, es, fr, etc." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
  471. </div>
  472. <div>
  473. <label style="display: block; margin-bottom: 5px; font-weight: bold;">Href URL</label>
  474. <input type="text" name="seo[hreflang][${index}][href]" value="${item.href || ''}" placeholder="https://example.com/en/" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
  475. </div>
  476. <button type="button" onclick="removeRepeaterItem(this)" style="background: #dc3545; color: white; border: none; padding: 8px 12px; border-radius: 4px; margin-top: 25px;">Remove</button>
  477. </div>
  478. </div>`;
  479. container.insertAdjacentHTML('beforeend', html);
  480. }
  481. }
  482. });
  483. }
  484. // Initialize on DOM ready (for new admin interface)
  485. if (document.readyState === 'loading') {
  486. document.addEventListener('DOMContentLoaded', loadExistingData);
  487. } else {
  488. loadExistingData();
  489. }