PageSourceSearch

https://woodlandchampions.co.uk/_lib/cms/assets/js/shiftlib.js?v=1790608484

js woodlandchampions.co.uk collected 2026-09-28 15:14:55 UTC 42,017 bytes, 1,253 lines download raw bytes

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        &plus;
190        </button>
191
192        <button type="button" class="icon" sl-editPage>
193        &#128295;
194        </button>
195
196        <button type="button" class="icon" sl-cancelPage style="display: none;">
197        <div style="transform: rotate(180deg);">&#10140;</div>
198        </button>
199
200        <button type="button" class="icon" sl-renamePage style="display: none;">
201        &#9998;
202        </button>
203
204        <button type="button" class="icon" sl-deletePage style="display: none;">
205        &#128465;
206        </button>
207
208        <button type="button" class="icon" sl-savePage disabled style="display: none;">
209        &#128190;
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        &#9881;
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&section=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 = '&#10005;';
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>&#10140;</button>
683                        <button type="button" sl-move-down>&#10140;</button>
684                        <button type="button" sl-duplicate-element style="font-weight: bold;">&plus;</button>
685                        <button type="button" sl-delete-element>&#128465;</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&section=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&section=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.