1/*jshint esversion: 11 */ 2 3// form validation 4window.addEventListener('DOMContentLoaded', function() { 5 // enable file upload 6 document.querySelectorAll('form.validate').forEach(function(form) { 7 /* 8 const fileInputs = form.querySelectorAll('*[type="file"], *[type="files"]'); 9 if (fileInputs.length) { 10 } 11 */ 12 13 form.setAttribute('enctype', 'multipart/form-data'); 14 }); 15 16 const validateForms = document.querySelectorAll('form.validate, form[sl-validate]'); 17 18 let submitHandler = async function (form, validate) { 19 // Disable the buttons 20 const submitButtons = form.querySelectorAll('*[type="submit"], *[type="image"]'); 21 submitButtons.forEach(button => button.disabled = true); 22 23 // Remove error messages 24 const errorDivs = form.querySelectorAll('div.sl-error'); 25 errorDivs.forEach(div => div.remove()); 26 27 let url = form.action ? form.action: location.href; 28 let formData; 29 let headers = {}; 30 31 if (validate) { 32 formData = new URLSearchParams(); 33 for (const pair of new FormData(form)) { 34 let value = pair[1] instanceof File ? pair[1].name: pair[1]; 35 formData.append(pair[0], value); 36 } 37 38 if (validate) { 39 formData.append('validate', 1); 40 } 41 headers.contentType = 'application/x-www-form-urlencoded' 42 } else { 43 formData = new FormData(form); 44 } 45 46 formData.append('nospam', 1); 47 48 // validate 49 let response = await fetch(url, { 50 method: form.method, 51 body: formData, 52 headers: headers 53 }); 54 55 let data = await response.json(); 56 57 if (!data.success && data !== 1) { 58 // display errors 59 let errors = data.errors ? data.errors: data; 60 let otherErrors = ''; 61 62 errors?.forEach(function(item) { 63 let pos = item.indexOf(' '); 64 let fieldName = item; 65 66 if (pos === -1) { 67 error = 'required'; 68 } else { 69 fieldName = item.substring(0, pos); 70 error = item.substring(pos + 1); 71 } 72 73 let parent = ''; 74 let fieldInput = form[fieldName + '[]'] ? form[fieldName + '[]']: form[fieldName]; 75 76 if (fieldInput) { 77 if (fieldInput.style) { 78 parent = fieldInput.parentNode; 79 } else if (fieldInput[0]) { 80 parent = fieldInput[0].parentNode.parentNode; 81 } 82 } else { 83 otherErrors += "\n" + item; 84 } 85 86 if (parent) { 87 div = document.createElement("div"); 88 div.innerHTML = error; 89 div.style.color = 'red'; 90 div.className = 'sl-error'; 91 parent.appendChild(div); 92 } 93 }) 94 95 if (otherErrors) { 96 alert('Check the following:' + otherErrors); 97 } 98 99 // scroll to first error 100 const firstError = document.querySelector('.sl-error'); 101 if (firstError) { 102 firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); 103 } 104 105 } else { 106 const recaptchaSiteKey = document.querySelector('#recaptcha')?.dataset?.key; 107 108 if (recaptchaSiteKey) { 109 await grecaptcha.ready(); 110 const token = await grecaptcha.execute(recaptchaSiteKey, { 111 action: 'submit' 112 }); 113 114 const recaptchaTextarea = form.querySelector('.recaptcha') || document.createElement('textarea'); 115 116 if (!recaptchaTextarea.classList.contains('recaptcha')) { 117 recaptchaTextarea.style.display = 'none'; 118 recaptchaTextarea.classList.add('recaptcha'); 119 recaptchaTextarea.name = 'g-recaptcha-response'; 120 form.appendChild(recaptchaTextarea); 121 } 122 123 recaptchaTextarea.value = token; 124 } 125 126 // submit again for reals 127 if (validate) { 128 // backcompat 129 if (form.classList.contains('validate')) { 130 form.submit(); 131 } else { 132 submitHandler(form); 133 } 134 } else { 135 // handle redirect 136 if (data.redirect) { 137 location.href = data.redirect; 138 } 139 140 // handle hide 141 if (form.hasAttribute('sl-hide')) { 142 form.style.display = 'none'; 143 } 144 145 // handle target template 146 if (form.hasAttribute('sl-target')) { 147 let el = document.querySelector(form.getAttribute('sl-target')); 148 if (!el.dataset.template) { 149 el.dataset.template = el.innerHTML; 150 } 151 152 const html = Handlebars.compile(el.dataset.template)(data.data); 153 el.innerHTML = html; 154 el.style.display = ''; 155 } 156 } 157 } 158 159 // re-enable submit 160 submitButtons.forEach(button => button.disabled = false); 161 } 162 163 // enable validation
164 validateForms.forEach(form => { 165 form.addEventListener('submit', function(event) { 166 // Your form validation logic goes here 167 event.preventDefault(); // Prevent default form submission 168 submitHandler(this, true); 169 }); 170 }); 171}); 172 173// inline cms 174class PageEditor { 175 constructor() { 176 let pageDataEl = document.getElementById('pageData'); 177 this.pageData = pageDataEl ? JSON.parse(pageDataEl.textContent) : {}; 178 179 let editable = document.querySelectorAll('[sl-id], [sl-name], [sl-block]'); 180 181 if (!editable.length && !this.pageData?.catcher) { 182 return; 183 } 184 185 const nav = document.createElement("div"); 186 nav.classList.add('sl-nav'); 187 nav.innerHTML = ` 188 <button type="button" class="icon" sl-addpage style="display: none;"> 189 + 190 </button> 191 192 <button type="button" class="icon" sl-editPage> 193 🔧 194 </button> 195 196 <button type="button" class="icon" sl-cancelPage style="display: none;"> 197 <div style="transform: rotate(180deg);">➜</div> 198 </button> 199 200 <button type="button" class="icon" sl-renamePage style="display: none;"> 201 ✎ 202 </button> 203 204 <button type="button" class="icon" sl-deletePage style="display: none;"> 205 🗑 206 </button> 207 208 <button type="button" class="icon" sl-savePage disabled style="display: none;"> 209 💾 210 </button> 211 212 <button type="button" class="icon" sl-publish style="display: none;"> 213 Publish 214 </button> 215 216 <button type="button" class="icon" sl-pageSettings style="display: none;"> 217 ⚙ 218 </button> 219 220 <div sl-uploads style="display: none;"></div> 221 <div sl-settings style="display: none;"> 222 <label style="width: 100%; padding: 10px; display: block;"> 223 <p>Page title</p> 224 <input type="text" name="page_title" placeholder="Page title" style="width: 100%;"> 225 </label> 226 <label style="width: 100%; padding: 10px; display: block;"> 227 <p>Meta description</p> 228 <input type="text" name="meta_description" placeholder="Meta description" style="width: 100%;"> 229 </label> 230 </div> 231 `; 232 document.body.appendChild(nav); 233 234 const style = document.createElement("style"); 235 style.textContent = ` 236 .sl-nav { 237 position: fixed; 238 top: 0; 239 left: 0; 240 width: 100%; 241 z-index: 10000; 242 padding: 5px; 243 } 244 245 [sl-editing] { 246 margin-top: 60px; 247 } 248 249 [sl-editing] .sl-nav { 250 background: #000; 251 color: #fff; 252 } 253 254 .sl-nav button { 255 border: none; 256 text-align: center; 257 text-decoration: none; 258 display: inline-block; 259 font-size: 16px; 260 padding: 16px; 261 border-radius: 50px; 262 opacity: 0.9; 263 } 264 265 .sl-nav button:not([disabled]):hover { 266 opacity: 1; 267 background: #666 !important; 268 } 269 270 .sl-nav button[disabled] { 271 opacity: 0.5; 272 } 273 274 .sl-nav button.icon { 275 background: transparent; color: #fff; 276 } 277 278 .mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before { 279 color: #ccc !important; 280 } 281 282 *[contentEditable="true"]:focus, 283 *[contentEditable="true"]:hover { 284 outline: 2px solid #1976D2; 285 } 286 287 *[sl-block], *[sl-type] { 288 position: relative; 289 } 290 291 *[sl-block]:focus, 292 *[sl-block]:hover, 293 [sl-editing] div[sl-type="upload"]:hover { 294 border: 2px solid #1976D2; 295 } 296 297 [sl-editing] div[sl-type="upload"] { 298 min-height: 100px; 299 min-width: 100px; 300 } 301 302 [sl-editing] [sl-cancelpage], 303 [sl-editing] [sl-savePage], 304 [sl-editing] [sl-pageSettings] 305 { 306 display: inline-block !important; 307 } 308 309 .sl-nav button.icon[sl-editpage] { 310 background-color: #000; 311 position: fixed; 312 bottom: 10px; 313 left: 10px; 314 } 315 316 [sl-editing] [sl-editpage] { 317 display: none !important; 318 } 319 320 *[sl-block] .sl-menu { 321 position: absolute; 322 top: 0; 323 } 324 325 .sl-menu, .sl-choose { 326 display: none; 327 } 328 329 *[sl-block]:hover .sl-menu, 330 div[sl-type]:hover .sl-choose { 331 display: block; 332 } 333 334 .sl-choose { 335 position: absolute; 336 top: 50%; 337 left: 50%; 338 transform: translate(-50%, -50%) 339 } 340 341 *[sl-uploads] { 342 position: fixed;
342 /* Stay in place even when content scrolls */ 343 top: 20px; /* Margin from the top */ 344 left: 20px; /* Margin from the left */ 345 right: 20px; /* Margin from the right */ 346 bottom: 20px; /* Margin from the bottom */ 347 background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent black background */ 348 width: calc(100% - 40px); /* Account for left and right margins */ 349 height: calc(100vh - 40px); /* Account for top and bottom margins and viewport height (vh) */ 350 z-index: 1000; 351 overflow: auto; 352 } 353 354 *[sl-uploads] img { 355 margin: 10px; 356 } 357 358 *[sl-uploads] .folder { 359 margin: 10px; 360 cursor: pointer; 361 } 362 363 *[sl-block]:first-child button[sl-move-up] { 364 display: none; 365 } 366 367 button[sl-move-down] { 368 transform: rotate(90deg); 369 padding-bottom: 3px; 370 } 371 372 button[sl-move-up] { 373 transform: rotate(-90deg); 374 padding-bottom: 3px; 375 } 376 377 *[sl-block]:last-child button[sl-move-down] { 378 display: none; 379 } 380 381 /* carousel fix */ 382 .cdxcarousel-item { 383 min-height: 235px; 384 height: auto; 385 } 386 `; 387 document.head.appendChild(style); 388 389 this.addPageButton = document.querySelector('[sl-addpage]'); 390 this.editPageButton = document.querySelector('[sl-editpage]'); 391 this.cancelButton = document.querySelector('[sl-cancelpage]'); 392 this.renamePageButton = document.querySelector('[sl-renamepage]'); 393 this.deletePageButton = document.querySelector('[sl-deletepage]'); 394 this.savePageButton = document.querySelector('[sl-savepage]'); 395 this.publishPageButton = document.querySelector('[sl-publish]'); 396 this.pageSettingsButton = document.querySelector('[sl-pageSettings]'); 397 this.settingsEl = document.querySelector('[sl-settings]'); 398 this.pageTitleEl = document.querySelector('[sl-settings] [name=page_title]'); 399 this.metaDescriptionEl = document.querySelector('[sl-settings] [name=meta_description]'); 400 401 this.editors = []; 402 this.blockEditors = []; 403 this.dirty = false; 404 405 // button handlers 406 this.addPageButton.addEventListener('click', 407 async () => { 408 let name = prompt('Enter a page name'); 409 410 if (name) { 411 let formData = new FormData(); 412 formData.append('name', name); 413 414 let pageName = this.pageData.base + '/' + this.strToPageName(name); 415 416 formData.append('page_name', pageName); 417 418 let url = '/_lib/api/v2/?cmd=save§ion=cms%20pages'; 419 let response = await fetch(url, { 420 method: 'post', 421 body: formData 422 }); 423 424 let data = await response.json(); 425 426 if (data.error) { 427 alert(data.error) 428 } else { 429 // redirect to page 430 if (pageName[0] != '/') { 431 pageName = '/' + pageName 432 } 433 434 location.href = pageName; 435 } 436 } 437 }) 438 439 this.cancelButton.addEventListener('click', () => { 440 document.querySelector('body').removeAttribute('sl-editing'); 441 this.editPageButton.disabled = false; 442 443 this.addPageButton.style.display = "none"; 444 this.renamePageButton.style.display = "none"; 445 this.deletePageButton.style.display = "none"; 446 this.publishPageButton.style.display = "none"; 447 448 // destroy tinymces 449 this.editors.forEach(editor => { 450 editor.remove(); 451 }); 452 this.editors = []; 453 454 // destroy editorjs 455 this.blockEditors.forEach(editor => { 456 console.log(editor) 457 editor.destroy(); 458 }); 459 this.blockEditors = []; 460 461 // remove elements 462 document.querySelectorAll('.sl-choose, .sl-menu').forEach(node => { 463 node.remove(); 464 }); 465 466 // unregister event listeners 467 document.body.removeEventListener('click', e => this.clickHandler(e)); 468 window.removeEventListener('beforeunload', this.checkChanges); 469 this.cancelButton.disabled = true; 470 471 this.editData = {}; 472 473 this.loadSavePoint(); 474 475 // needed for editorjs 476 location.reload(); 477 }); 478 this.cancelButton.disabled = true; 479 480 this.editPageButton.addEventListener('click', 481 async () => { 482 document.querySelector('body').setAttribute('sl-editing', true); 483 484 this.editPageButton.disabled = true; 485 486 if (this.pageData.catcher) { 487 this.addPageButton.style.display = ""; 488 489 if (this.pageData?.page?.name) { 490 this.renamePageButton.style.display = ""; 491 this.deletePageButton.style.display = ""; 492 this.publishPageButton.style.display = ""; 493 } 494 } 495 496 let textConfig = { 497 menubar: false, 498 inline: true, 499 toolbar: 'undo redo', 500 valid_elements: '', 501 valid_styles: {}, 502 license_key: 'gpl', 503 setup: (editor) => { 504 editor.on("change", e => { this.editorChangeHandler(e, editor); }); 505 }, 506 }; 507 508 let headingConfig = { 509 menubar: false, 510 inline: true, 511 plugins: [ 512 'lists', 513 'autolink' 514 ], 515 toolbar: 'undo redo | bold italic underline', 516 valid_elements: 'strong,em,span[style],a[href]', 517 valid_styles: { 518 '*': 'font-size,font-family,color,text-decoration,text-align' 519 }, 520 license_key: 'gpl', 521 setup: (editor) => { 522 editor.on("change", e => { this.editorChangeHandler(e, editor); }); 523 }, 524 }; 525 526 const copyConfig = { 527 menubar: false, 528 inline: true, 529 plugins: [ 530 'link', 531 'lists', 532 'autolink', 533 'image', 534 ], 535 toolbar: [ 536 'undo redo | bold italic underline | fontfamily fontsize', 537 'forecolor backcolor | alignleft aligncenter alignright al
537ignfull | numlist bullist outdent indent | hr link image forecolor backcolor' 538 ], 539 valid_elements: 'p[style],strong,em,span[style],a[href],ul,ol,li,img[longdesc|usemap|src|border|alt=|title|hspace|vspace|width|height|align]', 540 valid_styles: { 541 '*': 'font-size,font-family,color,text-decoration,text-align' 542 }, 543 file_picker_callback: cb => { 544 this.chooseImage(cb); 545 }, 546 license_key: 'gpl', 547 setup: (editor) => { 548 editor.on("change", e => { this.editorChangeHandler(e, editor); }); 549 }, 550 relative_urls: false, 551 remove_script_host: true 552 }; 553 554 document.querySelectorAll('[sl-type="text"]').forEach(async node => { 555 textConfig.target = node; 556 textConfig.placeholder = this.getFieldName(node); 557 let editor = await tinymce.init({ 558 ...textConfig 559 }); 560 this.editors.push(editor[0]); 561 }); 562 563 document.querySelectorAll('[sl-type="heading"]').forEach(async node => { 564 headingConfig.target = node; 565 headingConfig.placeholder = this.getFieldName(node); 566 let editor = await tinymce.init({ 567 ...headingConfig 568 }); 569 this.editors.push(editor[0]); 570 }); 571 572 document.querySelectorAll('[sl-type="editor"]').forEach(async node => { 573 copyConfig.target = node; 574 copyConfig.placeholder = this.getFieldName(node); 575 let editor = await tinymce.init({ 576 ...copyConfig 577 }); 578 this.editors.push(editor[0]); 579 }); 580 581 document.querySelectorAll('[sl-type="block"]').forEach(async node => { 582 let fieldName = this.getFieldName(node); 583 584 node.innerHTML = ''; 585 586 let tools = { 587 header: window.Header, 588 list: window.List, 589 image: { 590 class: window.ImageTool, 591 config: { 592 endpoints: { 593 byFile: '/_lib/api/v2/?cmd=uploads', 594 } 595 } 596 }, 597 carousel: { 598 class: window.Carousel, 599 config: { 600 endpoints: { 601 byFile: '/_lib/api/v2/?cmd=uploads', 602 } 603 } 604 }, 605 }; 606 607 this.pageData.block_tools.forEach(item => { 608 tools[item.name.toLowerCase()] = { 609 class: window[item.name], 610 config: item.config 611 } 612 }); 613 614 let blockEditor = new EditorJS({ 615 holder: node, 616 tools: tools, 617 data: this.meta[fieldName] ? this.meta[fieldName] : {}, 618 onChange: (editor) => { 619 editor.saver.save().then(async (outputData) => { 620 this.setValue(fieldName, outputData, this.meta); 621 }).catch((error) => { 622 console.log('Saving failed: ', error) 623 }); 624 } 625 }); 626 627 this.blockEditors.push(blockEditor); 628 }); 629 630 // image uploads 631 this.uploadsEl = document.querySelector('[sl-uploads]'); 632 633 this.uploadCallback = null; 634 635 document.querySelectorAll('[sl-type="upload"]').forEach(node => { 636 node.style.position = 'relative'; 637 638 const buttonContainer = document.createElement("div"); 639 buttonContainer.classList.add('sl-choose'); 640 node.appendChild(buttonContainer); 641
642 const chooseButton = document.createElement("button"); 643 chooseButton.innerText = 'Choose'; 644 645 chooseButton.addEventListener('click', e => { 646 this.chooseImage(node); 647 e.stopPropagation(); 648 e.preventDefault(); 649 }) 650 651 buttonContainer.appendChild(chooseButton); 652 653 const clearButton = document.createElement("button"); 654 clearButton.innerHTML = '✕'; 655 656 clearButton.addEventListener('click', e => { 657 let imgEl = node.querySelector('img'); 658 if (!imgEl) { 659 return; 660 } 661 662 // change child image 663 imgEl.src = ''; 664 665 // change meta value 666 let name = node.getAttribute('sl-name'); 667 this.setValue(name, '', this.meta); 668 }) 669 670 buttonContainer.appendChild(clearButton); 671 }); 672 673 // blocks 674 document.querySelectorAll('[sl-name]').forEach(node => { 675 let name = node.getAttribute('sl-name'); 676 if (name.endsWith(']')) { 677 node.setAttribute('sl-block', true); 678 679 const menu = document.createElement("div"); 680 menu.classList.add('sl-menu'); 681 menu.innerHTML = ` 682 <button type="button" sl-move-up>➜</button> 683 <button type="button" sl-move-down>➜</button> 684 <button type="button" sl-duplicate-element style="font-weight: bold;">+</button> 685 <button type="button" sl-delete-element>🗑</button> 686 `; 687 688 node.appendChild(menu); 689 } 690 }); 691 692 this.pageTitleEl.value = this.meta.page_title ? this.meta.page_title : ''; 693 this.metaDescriptionEl.value = this.meta.meta_description ? this.meta.meta_description : ''; 694 695 document.body.addEventListener('click', e => this.clickHandler(e)); 696 697 window.addEventListener('beforeunload', e => this.checkChanges(e)); 698 699 this.cancelButton.disabled = false; 700 701 this.setSavePoint(); 702 }) 703 704 this.pageTitleEl.addEventListener('change', async (e) => { 705 this.setValue('page_title', e.target.value, this.meta); 706 }); 707 708 this.metaDescriptionEl.addEventListener('change', async (e) => { 709 this.setValue('meta_description', e.target.value, this.meta); 710 }); 711 712 this.renamePageButton.addEventListener('click', 713 async () => { 714 let name = prompt('Enter a page name', this.pageData.page.name); 715 716 if (name) { 717 let formData = new FormData(); 718 formData.append('name', name); 719 720 let pageName = this.pageData.base + '/' + this.strToPageName(name); 721 722 formData.append('page_name', pageName); 723 724 let params = new URLSearchParams(); 725 params.append('cmd', 'save'); 726 params.append('section', 'cms pages'); 727 params.append('id', this.pageData.page.id); 728 params.append('fields[]', 'page_name'); 729 params.append('fields[]', 'name'); 730 731 let url = '/_lib/api/v2/?' + params.toString(); 732 let response = await fetch(url, { 733 method: 'post', 734 body: formData 735 }); 736 737 let data = await response.json(); 738 739 console.log(data) 740 741 if (data.error) { 742 alert(data.error) 743 } else { 744 // redirect to page 745 location.href = '/' + pageName; 746 } 747 } 748 }) 749 750 this.publishPageButton.addEventListener('click', 751 async () => { 752 let confirmed = confirm(this.publishPageButton.innerText + '?'); 753 754 if (!confirmed) { 755 return; 756 } 757 758 let formData = new FormData(); 759 formData.append('published', parseInt(this.pageData.page.published) ? 0: 1); 760 761 let params = new URLSearchParams(); 762 params.append('cmd', 'save'); 763 params.append('section', 'cms pages'); 764 params.append('id', this.pageData.page.id); 765 params.append('fields[]', 'published'); 766 767 let url = '/_lib/api/v2/?' + params.toString(); 768 let response = await fetch(url, { 769 method: 'post', 770 body: formData 771 }); 772 773 let data = await response.json(); 774 775 console.log(data) 776 777 if (data.error) { 778 alert(data.error) 779 } else { 780 // redirect to page 781 location.reload(); 782 } 783 }) 784 785 this.deletePageButton.addEventListener('click', 786 async () => { 787 let result = confirm('Delete the page?'); 788 789 if (result) { 790 let url = '/_lib/api/v2/?cmd=delete§ion=cms%20pages'; 791 792 let formData = new FormData(); 793 formData.append('ids[]', this.pageData.page.id); 794 795 let response = await fetch(url, { 796 method: 'post', 797 body: formData 798 }); 799 800 let data = await response.json(); 801 802 if (data.error) { 803 alert(data.error) 804 } else { 805 // redirect to page 806 location.href = '/' + this.pageData.base; 807 } 808 } 809 }) 810 811 this.savePageButton.addEventListener('click', 812 async () => { 813 let result; 814 815 for (const [section, value] of Object.entries(this.editData)) { 816 if (Array.isArray(value)) { 817 value.forEach(async (item, id) => { 818 result = await this.saveData(section, id, item); 819 }); 820 } else { 821 result = await this.saveData(section, 1, value); 822 } 823 } 824 825 if (this.meta) { 826 let formData = new FormData(); 827 828 for (const [name, value] of Object.entries(this.pageData.page)) { 829 if (name !== 'meta') { 830 formData.append(name, value); 831 } 832 } 833 834 formData.append('meta', JSON.stringify(this.meta)); 835 836 let url = '/_lib/api/v2/?cmd=save§ion=cms%20pages&id='
836+ this.pageData.page?.id; 837 let response = await fetch(url, { 838 method: 'post', 839 body: formData 840 }); 841 842 let data = await response.json(); 843 844 if (data.error) { 845 alert(data.error) 846 return; 847 } 848 } 849 850 if (result !== false) 851 this.setDirty(false); 852 853 this.setSavePoint(); 854 }) 855 856 this.pageSettingsButton.addEventListener('click', () => { 857 if (this.settingsEl.style.display) { 858 this.settingsEl.style.display = ''; 859 } else { 860 this.settingsEl.style.display = 'none'; 861 } 862 }); 863 864 this.meta = this.pageData.meta ? this.pageData.meta : {}; 865 this.editData = {}; 866 this.savePointData = {}; 867 868 if (!this.pageData.page) { 869 this.publishPageButton.style.display = "none"; 870 } else if (parseInt(this.pageData.page.published)) { 871 this.publishPageButton.innerText = 'Unpublish'; 872 } 873 } 874 875 setSavePoint() { 876 document.querySelectorAll('[sl-type="heading"],[sl-type="editor"],[sl-type="text"]').forEach(node => { 877 this.savePointData[this.getFieldName(node)] = node.innerHTML; 878 }); 879 880 document.querySelectorAll('[sl-type="upload"]').forEach(node => { 881 let imageEl = node.querySelector('img'); 882 883 if (imageEl) { 884 this.savePointData[this.getFieldName(node)] = imageEl.src; 885 } 886 }); 887 } 888 889 loadSavePoint() { 890 document.querySelectorAll('[sl-type="heading"],[sl-type="editor"],[sl-type="text"]').forEach(node => { 891 node.innerHTML = this.savePointData[this.getFieldName(node)]; 892 }); 893 894 document.querySelectorAll('[sl-type="upload"]').forEach(node => { 895 let imageEl = node.querySelector('img'); 896 897 if (imageEl) { 898 imageEl.src = this.savePointData[this.getFieldName(node)]; 899 } 900 }); 901 902 this.dirty = false; 903 } 904 905 async saveData(section, id, data) { 906 let formData = new FormData(); 907 908 if (!this.pageData.catcher) { 909 formData.append('published', 1); 910 } 911 912 let params = new URLSearchParams(); 913 params.append('cmd', 'save'); 914 params.append('section', section); 915 params.append('id', id); 916 917 for (const [name, value] of Object.entries(data)) { 918 formData.append(name, value); 919 params.append('fields[]', name); 920 } 921 922 let url = '/_lib/api/v2/?' + params.toString(); 923 924 let response = await fetch(url, { 925 method: 'post', 926 body: formData 927 }); 928 929 let result = await response.json(); 930 931 if (result.error) { 932 alert(result.error) 933 return false; 934 } 935 } 936 937 getFieldName(node) { 938 let name = node.getAttribute('sl-name'); 939 940 if (!name) { 941 name = node.getAttribute('sl-id'); 942 } 943 944 return name; 945 } 946 947 editorChangeHandler(e, editor) { 948 let node = e.target.bodyElement; 949 let name = node.getAttribute('sl-name'); 950 let obj = this.meta; 951 952 if (!name) { 953 name = node.getAttribute('sl-id'); 954 obj = this.editData; 955 } 956 957 this.setValue(name, editor.getContent(), obj); 958 } 959 960 checkChanges(e) { 961 if (this.dirty) { 962 e.preventDefault(); 963 return "Are you sure you want to leave? You have unsaved changes."; 964 } 965 } 966 967 clickHandler(e) { 968 // close when clicking outside 969 if (!this.uploadsEl.contains(e.target)) { // Check if clicked element is not a descendant of the overlay 970 this.uploadsEl.style.display = "none"; // Hide the overlay 971 } 972 973 // todo add handlers for duplicate and sort here 974 if (e.target.hasAttribute('sl-delete-element')) { 975 let confirmed = confirm('Delete this element?'); 976 977 if (!confirmed) { 978 return; 979 } 980 981 let node = e.target.closest('[sl-name]'); 982 983 this.updateProperty(node, (obj, arrayName, key) => { 984 let arr = obj[arrayName]; 985 arr.splice(key, 1); 986 }); 987 988 node.remove(); 989 990 this.setDirty(true); 991 992 } else if (e.target.hasAttribute('sl-duplicate-element')) { 993 let node = e.target.closest('[sl-name]'); 994 995 this.updateProperty(node, (obj, arrayName, key) => { 996 let arr = obj[arrayName]; 997 const firstPart = arr.slice(0, key + 1); 998 const duplicatedElement = arr[key]; 999 const secondPart = arr.slice(key + 1); 1000 1001 obj[arrayName] = firstPart.concat(duplicatedElement, secondPart); 1002 }); 1003 1004 node.parentNode.insertBefore(node.cloneNode(true), node.nextSibling); 1005 1006 this.setDirty(true); 1007 1008 } else if (e.target.hasAttribute('sl-move-down')) { 1009 let node = e.target.closest('[sl-name]'); 1010 1011 this.updateProperty(node, (obj, arrayName, key) => { 1012 let arr = obj[arrayName]; 1013 const item = arr.splice(key, 1)[0]; 1014 arr.splice(key + 1, 0, item); 1015 }); 1016 1017 node.parentNode.insertBefore(node, node.nextElementSibling.nextSibling); 1018 1019 this.setDirty(true); 1020 } else if (e.target.hasAttribute('sl-move-up')) { 1021 let node = e.target.closest('[sl-name]'); 1022 1023 this.updateProperty(node, (obj, arrayName, key) => { 1024 let arr = obj[arrayName]; 1025 const item = arr.splice(key, 1)[0]; 1026 arr.splice(key - 1, 0, item); 1027 }); 1028 1029 node.parentNode.insertBefore(node, node.previousElementSibling); 1030 1031 this.setDirty(true); 1032 } 1033 } 1034
1035 chooseImage(target) { 1036 this.uploadCallback = target; 1037 this.uploadsEl.style.display = ''; 1038 this.fetchImages(); 1039 } 1040 1041 updateProperty(node, cb) { 1042 let name = node.getAttribute('sl-name'); 1043 1044 let parts = name.split('.'); 1045 let obj = this.meta; 1046 parts.forEach((part, index) => { 1047 const regex = /^(.+?)\[(\d+)\]$/; 1048 const match = part.match(regex); 1049 1050 if (match) { 1051 const arrayName = match[1]; // "slides" 1052 const key = parseInt(match[2]); // 0 (as a number) 1053 1054 let arr = obj[arrayName]; 1055 1056 if (index + 1 == parts.length) { 1057 cb(obj, arrayName, key); 1058 } 1059 1060 obj = arr; 1061 } else { 1062 obj = obj[part]; 1063 } 1064 }) 1065 } 1066 1067 /* 1068 async fetchData() { 1069 // get data 1070 let url = '/_lib/api/v2/?section=cms_pages&fields[page_name]=' + this.pageData.request; 1071 let response = await fetch(url); 1072 1073 let data = await response.json(); 1074 console.log(data) 1075 1076 if (!data.data[0].id) { 1077 this.editPageButton.disabled = true; 1078 this.renamePageButton.disabled = true; 1079 this.deletePageButton.disabled = true; 1080 this.setDirty(false); 1081 } else { 1082 this.data = data.data[0]; 1083 this.meta = this.data.meta ? JSON.parse(this.data.meta): {}; 1084 console.log(this.meta) 1085 } 1086 this.meta = this.pageData.content; 1087 } 1088 */ 1089 1090 async fetchImages(path) { 1091 // get data 1092 let url = '/_lib/api/v2/?cmd=uploads&path=' + (path ? path : ''); 1093 let response = await fetch(url); 1094 1095 this.uploadsEl.innerHTML = '<h1>Loading..</h1>'; 1096 1097 let data = await response.json(); 1098 1099 this.uploadsEl.innerHTML = ''; 1100 1101 let labelEl = document.createElement('label'); 1102 labelEl.innerText = 'Choose file'; 1103 this.uploadsEl.appendChild(labelEl); 1104 1105 let fileEl = document.createElement('input'); 1106 fileEl.setAttribute('type', 'file'); 1107 fileEl.style.display = 'none'; 1108 labelEl.appendChild(fileEl); 1109 1110 fileEl.addEventListener('change', async () => { 1111 const formData = new FormData(); 1112 1113 console.log('File details:', fileEl); 1114 formData.append('file', fileEl.files[0]); 1115 1116 const result = await fetch('/_lib/api/v2/?cmd=uploads&path=', { 1117 method: 'post', 1118 body: formData 1119 }); 1120 1121 let data = await result.json(); 1122 1123 this.selectFile(data.file.url); 1124 }) 1125 1126 data.items.forEach((item) => { 1127 let node; 1128 1129 if (!item.leaf) { 1130 node = document.createElement('div'); 1131 node.innerText = item.name; 1132 node.classList.add("folder"); 1133 1134 node.addEventListener('click', () => { 1135 this.openFolder(item.id); 1136 }); 1137 } else if (item.thumb) { 1138 node = document.createElement('img'); 1139 node.src = item.thumb; 1140 node.addEventListener('click', () => { 1141 this.selectFile('/uploads/' + item.id); 1142 }); 1143 } else { 1144 return; 1145 } 1146 1147 this.uploadsEl.appendChild(node); 1148 }); 1149 } 1150 1151 openFolder(path) { 1152 this.fetchImages(path); 1153 } 1154 1155 selectFile(id) { 1156 if (typeof this.uploadCallback === 'function') { 1157 this.uploadCallback(id, { title: id }); 1158 } else { 1159 // change child image 1160 let imageEl = this.uploadCallback.querySelector('img'); 1161 1162 if (!imageEl) { 1163 imageEl = document.createElement('img'); 1164 this.uploadCallback.appendChild(imageEl); 1165 } 1166 1167 imageEl.src = id; 1168 1169 // change meta value 1170 let name = this.uploadCallback.getAttribute('sl-name'); 1171 this.setValue(name, id, this.meta); 1172 } 1173 1174 this.uploadsEl.style.display = 'none'; 1175 } 1176 1177 setValue(keyStr, value, obj) { 1178 // find nested object 1179 let parts = keyStr.split('.'); 1180
1181 parts.forEach((part, index) => { 1182 const regex = /^(.+?)\[(\d+)\]$/; 1183 const match = part.match(regex); 1184 1185 if (match) { 1186 const arrayName = match[1]; // "slides" 1187 const key = parseInt(match[2]); // 0 (as a number) 1188 1189 if (!Array.isArray(obj[arrayName])) { 1190 obj[arrayName] = []; 1191 } 1192 1193 if (typeof obj[arrayName][key] === 'undefined' || Array.isArray(obj[arrayName][key])) { 1194 obj[arrayName][key] = {}; 1195 } 1196 1197 obj = obj[arrayName][key]; 1198 } else { 1199 if (typeof obj[part] === 'undefined') { 1200 obj[part] = {}; 1201 } 1202 1203 if (index + 1 == parts.length) { 1204 // set value 1205 obj[part] = value; 1206 } 1207 1208 obj = obj[part]; 1209 } 1210 }) 1211 1212 this.setDirty(true); 1213 } 1214 1215 setDirty(dirty) { 1216 this.savePageButton.disabled = !dirty; 1217 this.dirty = dirty; 1218 } 1219 1220 strToPageName(pageName) { 1221 // Remove odd characters (except whitespace, A-Z, a-z, 0-9, ., -, /, and ()) 1222 pageName = pageName.toLowerCase().replace(/[^a-zA-Z0-9\s\.\-\/\(\)]/g, ""); 1223 1224 // Replace ">" with "/" 1225 pageName = pageName.replace(/>/g, "/"); 1226 1227 // Trim leading and trailing whitespace 1228 pageName = pageName.trim(); 1229 1230 // Replace spaces with dashes 1231 pageName = pageName.replace(/\s+/g, "-"); 1232 1233 // Remove trailing dot (.) 1234 pageName = pageName.replace(/\.$/, ""); 1235 1236 return pageName; 1237 } 1238} 1239 1240/* backcompat*/ 1241function clearFile(field) { 1242 inputFile = document.createElement("input"); 1243 inputFile.setAttribute('name', field.getAttribute('name')); 1244 inputFile.setAttribute('type', 'file'); 1245 1246 let cell = field.parentNode; 1247 1248 while (cell.childNodes.length >= 1) { 1249 cell.removeChild( cell.firstChild ); 1250 } 1251 1252 cell.appendChild(inputFile); 1253}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.